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