1# -*- coding: utf-8 -*- # 2# Copyright 2019 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Support library to generate Build and BuildTrigger configs.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import unicode_literals 20 21from googlecloudsdk.api_lib.cloudbuild import cloudbuild_util 22from googlecloudsdk.core import properties 23from googlecloudsdk.core.util import times 24 25import six 26 27_VERSION = '$COMMIT_SHA' 28 29_DEFAULT_TAGS = [ 30 'gcp-cloud-build-deploy', 31 'gcp-cloud-build-deploy-gcloud' 32] 33_DEFAULT_PR_PREVIEW_TAGS = [ 34 'gcp-cloud-build-deploy-gcloud', 35 'gcp-cloud-build-deploy-pr-preview', 36] 37_DEFAULT_CLEAN_PREVIEW_TAGS = [ 38 'gcp-cloud-build-deploy-gcloud', 39 'gcp-cloud-build-deploy-clean-preview', 40] 41 42_GKE_DEPLOY_PROD = 'gcr.io/cloud-builders/gke-deploy' 43 44_SUGGESTED_CONFIGS_PATH = '{0}/{1}/suggested' 45_EXPANDED_CONFIGS_PATH = '{0}/{1}/expanded' 46 47# Build substitution variables 48_DOCKERFILE_PATH_SUB_VAR = '_DOCKERFILE_PATH' 49_APP_NAME_SUB_VAR = '_APP_NAME' 50_K8S_YAML_PATH_SUB_VAR = '_K8S_YAML_PATH' 51_EXPOSE_PORT_SUB_VAR = '_EXPOSE_PORT' 52_GKE_CLUSTER_SUB_VAR = '_GKE_CLUSTER' 53_GKE_LOCATION_SUB_VAR = '_GKE_LOCATION' 54_OUTPUT_BUCKET_PATH_SUB_VAR = '_OUTPUT_BUCKET_PATH' 55_K8S_ANNOTATIONS_SUB_VAR = '_K8S_ANNOTATIONS' 56_K8S_NAMESPACE_SUB_VAR = '_K8S_NAMESPACE' 57_PREVIEW_EXPIRY_SUB_VAR = '_PREVIEW_EXPIRY' 58 59_EXPANDED_CONFIGS_PATH_DYNAMIC = _EXPANDED_CONFIGS_PATH.format( 60 '$' + _OUTPUT_BUCKET_PATH_SUB_VAR, '$BUILD_ID') 61_SUGGESTED_CONFIGS_PATH_DYNAMIC = _SUGGESTED_CONFIGS_PATH.format( 62 '$' + _OUTPUT_BUCKET_PATH_SUB_VAR, '$BUILD_ID') 63 64_SAVE_CONFIGS_SCRIPT = ''' 65set -e 66 67if [[ "${output_bucket_path}" ]]; then 68 gsutil -m cp output/expanded/* gs://{expanded} 69 echo "Copied expanded configs to gs://{expanded}" 70 echo "View expanded configs at https://console.cloud.google.com/storage/browser/{expanded}/" 71 if [[ ! "${k8s_yaml_path}" ]]; then 72 gsutil -m cp output/suggested/* gs://{suggested} 73 echo "Copied suggested base configs to gs://{suggested}" 74 echo "View suggested base configs at https://console.cloud.google.com/storage/browser/{suggested}/" 75 fi 76fi 77'''.format( 78 output_bucket_path=_OUTPUT_BUCKET_PATH_SUB_VAR, 79 expanded=_EXPANDED_CONFIGS_PATH_DYNAMIC, 80 k8s_yaml_path=_K8S_YAML_PATH_SUB_VAR, 81 suggested=_SUGGESTED_CONFIGS_PATH_DYNAMIC, 82) 83 84_PREPARE_PREVIEW_DEPLOY_SCRIPT = ''' 85set -e 86 87REPO_NAME_FIXED=$$(echo $REPO_NAME | tr "[:upper:]" "[:lower:]") 88NAMESPACE=preview-$$REPO_NAME_FIXED-$_PR_NUMBER 89 90# Save generated preview namespace to a file for use in other steps 91echo $$NAMESPACE > preview-namespace.txt 92 93gcloud container clusters get-credentials ${cluster} --zone=${location} 94 95# Fail if namespace exists but isn't for this repo name 96NAMESPACE_REF=$$(kubectl get namespace $$NAMESPACE --ignore-not-found) 97if [[ $$NAMESPACE_REF ]]; then 98 EXPECTED_REPO_NAME=$$(kubectl get namespace $$NAMESPACE -o=jsonpath="{{.metadata.annotations.preview/repo-name}}") 99 if [[ $$EXPECTED_REPO_NAME != $REPO_NAME ]]; then 100 echo "Namespace already exists but preview/repo-name annotation does not match: $$EXPECTED_REPO_NAME" 101 exit 1 102 fi 103fi 104 105/gke-deploy prepare \\ 106 --filename=${k8s_yaml_path} \\ 107 --image={image} \\ 108 --app=${app_name} \\ 109 --version=$COMMIT_SHA \\ 110 --namespace=$$NAMESPACE \\ 111 --output=output \\ 112 --annotation=gcb-build-id=$BUILD_ID,${k8s_annotations} \\ 113 --expose=${expose_port} 114''' 115 116_APPLY_PREVIEW_DEPLOY_SCRIPT = ''' 117set -e 118 119NAMESPACE=$$(cat preview-namespace.txt) 120 121/gke-deploy apply \\ 122 --filename=output/expanded \\ 123 --namespace=$$NAMESPACE \\ 124 --cluster=${cluster} \\ 125 --location=${location} \\ 126 --timeout=24h 127'''.format( 128 cluster=_GKE_CLUSTER_SUB_VAR, 129 location=_GKE_LOCATION_SUB_VAR, 130) 131 132# This should be done in a separate step because the date command in 133# _APPLY_PREVIEW_DEPLOY_SCRIPT is busybox date, which can't increment days. 134_ANNOTATE_PREVIEW_NAMESPACE_SCRIPT = ''' 135set -e 136 137NAMESPACE=$$(cat preview-namespace.txt) 138gcloud container clusters get-credentials ${cluster} --zone=${location} 139EXPIRY_EPOCH=$$(date -d "+${preview_expiry} days" "+%s") 140kubectl annotate namespace $$NAMESPACE preview/repo-name=$REPO_NAME preview/expiry=$$EXPIRY_EPOCH --overwrite 141'''.format( 142 cluster=_GKE_CLUSTER_SUB_VAR, 143 location=_GKE_LOCATION_SUB_VAR, 144 preview_expiry=_PREVIEW_EXPIRY_SUB_VAR, 145) 146 147_CLEANUP_PREVIEW_SCRIPT = ''' 148set -e 149 150gcloud container clusters get-credentials ${cluster} --zone=${location} --project=$PROJECT_ID 151 152IFS= 153NAMESPACES="$$(kubectl get namespace -o=jsonpath="{{range .items[?(@.metadata.annotations.preview/repo-name==\\"$REPO_NAME\\")]}}{{.metadata.name}},{{.metadata.annotations.preview/expiry}}{{\\"\\n\\"}}{{end}}")" 154 155if [[ -z $$NAMESPACES ]]; then 156 echo "No preview environments found" 157 exit 158fi 159 160while read -r i; do 161 NAMESPACE=$$(echo $$i | cut -d"," -f1) 162 EXPIRY=$$(echo $$i | cut -d"," -f2) 163 164 if [[ $$(date "+%s") -ge $$EXPIRY ]]; then 165 echo "Deleting expired preview environment in namespace $$NAMESPACE" 166 kubectl delete namespace $$NAMESPACE 167 else 168 echo "Preview environment in namespace $$NAMESPACE expires on $$(date --date="@$$EXPIRY" -u)" 169 fi 170done <<< $$NAMESPACES 171'''.format( 172 cluster=_GKE_CLUSTER_SUB_VAR, 173 location=_GKE_LOCATION_SUB_VAR, 174) 175 176# Build step IDs 177_BUILD_BUILD_STEP_ID = 'Build' 178_PUSH_BUILD_STEP_ID = 'Push' 179_PREPARE_DEPLOY_BUILD_STEP_ID = 'Prepare deploy' 180_SAVE_CONFIGS_BUILD_STEP_ID = 'Save generated Kubernetes configs' 181_APPLY_DEPLOY_BUILD_STEP_ID = 'Apply deploy' 182_ANNOTATE_PREVIEW_NAMESPACE_BUILD_STEP_ID = 'Annotate preview namespace' 183_CLEANUP_PREVIEW_BUILD_STEP_ID = 'Delete expired preview environments' 184 185 186def SuggestedConfigsPath(gcs_config_staging_path, build_id): 187 """Gets the formatted suggested configs path, without the 'gs://' prefix. 188 189 Args: 190 gcs_config_staging_path: The path to a GCS subdirectory where the configs 191 are saved to. 192 build_id: The build_id of the build that creates and saves the configs. 193 194 Returns: 195 Formatted suggested configs path as a string. 196 """ 197 return _SUGGESTED_CONFIGS_PATH.format( 198 gcs_config_staging_path, build_id) 199 200 201def ExpandedConfigsPath(gcs_config_staging_path, build_id): 202 """Gets the formatted expanded configs path, without the 'gs://' prefix. 203 204 Args: 205 gcs_config_staging_path: The path to a GCS subdirectory where the configs 206 are saved to. 207 build_id: The build_id of the build that creates and saves the configs. 208 209 Returns: 210 Formatted expanded configs path as a string. 211 """ 212 return _EXPANDED_CONFIGS_PATH.format( 213 gcs_config_staging_path, build_id) 214 215 216def SaveConfigsBuildStepIsSuccessful(messages, build): 217 """Returns True if the step with _SAVE_CONFIGS_BUILD_STEP_ID id is successful. 218 219 Args: 220 messages: Cloud Build messages module. This is the value returned from 221 cloudbuild_util.GetMessagesModule(). 222 build: The build that contains the step to check. 223 224 Returns: 225 True if the step is successful, else false. 226 """ 227 save_configs_build_step = next(( 228 x for x in build.steps if x.id == _SAVE_CONFIGS_BUILD_STEP_ID 229 ), None) 230 231 status = save_configs_build_step.status 232 return status == messages.BuildStep.StatusValueValuesEnum.SUCCESS 233 234 235def CreateBuild( 236 messages, build_timeout, build_and_push, staged_source, 237 image, dockerfile_path, app_name, app_version, config_path, namespace, 238 expose_port, gcs_config_staging_path, cluster, location, build_tags 239): 240 """Creates the Cloud Build config to run. 241 242 Args: 243 messages: Cloud Build messages module. This is the value returned from 244 cloudbuild_util.GetMessagesModule(). 245 build_timeout: An optional maximum time a build is run before it times out. 246 For example, "2h15m5s" is 2 hours, 15 minutes, and 5 seconds. If you do 247 not specify a unit, seconds is assumed. If this value is None, a timeout 248 is not set. 249 build_and_push: If True, the created build will have Build and Push steps. 250 staged_source: An optional GCS object for a staged source repository. The 251 object must have bucket, name, and generation fields. If this value is 252 None, the created build will not have a source. 253 image: The image that will be deployed and optionally built beforehand. The 254 image can include a tag or digest. 255 dockerfile_path: An optional path to the source repository's Dockerfile, 256 relative to the source repository's root directory. If this value is not 257 provided, 'Dockerfile' is used. 258 app_name: An optional app name that is set to a substitution variable. 259 If this value is None, the substitution variable is set to '' to indicate 260 its absence. 261 app_version: A app version that is set to the deployed application's 262 version. If this value is None, the version will be set to '' to indicate 263 its absence. 264 config_path: An optional path to the source repository's Kubernetes configs, 265 relative to the source repository's root directory that is set to a 266 substitution variable. If this value is None, the substitution variable is 267 set to '' to indicate its absence. 268 namespace: An optional Kubernetes namespace of the cluster to deploy to that 269 is set to a substitution variable. If this value is None, the substitution 270 variable is set to 'default'. 271 expose_port: An optional port that the deployed application listens to that 272 is set to a substitution variable. If this value is None, the substitution 273 variable is set to 0 to indicate its absence. 274 gcs_config_staging_path: An optional path to a GCS subdirectory to copy 275 application configs that is set to a substitution variable. If this value 276 is None, the substitution variable is set to '' to indicate its absence. 277 cluster: The name of the target cluster to deploy to. 278 location: The zone/region of the target cluster to deploy to. 279 build_tags: Tags to append to build tags in addition to default tags. 280 281 Returns: 282 messages.Build, the Cloud Build config. 283 """ 284 285 build = messages.Build() 286 287 if build_timeout is not None: 288 try: 289 # A bare number is interpreted as seconds. 290 build_timeout_secs = int(build_timeout) 291 except ValueError: 292 build_timeout_duration = times.ParseDuration(build_timeout) 293 build_timeout_secs = int(build_timeout_duration.total_seconds) 294 build.timeout = six.text_type(build_timeout_secs) + 's' 295 296 if staged_source: 297 build.source = messages.Source( 298 storageSource=messages.StorageSource( 299 bucket=staged_source.bucket, 300 object=staged_source.name, 301 generation=staged_source.generation 302 ) 303 ) 304 305 if config_path is None: 306 config_path = '' 307 308 if not expose_port: 309 expose_port = '0' 310 else: 311 expose_port = six.text_type(expose_port) 312 if app_version is None: 313 app_version = '' 314 315 build.steps = [] 316 317 if build_and_push: 318 build.steps.append(_BuildBuildStep(messages, image)) 319 build.steps.append(_PushBuildStep(messages, image)) 320 321 build.steps.append(messages.BuildStep( 322 id=_PREPARE_DEPLOY_BUILD_STEP_ID, 323 name=_GKE_DEPLOY_PROD, 324 args=[ 325 'prepare', 326 '--filename=${}'.format(_K8S_YAML_PATH_SUB_VAR), 327 '--image={}'.format(image), 328 '--app=${}'.format(_APP_NAME_SUB_VAR), 329 '--version={}'.format(app_version), 330 '--namespace=${}'.format(_K8S_NAMESPACE_SUB_VAR), 331 '--output=output', 332 '--annotation=gcb-build-id=$BUILD_ID,${}'.format( 333 _K8S_ANNOTATIONS_SUB_VAR), # You cannot embed a substitution 334 # variable in another, so gcb-build-id=$BUILD_ID must be hard-coded. 335 '--expose=${}'.format(_EXPOSE_PORT_SUB_VAR), 336 '--create-application-cr', 337 '--links="Build details=https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=$PROJECT_ID"', 338 ], 339 )) 340 build.steps.append(_SaveConfigsBuildStep(messages)) 341 build.steps.append(messages.BuildStep( 342 id=_APPLY_DEPLOY_BUILD_STEP_ID, 343 name=_GKE_DEPLOY_PROD, 344 args=[ 345 'apply', 346 '--filename=output/expanded', 347 '--namespace=${}'.format(_K8S_NAMESPACE_SUB_VAR), 348 '--cluster=${}'.format(_GKE_CLUSTER_SUB_VAR), 349 '--location=${}'.format(_GKE_LOCATION_SUB_VAR), 350 '--timeout=24h' # Set this to max value allowed for a build so that 351 # this step never times out. We prefer the timeout given to the build 352 # to take precedence. 353 ], 354 )) 355 356 substitutions = _BaseBuildSubstitutionsDict(dockerfile_path, app_name, 357 config_path, expose_port, cluster, 358 location, gcs_config_staging_path) 359 if namespace is None: 360 namespace = 'default' 361 substitutions[_K8S_NAMESPACE_SUB_VAR] = namespace 362 363 build.substitutions = cloudbuild_util.EncodeSubstitutions( 364 substitutions, messages) 365 366 build.tags = _DEFAULT_TAGS[:] 367 if build_tags: 368 for tag in build_tags: 369 build.tags.append(tag) 370 371 build.options = messages.BuildOptions() 372 build.options.substitutionOption = messages.BuildOptions.SubstitutionOptionValueValuesEnum.ALLOW_LOOSE 373 374 if build_and_push: 375 build.images = [image] 376 377 build.artifacts = messages.Artifacts( 378 objects=messages.ArtifactObjects( 379 location='gs://' + _EXPANDED_CONFIGS_PATH_DYNAMIC, 380 paths=['output/expanded/*'] 381 ) 382 ) 383 384 return build 385 386 387def CreateGitPushBuildTrigger( 388 messages, name, description, build_timeout, 389 csr_repo_name, github_repo_owner, github_repo_name, 390 branch_pattern, tag_pattern, 391 image, dockerfile_path, app_name, config_path, namespace, 392 expose_port, gcs_config_staging_path, cluster, location, build_tags, 393 build_trigger_tags 394): 395 """Creates the Cloud BuildTrigger config that deploys an application when triggered by a git push. 396 397 Args: 398 messages: Cloud Build messages module. This is the value returned from 399 cloudbuild_util.GetMessagesModule(). 400 name: Trigger name, which must be unique amongst all triggers in a project. 401 description: Trigger description. 402 build_timeout: An optional maximum time a triggered build is run before it 403 times out. For example, "2h15m5s" is 2 hours, 15 minutes, and 5 seconds. 404 If you do not specify a unit, seconds is assumed. If this value is None, a 405 timeout is not set. 406 csr_repo_name: An optional CSR repo name to be used in the trigger's 407 triggerTemplate field. If this field is provided, github_repo_owner and 408 github_repo_name should not be provided. Either csr_repo_name or both 409 github_repo_owner and github_repo_name must be provided. 410 github_repo_owner: An optional GitHub repo owner to be used in the trigger's 411 github field. If this field is provided, github_repo_name must be provided 412 and csr_repo_name should not be provided. Either csr_repo_name or both 413 github_repo_owner and github_repo_name must be provided. 414 github_repo_name: An optional GitHub repo name to be used in the trigger's 415 github field. If this field is provided, github_repo_owner must be 416 provided and csr_repo_name should not be provided. Either csr_repo_name or 417 both github_repo_owner and github_repo_name must be provided. 418 branch_pattern: An optional regex value to be used to trigger. If this value 419 if provided, tag_pattern should not be provided. branch_pattern or 420 tag_pattern must be provided. 421 tag_pattern: An optional regex value to be used to trigger. If this value 422 if provided, branch_pattern should not be provided. branch_pattern or 423 tag_pattern must be provided. 424 image: The image that will be built and deployed. The image can include a 425 tag or digest. 426 dockerfile_path: An optional path to the source repository's Dockerfile, 427 relative to the source repository's root directory that is set to a 428 substitution variable. If this value is not provided, 'Dockerfile' is 429 used. 430 app_name: An optional app name that is set to a substitution variable. 431 If this value is None, the substitution variable is set to '' to indicate 432 its absence. 433 config_path: An optional path to the source repository's Kubernetes configs, 434 relative to the source repository's root directory that is set to a 435 substitution variable. If this value is None, the substitution variable is 436 set to '' to indicate its absence. 437 namespace: An optional Kubernetes namespace of the cluster to deploy to that 438 is set to a substitution variable. If this value is None, the substitution 439 variable is set to 'default'. 440 expose_port: An optional port that the deployed application listens to that 441 is set to a substitution variable. If this value is None, the substitution 442 variable is set to 0 to indicate its absence. 443 gcs_config_staging_path: An optional path to a GCS subdirectory to copy 444 application configs that is set to a substitution variable. If this value 445 is None, the substitution variable is set to '' to indicate its absence. 446 cluster: The name of the target cluster to deploy to that is set to a 447 substitution variable. 448 location: The zone/region of the target cluster to deploy to that is set to 449 a substitution variable. 450 build_tags: Tags to append to build tags in addition to default tags. 451 build_trigger_tags: Tags to append to build trigger tags in addition to 452 default tags. 453 454 Returns: 455 messages.BuildTrigger, the Cloud BuildTrigger config. 456 """ 457 458 substitutions = _BaseBuildSubstitutionsDict(dockerfile_path, app_name, 459 config_path, expose_port, cluster, 460 location, gcs_config_staging_path) 461 if namespace is None: 462 namespace = 'default' 463 substitutions[_K8S_NAMESPACE_SUB_VAR] = namespace 464 465 build_trigger = messages.BuildTrigger( 466 name=name, 467 description=description, 468 build=CreateBuild(messages, build_timeout, True, None, image, 469 dockerfile_path, app_name, _VERSION, config_path, 470 namespace, expose_port, gcs_config_staging_path, 471 cluster, location, build_tags), 472 substitutions=cloudbuild_util.EncodeTriggerSubstitutions( 473 substitutions, 474 messages)) 475 476 if csr_repo_name: 477 build_trigger.triggerTemplate = messages.RepoSource( 478 projectId=properties.VALUES.core.project.Get(required=True), 479 repoName=csr_repo_name, 480 branchName=branch_pattern, 481 tagName=tag_pattern 482 ) 483 elif github_repo_owner and github_repo_name: 484 build_trigger.github = messages.GitHubEventsConfig( 485 owner=github_repo_owner, 486 name=github_repo_name, 487 push=messages.PushFilter( 488 branch=branch_pattern, 489 tag=tag_pattern 490 ) 491 ) 492 493 build_trigger.tags = _DEFAULT_TAGS[:] 494 if build_trigger_tags: 495 for tag in build_trigger_tags: 496 build_trigger.tags.append(tag) 497 498 return build_trigger 499 500 501def CreatePRPreviewBuildTrigger( 502 messages, name, description, build_timeout, 503 github_repo_owner, github_repo_name, 504 pr_pattern, preview_expiry_days, comment_control, 505 image, dockerfile_path, app_name, config_path, expose_port, 506 gcs_config_staging_path, cluster, location, build_tags, 507 build_trigger_tags 508): 509 """Creates the Cloud BuildTrigger config that deploys an application when triggered by a PR create/update. 510 511 Args: 512 messages: Cloud Build messages module. This is the value returned from 513 cloudbuild_util.GetMessagesModule(). 514 name: Trigger name, which must be unique amongst all triggers in a project. 515 description: Trigger description. 516 build_timeout: An optional maximum time a triggered build is run before it 517 times out. For example, "2h15m5s" is 2 hours, 15 minutes, and 5 seconds. 518 If you do not specify a unit, seconds is assumed. If this value is None, a 519 timeout is not set. 520 github_repo_owner: A GitHub repo owner to be used in the trigger's github 521 field. 522 github_repo_name: A GitHub repo name to be used in the trigger's github 523 field. 524 pr_pattern: A regex value that is the base branch that the PR is targeting, 525 which triggers the creation of the PR preview deployment. 526 preview_expiry_days: How long a deployed preview application can exist 527 before it is expired, in days, that is set to a substitution variable. 528 comment_control: Whether or not a user must comment /gcbrun to trigger 529 the deployment build. 530 image: The image that will be built and deployed. The image can include a 531 tag or digest. 532 dockerfile_path: An optional path to the source repository's Dockerfile, 533 relative to the source repository's root directory that is set to a 534 substitution variable. If this value is not provided, 'Dockerfile' is 535 used. 536 app_name: An optional app name that is set to a substitution variable. 537 If this value is None, the substitution variable is set to '' to indicate 538 its absence. 539 config_path: An optional path to the source repository's Kubernetes configs, 540 relative to the source repository's root directory that is set to a 541 substitution variable. If this value is None, the substitution variable is 542 set to '' to indicate its absence. 543 expose_port: An optional port that the deployed application listens to that 544 is set to a substitution variable. If this value is None, the substitution 545 variable is set to 0 to indicate its absence. 546 gcs_config_staging_path: An optional path to a GCS subdirectory to copy 547 application configs that is set to a substitution variable. If this value 548 is None, the substitution variable is set to '' to indicate its absence. 549 cluster: The name of the target cluster to deploy to that is set to a 550 substitution variable. 551 location: The zone/region of the target cluster to deploy to that is set to 552 a substitution variable. 553 build_tags: Tags to append to build tags in addition to default tags. 554 build_trigger_tags: Tags to append to build trigger tags in addition to 555 default tags. 556 557 Returns: 558 messages.BuildTrigger, the Cloud BuildTrigger config. 559 """ 560 561 substitutions = _BaseBuildSubstitutionsDict(dockerfile_path, app_name, 562 config_path, expose_port, cluster, 563 location, gcs_config_staging_path) 564 substitutions[_PREVIEW_EXPIRY_SUB_VAR] = six.text_type(preview_expiry_days) 565 566 build = messages.Build( 567 steps=[ 568 _BuildBuildStep(messages, image), 569 _PushBuildStep(messages, image), 570 messages.BuildStep( 571 id=_PREPARE_DEPLOY_BUILD_STEP_ID, 572 name=_GKE_DEPLOY_PROD, 573 entrypoint='sh', 574 args=[ 575 '-c', 576 _PREPARE_PREVIEW_DEPLOY_SCRIPT.format( 577 image=image, 578 cluster=_GKE_CLUSTER_SUB_VAR, 579 location=_GKE_LOCATION_SUB_VAR, 580 k8s_yaml_path=_K8S_YAML_PATH_SUB_VAR, 581 app_name=_APP_NAME_SUB_VAR, 582 k8s_annotations=_K8S_ANNOTATIONS_SUB_VAR, 583 expose_port=_EXPOSE_PORT_SUB_VAR, 584 ) 585 ] 586 ), 587 _SaveConfigsBuildStep(messages), 588 messages.BuildStep( 589 id=_APPLY_DEPLOY_BUILD_STEP_ID, 590 name=_GKE_DEPLOY_PROD, 591 entrypoint='sh', 592 args=[ 593 '-c', 594 _APPLY_PREVIEW_DEPLOY_SCRIPT 595 ] 596 ), 597 messages.BuildStep( 598 id=_ANNOTATE_PREVIEW_NAMESPACE_BUILD_STEP_ID, 599 name='gcr.io/cloud-builders/kubectl', 600 entrypoint='sh', 601 args=[ 602 '-c', 603 _ANNOTATE_PREVIEW_NAMESPACE_SCRIPT 604 ] 605 ) 606 ], 607 substitutions=cloudbuild_util.EncodeSubstitutions( 608 substitutions, messages), 609 options=messages.BuildOptions( 610 substitutionOption=messages.BuildOptions 611 .SubstitutionOptionValueValuesEnum.ALLOW_LOOSE 612 ), 613 images=[image], 614 artifacts=messages.Artifacts( 615 objects=messages.ArtifactObjects( 616 location='gs://' + _EXPANDED_CONFIGS_PATH_DYNAMIC, 617 paths=['output/expanded/*'] 618 ) 619 ) 620 ) 621 622 if build_timeout is not None: 623 try: 624 # A bare number is interpreted as seconds. 625 build_timeout_secs = int(build_timeout) 626 except ValueError: 627 build_timeout_duration = times.ParseDuration(build_timeout) 628 build_timeout_secs = int(build_timeout_duration.total_seconds) 629 build.timeout = six.text_type(build_timeout_secs) + 's' 630 631 build.tags = _DEFAULT_PR_PREVIEW_TAGS[:] 632 if build_tags: 633 for tag in build_tags: 634 build.tags.append(tag) 635 636 github_config = messages.GitHubEventsConfig( 637 owner=github_repo_owner, 638 name=github_repo_name, 639 pullRequest=messages.PullRequestFilter( 640 branch=pr_pattern 641 ) 642 ) 643 644 if comment_control: 645 github_config.pullRequest.commentControl = messages.PullRequestFilter.CommentControlValueValuesEnum.COMMENTS_ENABLED 646 647 build_trigger = messages.BuildTrigger( 648 name=name, 649 description=description, 650 build=build, 651 github=github_config, 652 substitutions=cloudbuild_util.EncodeTriggerSubstitutions( 653 substitutions, messages) 654 ) 655 656 build_trigger.tags = _DEFAULT_PR_PREVIEW_TAGS[:] 657 if build_trigger_tags: 658 for tag in build_trigger_tags: 659 build_trigger.tags.append(tag) 660 661 return build_trigger 662 663 664def CreateCleanPreviewBuildTrigger(messages, name, description, 665 github_repo_owner, github_repo_name, 666 cluster, location, build_tags, 667 build_trigger_tags): 668 """Creates the Cloud BuildTrigger config that deletes expired preview deployments. 669 670 Args: 671 messages: Cloud Build messages module. This is the value returned from 672 cloudbuild_util.GetMessagesModule(). 673 name: Trigger name, which must be unique amongst all triggers in a project. 674 description: Trigger description. 675 github_repo_owner: A GitHub repo owner to be used in the trigger's github 676 field. 677 github_repo_name: A GitHub repo name to be used in the trigger's github 678 field. 679 cluster: The name of the target cluster to check for expired deployments 680 that is set to a substitution variable. 681 location: The zone/region of the target cluster to check for the expired 682 deployments that is set to a substitution variable. 683 build_tags: Tags to append to build tags in addition to default tags. 684 build_trigger_tags: Tags to append to build trigger tags in addition to 685 default tags. 686 687 Returns: 688 messages.BuildTrigger, the Cloud BuildTrigger config. 689 """ 690 691 substitutions = { 692 _GKE_CLUSTER_SUB_VAR: cluster, 693 _GKE_LOCATION_SUB_VAR: location, 694 } 695 696 build_trigger = messages.BuildTrigger( 697 name=name, 698 description=description, 699 github=messages.GitHubEventsConfig( 700 owner=github_repo_owner, 701 name=github_repo_name, 702 push=messages.PushFilter( 703 branch='$manual-only^', 704 ) 705 ), 706 build=messages.Build( 707 steps=[ 708 messages.BuildStep( 709 id=_CLEANUP_PREVIEW_BUILD_STEP_ID, 710 name='gcr.io/cloud-builders/kubectl', 711 entrypoint='bash', 712 args=[ 713 '-c', 714 _CLEANUP_PREVIEW_SCRIPT 715 ] 716 ) 717 ], 718 substitutions=cloudbuild_util.EncodeSubstitutions( 719 substitutions, messages), 720 timeout='600s' 721 ), 722 substitutions=cloudbuild_util.EncodeTriggerSubstitutions( 723 substitutions, messages) 724 ) 725 726 build_trigger.build.tags = _DEFAULT_CLEAN_PREVIEW_TAGS[:] 727 if build_tags: 728 for tag in build_tags: 729 build_trigger.build.tags.append(tag) 730 731 build_trigger.tags = _DEFAULT_CLEAN_PREVIEW_TAGS[:] 732 if build_trigger_tags: 733 for tag in build_trigger_tags: 734 build_trigger.tags.append(tag) 735 736 return build_trigger 737 738 739def AddAnnotationToPrepareDeployStep(build_trigger, key, value): 740 """Adds an additional annotation key value pair to the Prepare Deploy step, through the substitution variable. 741 742 Args: 743 build_trigger: BuildTrigger config to modify. 744 key: Annotation key. 745 value: Annotation value. 746 """ 747 748 # BuildTrigger 749 annotations_substitution = next(( 750 x for x in build_trigger.substitutions.additionalProperties 751 if x.key == _K8S_ANNOTATIONS_SUB_VAR 752 ), None) 753 annotations_substitution.value = '{},{}={}'.format( 754 annotations_substitution.value, key, value) 755 756 # BuildTrigger.Build 757 annotations_substitution = next(( 758 x for x in build_trigger.build.substitutions.additionalProperties 759 if x.key == _K8S_ANNOTATIONS_SUB_VAR 760 ), None) 761 annotations_substitution.value = '{},{}={}'.format( 762 annotations_substitution.value, key, value) 763 764 765def _BaseBuildSubstitutionsDict(dockerfile_path, app_name, config_path, 766 expose_port, cluster, location, 767 gcs_config_staging_path): 768 """Creates a base dict of substitutions for a Build or BuildTrigger to encode. 769 770 The returned dict contains shared substitutions of Builds and BuildTriggers 771 that this library creates. 772 773 Args: 774 dockerfile_path: Value for _DOCKERFILE_PATH_SUB_VAR substitution variable. 775 app_name: Value for _APP_NAME_SUB_VAR substitution variable. 776 config_path: Value for _K8S_YAML_PATH_SUB_VAR substitution variable. 777 expose_port: Value for _EXPOSE_PORT_SUB_VAR substitution variable. 778 cluster: Value for _GKE_CLUSTER_SUB_VAR substitution variable. 779 location: Value for _GKE_LOCATION_SUB_VAR substitution variable. 780 gcs_config_staging_path: Value for _OUTPUT_BUCKET_PATH_SUB_VAR substitution 781 variable. 782 783 Returns: 784 Dict of substitutions mapped to values. 785 """ 786 if not dockerfile_path: 787 dockerfile_path = 'Dockerfile' 788 789 if config_path is None: 790 config_path = '' 791 792 if not expose_port: 793 expose_port = '0' 794 else: 795 expose_port = six.text_type(expose_port) 796 797 return { 798 _DOCKERFILE_PATH_SUB_VAR: dockerfile_path, 799 _APP_NAME_SUB_VAR: app_name, 800 _K8S_YAML_PATH_SUB_VAR: config_path, 801 _EXPOSE_PORT_SUB_VAR: expose_port, 802 _GKE_CLUSTER_SUB_VAR: cluster, 803 _GKE_LOCATION_SUB_VAR: location, 804 _OUTPUT_BUCKET_PATH_SUB_VAR: gcs_config_staging_path, 805 _K8S_ANNOTATIONS_SUB_VAR: '', 806 } 807 808 809def _BuildBuildStep(messages, image): 810 return messages.BuildStep( 811 id=_BUILD_BUILD_STEP_ID, 812 name='gcr.io/cloud-builders/docker', 813 args=[ 814 'build', 815 '--network', 816 'cloudbuild', 817 '--no-cache', 818 '-t', 819 image, 820 '-f', 821 '${}'.format(_DOCKERFILE_PATH_SUB_VAR), 822 '.' 823 ] 824 ) 825 826 827def _PushBuildStep(messages, image): 828 return messages.BuildStep( 829 id=_PUSH_BUILD_STEP_ID, 830 name='gcr.io/cloud-builders/docker', 831 args=[ 832 'push', 833 image, 834 ] 835 ) 836 837 838def _SaveConfigsBuildStep(messages): 839 return messages.BuildStep( 840 id=_SAVE_CONFIGS_BUILD_STEP_ID, 841 name='gcr.io/cloud-builders/gsutil', 842 entrypoint='bash', 843 args=[ 844 '-c', 845 _SAVE_CONFIGS_SCRIPT 846 ] 847 ) 848