├── colors-e2e ├── .gitignore ├── colors-fd │ ├── go.mod │ ├── Dockerfile │ ├── skaffold.yaml.template │ ├── cloudbuild.yaml.template │ ├── k8s.yaml.template │ └── main.go ├── colors-be │ ├── Dockerfile │ ├── cloudbuild.yaml.template │ ├── go.mod │ ├── k8s.yaml.template │ ├── skaffold.yaml.template │ └── main.go ├── update_files_with_vars.sh ├── README.md └── clouddeploy.yaml.template ├── CONTRIBUTING ├── packages ├── README.md ├── secrets │ └── secrets.go ├── cdenv │ ├── cdenv_test.go │ └── cdenv.go └── gcs │ └── gcs.go ├── custom-targets ├── helm │ ├── quickstart │ │ ├── configuration │ │ │ ├── mychart │ │ │ │ ├── values.yaml │ │ │ │ ├── .helmignore │ │ │ │ ├── templates │ │ │ │ │ └── deployment.yaml │ │ │ │ └── Chart.yaml │ │ │ └── skaffold.yaml │ │ ├── clouddeploy.yaml │ │ └── QUICKSTART.md │ ├── build_and_register.sh │ ├── helm-deployer │ │ ├── Dockerfile │ │ ├── main.go │ │ ├── params.go │ │ └── cmd.go │ └── README.md ├── git-ops │ ├── quickstart │ │ ├── configuration │ │ │ ├── k8s-pod.yaml │ │ │ └── skaffold.yaml │ │ └── clouddeploy.yaml │ ├── build_and_register.sh │ └── git-deployer │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── providers │ │ ├── provider.go │ │ ├── github.go │ │ └── gitlab.go │ │ ├── cmd.go │ │ ├── git.go │ │ └── main.go ├── README.md ├── vertex-ai-pipeline │ ├── quickstart │ │ ├── configuration │ │ │ ├── production │ │ │ │ └── pipelineJob.yaml │ │ │ ├── staging │ │ │ │ └── pipelineJob.yaml │ │ │ └── skaffold.yaml │ │ └── clouddeploy.yaml │ ├── build_and_register.sh │ └── pipeline-deployer │ │ ├── vertexai_test.go │ │ ├── deploy_test.go │ │ ├── Dockerfile │ │ ├── main.go │ │ ├── vertexai.go │ │ ├── render_test.go │ │ └── deploy.go ├── terraform │ ├── build_and_register.sh │ ├── terraform-deployer │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── params.go │ │ └── main.go │ └── quickstart │ │ ├── configuration │ │ ├── skaffold.yaml │ │ ├── environments │ │ │ ├── dev │ │ │ │ ├── variables.tf │ │ │ │ ├── main.tf │ │ │ │ └── outputs.tf │ │ │ └── prod │ │ │ │ ├── variables.tf │ │ │ │ ├── main.tf │ │ │ │ └── outputs.tf │ │ └── network-module │ │ │ ├── providers.tf │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ └── clouddeploy.yaml ├── vertex-ai │ ├── build_and_register.sh │ ├── quickstart │ │ ├── configuration │ │ │ ├── prod │ │ │ │ └── deployedModel.yaml │ │ │ └── skaffold.yaml │ │ ├── clouddeploy.yaml │ │ └── replace_variables.sh │ └── model-deployer │ │ ├── Dockerfile │ │ ├── main.go │ │ ├── polling.go │ │ ├── addaliases.go │ │ └── vertexai_test.go ├── util │ ├── cloudbuild.yaml │ ├── applysetters │ │ └── walk.go │ └── build_and_register.sh └── infrastructure-manager │ ├── build_and_register.sh │ ├── im-deployer │ ├── .gitignore │ ├── Dockerfile │ └── main.go │ └── quickstart │ ├── configuration │ ├── skaffold.yaml │ ├── dev │ │ ├── variables.tf │ │ ├── main.tf │ │ └── outputs.tf │ └── prod │ │ ├── variables.tf │ │ ├── main.tf │ │ └── outputs.tf │ └── clouddeploy.yaml ├── .github ├── trusted-contribution.yml └── workflows │ └── ci.yaml ├── postdeploy-hooks └── k8s-cleanup │ ├── quickstart │ └── configuration │ │ ├── kubernetes.yaml │ │ ├── clouddeploy.yaml │ │ └── skaffold.yaml │ ├── util.go │ ├── util_test.go │ ├── Dockerfile │ ├── results.go │ ├── executor.go │ ├── kubectl_test.go │ ├── README.md │ └── main.go ├── basic-deploy ├── skaffold.yaml ├── k8.yaml ├── README.md └── clouddeploy.yaml ├── verify-evaluate-cloud-metrics ├── configuration │ ├── run.yaml │ ├── clouddeploy.yaml │ └── skaffold.yaml └── Dockerfile ├── presubmit.yaml └── README.md /colors-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | .tmp -------------------------------------------------------------------------------- /colors-e2e/colors-fd/go.mod: -------------------------------------------------------------------------------- 1 | module color-frontdoor 2 | 3 | go 1.24 4 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | This project does not accept contributions at this time -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | These packages are helper packages to be used by the actual samples. They are 3 | not Cloud Deploy samples. -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/configuration/mychart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for mychart. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 -------------------------------------------------------------------------------- /.github/trusted-contribution.yml: -------------------------------------------------------------------------------- 1 | trustedContributors: 2 | - "dependabot[bot]" 3 | - "cloud-deploy-samples-bot" 4 | - "copybara-service[bot]" 5 | annotations: 6 | # Trigger Cloud Build tests 7 | - type: comment 8 | text: "/gcbrun" 9 | -------------------------------------------------------------------------------- /custom-targets/git-ops/quickstart/configuration/k8s-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: getting-started 5 | spec: 6 | containers: 7 | - name: nginx 8 | image: nginx:1.14.2 9 | ports: 10 | - containerPort: 80 -------------------------------------------------------------------------------- /custom-targets/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Deploy Custom Target Samples 2 | 3 | This directory contains a collection of Cloud Deploy Custom Target samples. Each subdirectory contains the sample implementation for a specific Custom Target and a quickstart to get you started. -------------------------------------------------------------------------------- /colors-e2e/colors-be/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /app 4 | COPY go.mod ./ 5 | RUN go mod download 6 | COPY . . 7 | 8 | RUN go build -o /app/main 9 | 10 | FROM alpine:latest 11 | WORKDIR /app 12 | COPY --from=builder /app/main . 13 | CMD ["/app/main"] -------------------------------------------------------------------------------- /colors-e2e/colors-fd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /app 4 | COPY go.mod ./ 5 | RUN go mod download 6 | COPY . . 7 | 8 | RUN go build -o /app/main 9 | 10 | FROM alpine:latest 11 | WORKDIR /app 12 | COPY --from=builder /app/main . 13 | CMD ["/app/main"] -------------------------------------------------------------------------------- /colors-e2e/colors-fd/skaffold.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v3alpha1 2 | kind: Config 3 | metadata: 4 | name: table-ui-demo 5 | build: 6 | artifacts: 7 | - image: $IMAGE_REPO/colors-frontdoor 8 | docker: 9 | dockerfile: Dockerfile 10 | manifests: 11 | rawYaml: 12 | - k8s.yaml 13 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/quickstart/configuration/production/pipelineJob.yaml: -------------------------------------------------------------------------------- 1 | labels: 2 | environment: production 3 | serviceAccount: $PROD_PROJECT_NUMBER-compute@developer.gserviceaccount.com 4 | runtimeConfig: 5 | gcsOutputDirectory: gs://$PROD_BUCKET 6 | failurePolicy: PIPELINE_FAILURE_POLICY_FAIL_FAST -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/quickstart/configuration/staging/pipelineJob.yaml: -------------------------------------------------------------------------------- 1 | labels: 2 | environment: staging 3 | serviceAccount: $STAGING_PROJECT_NUMBER-compute@developer.gserviceaccount.com 4 | runtimeConfig: 5 | gcsOutputDirectory: gs://$STAGING_BUCKET 6 | failurePolicy: PIPELINE_FAILURE_POLICY_FAIL_SLOW 7 | -------------------------------------------------------------------------------- /custom-targets/helm/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_DOCKERFILE_LOCATION="custom-targets/helm/helm-deployer/Dockerfile" 4 | 5 | export _CT_IMAGE_NAME=helm 6 | export _CT_TYPE_NAME=helm 7 | export _CT_CUSTOM_ACTION_NAME=helm-deployer 8 | export _CT_GCS_DIRECTORY=helm 9 | export _CT_SKAFFOLD_CONFIG_NAME=helmConfig 10 | 11 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | run-name: ${{ github.actor }} is running tests 🚀 3 | on: [push] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Setup Go 10 | uses: actions/setup-go@v5 11 | - name: Run all the unit tests 12 | working-directory: . 13 | run: go test ./... -cover -v -------------------------------------------------------------------------------- /custom-targets/git-ops/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_DOCKERFILE_LOCATION="custom-targets/git-ops/git-deployer/Dockerfile" 4 | export _CT_IMAGE_NAME=git 5 | export _CT_TYPE_NAME=git 6 | export _CT_CUSTOM_ACTION_NAME=git-deployer 7 | export _CT_GCS_DIRECTORY=git 8 | export _CT_SKAFFOLD_CONFIG_NAME=gitConfig 9 | export _CT_USE_DEFAULT_RENDERER=true 10 | 11 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" -------------------------------------------------------------------------------- /custom-targets/terraform/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_DOCKERFILE_LOCATION="custom-targets/terraform/terraform-deployer/Dockerfile" 4 | export _CT_IMAGE_NAME=terraform 5 | export _CT_TYPE_NAME=terraform 6 | export _CT_CUSTOM_ACTION_NAME=terraform-deployer 7 | export _CT_GCS_DIRECTORY=terraform 8 | export _CT_SKAFFOLD_CONFIG_NAME=terraformConfig 9 | 10 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" 11 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_DOCKERFILE_LOCATION="custom-targets/vertex-ai/model-deployer/Dockerfile" 4 | export _CT_IMAGE_NAME=vertexai 5 | export _CT_TYPE_NAME=vertex-ai-endpoint 6 | export _CT_CUSTOM_ACTION_NAME=vertex-ai-model-deployer 7 | export _CT_GCS_DIRECTORY=vertexai 8 | export _CT_SKAFFOLD_CONFIG_NAME=vertexAiConfig 9 | 10 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_DOCKERFILE_LOCATION="custom-targets/vertex-ai-pipeline/pipeline-deployer/Dockerfile" 4 | export _CT_IMAGE_NAME=vertexai 5 | export _CT_TYPE_NAME=vertex-ai-pipeline 6 | export _CT_CUSTOM_ACTION_NAME=vertex-ai-pipeline-deployer 7 | export _CT_GCS_DIRECTORY=vertexai 8 | export _CT_SKAFFOLD_CONFIG_NAME=vertexAiConfig 9 | 10 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" 11 | 12 | -------------------------------------------------------------------------------- /custom-targets/util/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: [ 4 | 'build', 5 | '--build-arg', 'COMMIT_SHA=$COMMIT_SHA', 6 | '-t', '$LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPO_NAME/$_IMAGE_NAME', 7 | '-f', '$_DOCKERFILE_PATH', 8 | '.' 9 | ] 10 | images: 11 | - '$LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPO_NAME/$_IMAGE_NAME' 12 | options: 13 | logging: CLOUD_LOGGING_ONLY 14 | requestedVerifyOption: VERIFIED 15 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/quickstart/configuration/kubernetes.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: k8s-cleanup-deployment-orig 5 | labels: 6 | app: my-app 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: my-app 13 | template: 14 | metadata: 15 | labels: 16 | app: my-app 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: my-app-image 21 | -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/configuration/mychart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | export _CT_SRCDIR="${SOURCE_DIR}/im-deployer" 6 | export _CT_IMAGE_NAME=infra-manager 7 | export _CT_TYPE_NAME=infrastructure-manager 8 | export _CT_CUSTOM_ACTION_NAME=infra-manager-deployer 9 | export _CT_GCS_DIRECTORY=infra-manager 10 | export _CT_SKAFFOLD_CONFIG_NAME=infraManagerConfig 11 | 12 | "${SOURCE_DIR}/../util/build_and_register.sh" "$@" 13 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // diffSlices returns the elements in slice1 that are not in slice2. 4 | func diffSlices(slice1, slice2 []string) []string { 5 | var diff []string 6 | slice2Map := make(map[string]bool) 7 | 8 | for _, val := range slice2 { 9 | slice2Map[val] = true 10 | } 11 | 12 | for _, val := range slice1 { 13 | // If they're not in slice2 add them to the diff slice. 14 | if !slice2Map[val] { 15 | diff = append(diff, val) 16 | } 17 | } 18 | 19 | return diff 20 | } 21 | -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/configuration/mychart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-nginx 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: nginx 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app: nginx 13 | template: 14 | metadata: 15 | labels: 16 | app: nginx 17 | spec: 18 | containers: 19 | - name: nginx 20 | image: nginx:1.14.2 21 | ports: 22 | - containerPort: 80 -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # OS generate file 25 | .DS_STORE -------------------------------------------------------------------------------- /custom-targets/terraform/terraform-deployer/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # OS generate file 25 | .DS_STORE 26 | -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/im-deployer/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # OS generate file 25 | .DS_STORE 26 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/quickstart/configuration/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: deploy.cloud.google.com/v1 2 | kind: DeliveryPipeline 3 | metadata: 4 | name: k8s-cleanup-qs 5 | description: pipeline that includes a post-deploy hook to clean up resources 6 | serialPipeline: 7 | stages: 8 | - targetId: k8s-cleanup-qs-prod 9 | profiles: [] 10 | strategy: 11 | standard: 12 | postdeploy: 13 | actions: ["cleanup-action"] 14 | --- 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: Target 17 | metadata: 18 | name: k8s-cleanup-qs-prod 19 | description: k8s-cleanup quickstart prod cluster 20 | gke: 21 | cluster: projects/$PROJECT_ID/locations/$REGION/clusters/$CLUSTER_ID 22 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/pipeline-deployer/vertexai_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Tests that pipelineRequestFromManifest fails when given an incorrect path. Does not test correct path or incomplete file! 8 | func TestPipelineRequestFromManifest(t *testing.T) { 9 | _, err := pipelineRequestFromManifest("") 10 | if err == nil { 11 | t.Errorf("Expected: error, Actual: %s", err) 12 | } 13 | 14 | _, err = pipelineRequestFromManifest("testPath") 15 | if err == nil { 16 | t.Errorf("Expected: error, Actual: %s", err) 17 | } 18 | 19 | _, err = pipelineRequestFromManifest(" ") 20 | if err == nil { 21 | t.Errorf("Expected: error, Actual: %s", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v4beta7 16 | kind: Config -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v4beta7 16 | kind: Config -------------------------------------------------------------------------------- /custom-targets/vertex-ai/quickstart/configuration/prod/deployedModel.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | dedicatedResources: 15 | maxReplicaCount: 9 16 | -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v4beta7 16 | kind: Config -------------------------------------------------------------------------------- /basic-deploy/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v2beta16 16 | kind: Config 17 | deploy: 18 | kubectl: 19 | manifests: 20 | - k8.yaml -------------------------------------------------------------------------------- /custom-targets/git-ops/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v4beta7 16 | kind: Config 17 | manifests: 18 | rawYaml: 19 | - k8s-pod.yaml -------------------------------------------------------------------------------- /basic-deploy/k8.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: getting-started 19 | spec: 20 | containers: 21 | - name: echoserver 22 | image: my-app-image -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/configuration/dev/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | variable "project_id" { 18 | description = "The ID of the project for the vpc module" 19 | type = string 20 | } -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/configuration/prod/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | variable "project_id" { 18 | description = "The ID of the project for the vpc module" 19 | type = string 20 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/dev/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | variable "project_id" { 18 | description = "The ID of the project for the network module" 19 | type = string 20 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/prod/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | variable "project_id" { 18 | description = "The ID of the project for the network module" 19 | type = string 20 | } -------------------------------------------------------------------------------- /verify-evaluate-cloud-metrics/configuration/run.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: serving.knative.dev/v1 16 | kind: Service 17 | metadata: 18 | name: hello-app 19 | spec: 20 | template: 21 | spec: 22 | containers: 23 | - image: gcr.io/cloudrun/hello -------------------------------------------------------------------------------- /presubmit.yaml: -------------------------------------------------------------------------------- 1 | # Cloud Build YAML to run all presubmit tasks 2 | # Using a machine with 8 CPUs to speed up the builds. 3 | steps: 4 | - name: docker 5 | script: | 6 | set -e 7 | # Find all directories that contain a file named `Dockerfile` 8 | # The colors-e2e directories are their own modules, so docker builds them using the directory 9 | # as the build context (since their go.mod and go.sum files are within that directory). 10 | # All other directories are part of one module. 11 | for dir in $(find . -type d -exec test -e '{}'/Dockerfile \; -print); do 12 | echo "BUILDING $dir" 13 | if [[ "$dir" == "./colors-e2e/colors-be" ]] || \ 14 | [[ "$dir" == "./colors-e2e/colors-fd" ]]; then 15 | docker build $dir 16 | else 17 | docker build . -f "$dir/Dockerfile" 18 | fi 19 | done 20 | options: 21 | machineType: 'E2_HIGHCPU_8' -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/dev/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | module "network" { 18 | source = "../../network-module" 19 | network_name = "tf-ct-quickstart-dev-network" 20 | description = "This is my dev network" 21 | project_id = var.project_id 22 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/prod/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | module "network" { 18 | source = "../../network-module" 19 | network_name = "tf-ct-quickstart-prod-network" 20 | description = "This is my prod network" 21 | project_id = var.project_id 22 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/network-module/providers.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | terraform { 18 | required_version = ">= 0.13.0" 19 | 20 | required_providers { 21 | google = { 22 | source = "hashicorp/google" 23 | version = ">= 4.64, < 6" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/network-module/main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | resource "google_compute_network" "network" { 18 | name = var.network_name 19 | description = var.description 20 | project = var.project_id 21 | auto_create_subnetworks = var.auto_create_subnetworks 22 | } -------------------------------------------------------------------------------- /custom-targets/vertex-ai/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: skaffold/v4beta7 15 | kind: Config 16 | customActions: 17 | - name: add-aliases 18 | containers: 19 | - name: add-aliases 20 | image: $REGION-docker.pkg.dev/$PROJECT_ID/cd-custom-targets/$_CT_IMAGE_NAME@$IMAGE_SHA 21 | args: ["/bin/vertex-ai-deployer", "--add-aliases-mode"] -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/quickstart/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: skaffold/v4beta7 15 | kind: Config 16 | customActions: 17 | - name: add-aliases 18 | containers: 19 | - name: add-aliases 20 | image: $REGION-docker.pkg.dev/$PROJECT_ID/cd-custom-targets/$_CT_IMAGE_NAME@$IMAGE_SHA 21 | args: ["/bin/vertex-ai-deployer", "--add-aliases-mode"] -------------------------------------------------------------------------------- /colors-e2e/colors-be/cloudbuild.yaml.template: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | script: | 4 | #!/bin/bash 5 | docker build -t $IMAGE_REPO/colors-backend colors-be 6 | docker push $IMAGE_REPO/colors-backend 7 | docker inspect --format='{{index .RepoDigests 0}}' $IMAGE_REPO/colors-backend > sha.txt 8 | 9 | - name: 'gcr.io/cloud-builders/gcloud' 10 | script: | 11 | #!/bin/bash 12 | sha=$( sha.txt 8 | 9 | - name: 'gcr.io/cloud-builders/gcloud' 10 | script: | 11 | #!/bin/bash 12 | sha=$(/locations//clusters/ 4 | STAGING_CLUSTER=projects//locations//clusters/ 5 | PROD1_CLUSTER=projects//locations//clusters/ 6 | PROD2_CLUSTER=projects//locations//clusters/ 7 | COMPUTE_SERVICE_ACCOUNT=-compute@developer.gserviceaccount.com 8 | IMAGE_REPO="Repo for images" # Ex: us-central1-docker.pkg.dev// 9 | GIT_REPO=https://github.com/GoogleCloudPlatform/cloud-deploy-samples 10 | 11 | for file in clouddeploy.yaml \ 12 | colors-fd/cloudbuild.yaml colors-fd/k8s.yaml colors-fd/skaffold.yaml \ 13 | colors-be/cloudbuild.yaml colors-be/k8s.yaml colors-be/skaffold.yaml; do 14 | echo Updating $file 15 | sed 's,\$DEV_CLUSTER,'$DEV_CLUSTER',' $file.template | \ 16 | sed 's,\$STAGING_CLUSTER,'$STAGING_CLUSTER',' | \ 17 | sed 's,\$PROD1_CLUSTER,'$PROD1_CLUSTER',' | \ 18 | sed 's,\$COMPUTE_SERVICE_ACCOUNT,'$COMPUTE_SERVICE_ACCOUNT',' | \ 19 | sed 's,\$IMAGE_REPO,'$IMAGE_REPO',' | \ 20 | sed 's,\$PROD2_CLUSTER,'$PROD2_CLUSTER',' > $file 21 | done -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: helm-pipeline 19 | serialPipeline: 20 | stages: 21 | - targetId: helm-cluster 22 | deployParameters: 23 | - values: 24 | customTarget/helmConfigurationPath: mychart 25 | --- 26 | apiVersion: deploy.cloud.google.com/v1 27 | kind: Target 28 | metadata: 29 | name: helm-cluster 30 | customTarget: 31 | customTargetType: helm 32 | deployParameters: 33 | customTarget/helmGKECluster: projects/$PROJECT_ID/locations/$REGION/clusters/$CLUSTER_ID -------------------------------------------------------------------------------- /colors-e2e/colors-be/go.mod: -------------------------------------------------------------------------------- 1 | module color-backend 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | cloud.google.com/go/compute/metadata v0.3.0 7 | cloud.google.com/go/monitoring v1.17.0 8 | github.com/golang/protobuf v1.5.3 9 | google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b 10 | ) 11 | 12 | require ( 13 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 14 | github.com/google/s2a-go v0.1.7 // indirect 15 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 16 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 17 | go.opencensus.io v0.24.0 // indirect 18 | golang.org/x/crypto v0.45.0 // indirect 19 | golang.org/x/net v0.47.0 // indirect 20 | golang.org/x/oauth2 v0.27.0 // indirect 21 | golang.org/x/sync v0.18.0 // indirect 22 | golang.org/x/sys v0.38.0 // indirect 23 | golang.org/x/text v0.31.0 // indirect 24 | google.golang.org/api v0.149.0 // indirect 25 | google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect 26 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect 27 | google.golang.org/grpc v1.59.0 // indirect 28 | google.golang.org/protobuf v1.33.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /basic-deploy/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: my-app-pipeline 19 | serialPipeline: 20 | stages: 21 | - targetId: staging 22 | - targetId: prod 23 | --- 24 | apiVersion: deploy.cloud.google.com/v1 25 | kind: Target 26 | metadata: 27 | name: staging 28 | gke: 29 | cluster: projects/%PROJECT_ID%/locations/%CLUSTER_LOCATION%/clusters/%CLUSTER_NAME% 30 | --- 31 | apiVersion: deploy.cloud.google.com/v1 32 | kind: Target 33 | metadata: 34 | name: prod 35 | gke: 36 | cluster: projects/%PROJECT_ID%/locations/%CLUSTER_LOCATION%/clusters/%CLUSTER_NAME% -------------------------------------------------------------------------------- /packages/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | // Package secrets contains utilities for accessing secrets from Secret Manager. 2 | package secrets 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "hash/crc32" 8 | 9 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 10 | "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 11 | ) 12 | 13 | // SecretVersionData accesses the Secret Manager SecretVersion and returns the data payload. 14 | func SecretVersionData(ctx context.Context, secretVersion string, smClient *secretmanager.Client) (string, error) { 15 | fmt.Printf("Accessing SecretVersion %s\n", secretVersion) 16 | res, err := smClient.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ 17 | Name: secretVersion, 18 | }) 19 | if err != nil { 20 | return "", fmt.Errorf("failed to access secret version %s: %v", secretVersion, err) 21 | } 22 | crc32c := crc32.MakeTable(crc32.Castagnoli) 23 | // Verify the data checksum 24 | checksum := int64(crc32.Checksum(res.Payload.Data, crc32c)) 25 | if checksum != *res.Payload.DataCrc32C { 26 | return "", fmt.Errorf("data corruption detected with secret version") 27 | } 28 | fmt.Printf("Accessed SecretVersion %s\n", secretVersion) 29 | return string(res.Payload.Data), nil 30 | } 31 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | ARG GCLOUD_VERSION=506.0.0 15 | ARG GO_VERSION=1.24 16 | 17 | FROM golang:${GO_VERSION} AS build 18 | WORKDIR /cleanup 19 | COPY go.mod go.sum ./ 20 | COPY packages/ ./packages/ 21 | COPY postdeploy-hooks/k8s-cleanup/*.go ./postdeploy-hooks/k8s-cleanup/ 22 | RUN go mod download 23 | RUN CGO_ENABLED=0 GOOS=linux go build -o /cleanup-kubernetes-resources ./postdeploy-hooks/k8s-cleanup/ 24 | 25 | FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:${GCLOUD_VERSION} 26 | WORKDIR / 27 | COPY --from=build /cleanup-kubernetes-resources /cleanup-kubernetes-resources 28 | ENTRYPOINT ["/cleanup-kubernetes-resources"] 29 | -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/configuration/dev/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | output "network" { 18 | value = module.vpc.network 19 | description = "The network resource created" 20 | } 21 | 22 | output "network_name" { 23 | value = module.vpc.network.name 24 | description = "The name of the network created" 25 | } 26 | 27 | output "network_id" { 28 | value = module.vpc.network.id 29 | description = "The ID of the network created" 30 | } 31 | 32 | output "network_description" { 33 | value = module.vpc.network.description 34 | description = "The description of the network created" 35 | } -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/configuration/prod/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | output "network" { 18 | value = module.vpc.network 19 | description = "The network resource created" 20 | } 21 | 22 | output "network_name" { 23 | value = module.vpc.network.name 24 | description = "The name of the network created" 25 | } 26 | 27 | output "network_id" { 28 | value = module.vpc.network.id 29 | description = "The ID of the network created" 30 | } 31 | 32 | output "network_description" { 33 | value = module.vpc.network.description 34 | description = "The description of the network created" 35 | } -------------------------------------------------------------------------------- /custom-targets/helm/quickstart/configuration/mychart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: mychart 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/dev/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | output "network" { 18 | value = module.network.network 19 | description = "The network resource created" 20 | } 21 | 22 | output "network_name" { 23 | value = module.network.network.name 24 | description = "The name of the network created" 25 | } 26 | 27 | output "network_id" { 28 | value = module.network.network.id 29 | description = "The ID of the network created" 30 | } 31 | 32 | output "network_description" { 33 | value = module.network.network.description 34 | description = "The description of the network created" 35 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/environments/prod/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | output "network" { 18 | value = module.network.network 19 | description = "The network resource created" 20 | } 21 | 22 | output "network_name" { 23 | value = module.network.network.name 24 | description = "The name of the network created" 25 | } 26 | 27 | output "network_id" { 28 | value = module.network.network.id 29 | description = "The ID of the network created" 30 | } 31 | 32 | output "network_description" { 33 | value = module.network.network.description 34 | description = "The description of the network created" 35 | } -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/network-module/outputs.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | output "network" { 18 | value = google_compute_network.network 19 | description = "The network resource created" 20 | } 21 | 22 | output "network_name" { 23 | value = google_compute_network.network.name 24 | description = "The name of the network created" 25 | } 26 | 27 | output "network_id" { 28 | value = google_compute_network.network.id 29 | description = "The ID of the network created" 30 | } 31 | 32 | output "network_description" { 33 | value = google_compute_network.network.description 34 | description = "The description of the network created" 35 | } -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/results.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "cloud.google.com/go/storage" 10 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/cdenv" 11 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/gcs" 12 | ) 13 | 14 | // postdeployHookResult represents the json data in the results file for a 15 | // postdeploy hook operation. 16 | type postdeployHookResult struct { 17 | Metadata map[string]string `json:"metadata,omitempty"` 18 | } 19 | 20 | // uploadResult uploads the provided deploy result to the Cloud Storage path where Cloud Deploy expects it. 21 | func uploadResult(ctx context.Context, gcsClient *storage.Client, deployHookResult *postdeployHookResult) error { 22 | // Get the GCS URI where the results file should be uploaded. The full path is in the format of 23 | // {outputPath}/{gcs.ResultObjectSuffix}. 24 | outputPath := os.Getenv(cdenv.OutputGCSEnvKey) 25 | uri := fmt.Sprintf("%s/%s", outputPath, gcs.ResultObjectSuffix) 26 | jsonResult, err := json.Marshal(deployHookResult) 27 | if err != nil { 28 | return fmt.Errorf("error marshalling postdeploy hook result: %v", err) 29 | } 30 | if err := gcs.Upload(ctx, gcsClient, uri, &gcs.UploadContent{Data: jsonResult}); err != nil { 31 | return err 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /verify-evaluate-cloud-metrics/configuration/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: Target 17 | metadata: 18 | name: prod-target 19 | description: Cloud Run Prod Service 20 | run: 21 | location: projects/%PROJECT_ID%/locations/%REGION% # To be replaced 22 | --- 23 | apiVersion: deploy.cloud.google.com/v1beta1 24 | kind: DeliveryPipeline 25 | metadata: 26 | name: my-verify-pipeline 27 | description: main application pipeline 28 | serialPipeline: 29 | stages: 30 | - targetId: prod-target 31 | profiles: [] 32 | strategy: 33 | canary: 34 | runtimeConfig: 35 | cloudRun: 36 | automaticTrafficControl: true 37 | canaryDeployment: 38 | percentages: [50] 39 | verify: true 40 | -------------------------------------------------------------------------------- /verify-evaluate-cloud-metrics/configuration/skaffold.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: skaffold/v4beta4 16 | kind: Config 17 | manifests: 18 | rawYaml: 19 | - run.yaml 20 | deploy: 21 | cloudrun: {} 22 | verify: 23 | - name: verify-requests-are-not-5xx 24 | container: 25 | name: verify-requests 26 | image: %IMAGE% # To be replaced 27 | command: ["./verify-evaluate-cloud-metrics"] 28 | args: 29 | - --table-name=cloud_run_revision 30 | - --metric-type=run.googleapis.com/request_count 31 | - --predicates=resource.location=='us-west2',resource.service_name=='hello-app' # filter out metrics for `hello-app` in `us-west2` 32 | - --refresh-period=1m 33 | - --sliding-window=1m 34 | - --max-error-percentage=15 # verify that less than 15% of the requests are 5xx errors 35 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/model-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GO_VERSION=1.24 16 | 17 | FROM golang:${GO_VERSION} AS go-build 18 | ARG COMMIT_SHA=unknown 19 | WORKDIR /app 20 | COPY go.mod go.sum ./ 21 | COPY packages/ ./packages/ 22 | COPY custom-targets/util/ ./custom-targets/util/ 23 | COPY custom-targets/vertex-ai/model-deployer/*.go ./custom-targets/vertex-ai/model-deployer/ 24 | RUN go mod download 25 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /vertex-ai-deployer ./custom-targets/vertex-ai/model-deployer/ 26 | 27 | FROM gcr.io/distroless/static-debian12:latest AS release 28 | COPY --from=go-build /vertex-ai-deployer /bin/vertex-ai-deployer 29 | 30 | CMD ["/bin/vertex-ai-deployer"] -------------------------------------------------------------------------------- /custom-targets/vertex-ai/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: deploy.cloud.google.com/v1 15 | kind: DeliveryPipeline 16 | metadata: 17 | name: vertex-ai-cloud-deploy-pipeline 18 | serialPipeline: 19 | stages: 20 | - targetId: prod-endpoint 21 | strategy: 22 | standard: 23 | postdeploy: 24 | actions: ["add-aliases"] 25 | --- 26 | apiVersion: deploy.cloud.google.com/v1 27 | kind: Target 28 | metadata: 29 | name: prod-endpoint 30 | customTarget: 31 | customTargetType: vertex-ai-endpoint 32 | deployParameters: 33 | customTarget/vertexAIEndpoint: projects/$PROJECT_ID/locations/$REGION/endpoints/$ENDPOINT_ID 34 | customTarget/vertexAIConfigurationPath: "prod/deployedModel.yaml" 35 | customTarget/vertexAIMinReplicaCount: "3" 36 | customTarget/vertexAIAliases: "prod,champion" -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/im-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GO_VERSION=1.24.4 16 | 17 | FROM golang:${GO_VERSION} AS go-build 18 | ARG COMMIT_SHA=unknown 19 | WORKDIR /app 20 | COPY custom-targets/util/ ./custom-targets/util/ 21 | COPY packages/ ./packages/ 22 | COPY go.mod go.sum ./ 23 | COPY custom-targets/infrastructure-manager/im-deployer/*.go ./custom-targets/infrastructure-manager/im-deployer/ 24 | RUN go mod download 25 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /im-deployer ./custom-targets/infrastructure-manager/im-deployer/ 26 | 27 | FROM gcr.io/distroless/static-debian12:latest AS release 28 | COPY --from=go-build /im-deployer /bin/im-deployer 29 | 30 | CMD ["/bin/im-deployer"] -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/pipeline-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GO_VERSION=1.24 16 | 17 | FROM golang:${GO_VERSION} AS go-build 18 | ARG COMMIT_SHA=unknown 19 | WORKDIR /app 20 | COPY go.mod go.sum ./ 21 | COPY packages/ ./packages/ 22 | COPY custom-targets/util/ ./custom-targets/util/ 23 | COPY custom-targets/vertex-ai-pipeline/pipeline-deployer/*.go ./custom-targets/vertex-ai-pipeline/pipeline-deployer/ 24 | RUN go mod download 25 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /vertex-ai-deployer ./custom-targets/vertex-ai-pipeline/pipeline-deployer/ 26 | 27 | 28 | FROM gcr.io/distroless/static-debian12:latest AS release 29 | COPY --from=go-build /vertex-ai-deployer /bin/vertex-ai-deployer 30 | 31 | CMD ["/bin/vertex-ai-deployer"] -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/configuration/network-module/variables.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 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 | */ 16 | 17 | variable "network_name" { 18 | description = "The name of the network being created" 19 | type = string 20 | } 21 | 22 | variable "description" { 23 | description = "Description of the resource" 24 | type = string 25 | } 26 | 27 | variable "project_id" { 28 | description = "The ID of the project where the network will be created" 29 | type = string 30 | } 31 | 32 | variable "auto_create_subnetworks" { 33 | description = "When set to true, the network is created in `auto subnet mode` and it will create a subnet for each region automatically across the 10.128.0.0/9 address range. When set to false, the network is created in `custom subnet mode` so the user can explicitly connect subnetwork resources" 34 | type = bool 35 | default = false 36 | } -------------------------------------------------------------------------------- /colors-e2e/colors-be/k8s.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: colors-be 5 | labels: 6 | app: be 7 | spec: 8 | replicas: 9 9 | selector: 10 | matchLabels: 11 | app: be 12 | template: 13 | metadata: 14 | labels: 15 | app: be 16 | spec: 17 | containers: 18 | - image: $IMAGE_REPO/colors-backend:latest 19 | name: colors-fd 20 | env: 21 | - name: OverrideColor 22 | value: "Green" 23 | - name: FaultPercent 24 | value: "0" # from-param: ${faultPercent} 25 | - name: PodName 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.name 29 | - name: PodNamespace 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.namespace 33 | - name: DeploymentName 34 | valueFrom: 35 | fieldRef: 36 | fieldPath: metadata.labels['app'] 37 | - name: ReleaseId 38 | valueFrom: 39 | fieldRef: 40 | fieldPath: "metadata.labels['deploy.cloud.google.com/release-id']" 41 | ports: 42 | - containerPort: 8080 43 | lifecycle: 44 | preStop: 45 | exec: 46 | command: ["/bin/sh", "-c", "sleep 5"] 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: colors-be-scv 52 | spec: 53 | selector: 54 | app: be 55 | ports: 56 | - protocol: TCP 57 | port: 8080 58 | targetPort: 8080 59 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GCLOUD_VERSION=456.0.0 16 | ARG GO_VERSION=1.24 17 | 18 | FROM golang:${GO_VERSION} AS go-build 19 | ARG COMMIT_SHA=unknown 20 | WORKDIR /app 21 | 22 | COPY go.mod go.sum ./ 23 | COPY packages/ ./packages/ 24 | COPY custom-targets/util/ ./custom-targets/util/ 25 | COPY custom-targets/git-ops/git-deployer/*.go ./custom-targets/git-ops/git-deployer/ 26 | COPY custom-targets/git-ops/git-deployer/providers/*.go ./custom-targets/git-ops/git-deployer/providers/ 27 | RUN go mod download 28 | 29 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /git-deployer ./custom-targets/git-ops/git-deployer 30 | 31 | FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:${GCLOUD_VERSION} AS release 32 | COPY --from=go-build /git-deployer /bin/git-deployer 33 | 34 | CMD ["/bin/git-deployer"] -------------------------------------------------------------------------------- /custom-targets/vertex-ai/quickstart/replace_variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export _CT_IMAGE_NAME=vertexai 4 | 5 | while getopts "p:r:e:t:" arg; do 6 | case "${arg}" in 7 | p) 8 | PROJECT="${OPTARG}" 9 | ;; 10 | r) 11 | REGION="${OPTARG}" 12 | ;; 13 | e) 14 | ENDPOINT="${OPTARG}" 15 | ;; 16 | t) 17 | TMPDIR="${OPTARG}" 18 | ;; 19 | *) 20 | usage 21 | exit 1 22 | ;; 23 | esac 24 | done 25 | 26 | if [[ ! -v PROJECT || ! -v REGION || ! -v ENDPOINT || ! -v TMPDIR ]]; then 27 | usage 28 | exit 1 29 | fi 30 | 31 | # get the location where the custom image was uploaded 32 | AR_REPO=$REGION-docker.pkg.dev/$PROJECT/cd-custom-targets 33 | 34 | # get the image digest of the most recently built image 35 | IMAGE_SHA=$(gcloud -q artifacts docker images describe "${AR_REPO}/${_CT_IMAGE_NAME}:latest" --format 'get(image_summary.digest)') 36 | 37 | 38 | cp clouddeploy.yaml "$TMPDIR"/clouddeploy.yaml 39 | cp -r configuration "$TMPDIR"/configuration 40 | 41 | # replace variables in clouddeploy.yaml with actual values 42 | sed -i "s/\$PROJECT_ID/${PROJECT}/g" "$TMPDIR"/clouddeploy.yaml 43 | sed -i "s/\$REGION/${REGION}/g" "$TMPDIR"/clouddeploy.yaml 44 | sed -i "s/\$ENDPOINT_ID/${ENDPOINT}/g" "$TMPDIR"/clouddeploy.yaml 45 | 46 | # replace variables in configuration/skaffold.yaml with actual values 47 | sed -i "s/\$REGION/${REGION}/g" "$TMPDIR"/configuration/skaffold.yaml 48 | sed -i "s/\$PROJECT_ID/${PROJECT}/g" "$TMPDIR"/configuration/skaffold.yaml 49 | sed -i "s/\$_CT_IMAGE_NAME/${_CT_IMAGE_NAME}/g" "$TMPDIR"/configuration/skaffold.yaml 50 | sed -i "s/\$IMAGE_SHA/${IMAGE_SHA}/g" "$TMPDIR"/configuration/skaffold.yaml 51 | 52 | 53 | -------------------------------------------------------------------------------- /custom-targets/terraform/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: tf-network-pipeline 19 | serialPipeline: 20 | stages: 21 | - targetId: tf-dev 22 | deployParameters: 23 | - values: 24 | customTarget/tfBackendPrefix: terraform/network-state 25 | customTarget/tfConfigurationPath: environments/dev 26 | - targetId: tf-prod 27 | deployParameters: 28 | - values: 29 | customTarget/tfBackendPrefix: terraform/network-state 30 | customTarget/tfConfigurationPath: environments/prod 31 | --- 32 | apiVersion: deploy.cloud.google.com/v1 33 | kind: Target 34 | metadata: 35 | name: tf-dev 36 | customTarget: 37 | customTargetType: terraform 38 | deployParameters: 39 | customTarget/tfBackendBucket: $DEV_BACKEND_BUCKET 40 | TF_VAR_project_id: $PROJECT_ID 41 | --- 42 | apiVersion: deploy.cloud.google.com/v1 43 | kind: Target 44 | metadata: 45 | name: tf-prod 46 | customTarget: 47 | customTargetType: terraform 48 | deployParameters: 49 | customTarget/tfBackendBucket: $PROD_BACKEND_BUCKET 50 | TF_VAR_project_id: $PROJECT_ID 51 | -------------------------------------------------------------------------------- /custom-targets/git-ops/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: git-pipeline 19 | serialPipeline: 20 | stages: 21 | - targetId: git-dev 22 | - targetId: git-prod 23 | --- 24 | apiVersion: deploy.cloud.google.com/v1 25 | kind: Target 26 | metadata: 27 | name: git-dev 28 | customTarget: 29 | customTargetType: git 30 | deployParameters: 31 | customTarget/gitRepo: github.com/$GIT_OWNER/$GIT_REPO 32 | customTarget/gitPath: dev/k8s.yaml 33 | customTarget/gitSecret: projects/$PROJECT_ID/secrets/$SECRET_ID/versions/$SECRET_VERSION 34 | customTarget/gitSourceBranch: deploy 35 | customTarget/gitDestinationBranch: main 36 | --- 37 | apiVersion: deploy.cloud.google.com/v1 38 | kind: Target 39 | metadata: 40 | name: git-prod 41 | customTarget: 42 | customTargetType: git 43 | deployParameters: 44 | customTarget/gitRepo: github.com/$GIT_OWNER/$GIT_REPO 45 | customTarget/gitPath: prod/k8s.yaml 46 | customTarget/gitSecret: projects/$PROJECT_ID/secrets/$SECRET_ID/versions/$SECRET_VERSION 47 | customTarget/gitSourceBranch: deploy 48 | customTarget/gitDestinationBranch: main 49 | -------------------------------------------------------------------------------- /custom-targets/helm/helm-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GCLOUD_VERSION=456.0.0 16 | ARG GO_VERSION=1.24 17 | ARG HELM_VERSION=3.13.2 18 | 19 | FROM golang:${GO_VERSION} AS go-build 20 | ARG COMMIT_SHA=unknown 21 | WORKDIR /app 22 | COPY go.mod go.sum ./ 23 | COPY packages/ ./packages/ 24 | COPY custom-targets/util/ ./custom-targets/util/ 25 | COPY custom-targets/helm/helm-deployer/*.go ./custom-targets/helm/helm-deployer/ 26 | RUN go mod download 27 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /helm-deployer ./custom-targets/helm/helm-deployer 28 | 29 | FROM debian:stable-slim AS dependencies 30 | ARG HELM_VERSION 31 | RUN apt-get update -y && apt-get install unzip wget -y 32 | RUN wget -q https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz 33 | RUN tar xvzf helm-v${HELM_VERSION}-linux-amd64.tar.gz linux-amd64/helm 34 | 35 | FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:${GCLOUD_VERSION} AS release 36 | COPY --from=go-build /helm-deployer /bin/helm-deployer 37 | COPY --from=dependencies /linux-amd64/helm /bin/helm 38 | 39 | CMD ["/bin/helm-deployer"] 40 | -------------------------------------------------------------------------------- /custom-targets/terraform/terraform-deployer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | ARG GO_VERSION=1.24 16 | ARG TERRAFORM_VERSION=1.2.3 17 | 18 | FROM golang:${GO_VERSION} AS go-build 19 | ARG COMMIT_SHA=unknown 20 | WORKDIR /app 21 | COPY go.mod go.sum ./ 22 | COPY packages/ ./packages/ 23 | COPY custom-targets/util/ ./custom-targets/util/ 24 | COPY custom-targets/terraform/terraform-deployer/*.go ./custom-targets/terraform/terraform-deployer/ 25 | RUN go mod download 26 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy.GitCommit=${COMMIT_SHA}" -o /terraform-deployer ./custom-targets/terraform/terraform-deployer/ 27 | 28 | FROM debian:stable-slim AS dependencies 29 | ARG TERRAFORM_VERSION 30 | RUN apt-get update -y && apt-get install unzip wget -y 31 | RUN wget -q https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip 32 | RUN unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip 33 | 34 | FROM gcr.io/distroless/static-debian12:latest AS release 35 | COPY --from=go-build /terraform-deployer /bin/terraform-deployer 36 | COPY --from=dependencies /terraform /bin/terraform 37 | 38 | CMD ["/bin/terraform-deployer"] 39 | -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: deploy.cloud.google.com/v1 16 | kind: DeliveryPipeline 17 | metadata: 18 | name: im-network-pipeline 19 | serialPipeline: 20 | stages: 21 | - targetId: im-dev 22 | deployParameters: 23 | - values: 24 | customTarget/imDeployment: dev-vpc-network 25 | customTarget/imConfigurationPath: dev 26 | - targetId: im-prod 27 | deployParameters: 28 | - values: 29 | customTarget/imDeployment: prod-vpc-network 30 | customTarget/imConfigurationPath: prod 31 | --- 32 | apiVersion: deploy.cloud.google.com/v1 33 | kind: Target 34 | metadata: 35 | name: im-dev 36 | customTarget: 37 | customTargetType: infrastructure-manager 38 | deployParameters: 39 | customTarget/imProject: $PROJECT_ID 40 | customTarget/imLocation: $REGION 41 | customTarget/imVar_project_id: $PROJECT_ID 42 | --- 43 | apiVersion: deploy.cloud.google.com/v1 44 | kind: Target 45 | metadata: 46 | name: im-prod 47 | customTarget: 48 | customTargetType: infrastructure-manager 49 | deployParameters: 50 | customTarget/imProject: $PROJECT_ID 51 | customTarget/imLocation: $REGION 52 | customTarget/imVar_project_id: $PROJECT_ID 53 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/pipeline-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "os" 22 | 23 | "cloud.google.com/go/storage" 24 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 25 | ) 26 | 27 | func main() { 28 | if err := do(); err != nil { 29 | fmt.Printf("err: %v\n", err) 30 | os.Exit(1) 31 | } 32 | fmt.Println("Done!") 33 | } 34 | 35 | func do() error { 36 | ctx := context.Background() 37 | 38 | gcsClient, err := storage.NewClient(ctx) 39 | if err != nil { 40 | return fmt.Errorf("unable to create gcs client: %v", err) 41 | } 42 | 43 | flag.Parse() 44 | 45 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{"CANARY"}) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | params, err := determineParams() 51 | if err != nil { 52 | return fmt.Errorf("unable to parse params: %v", err) 53 | } 54 | 55 | aiPlatformService, err := newAIPlatformService(ctx, params.location) 56 | if err != nil { 57 | return fmt.Errorf("unable to create aiplatform.Service object : %v", err) 58 | } 59 | 60 | handler, err := createRequestHandler(req, params, gcsClient, aiPlatformService) 61 | if err != nil { 62 | return fmt.Errorf("unable to create request handler: %v", err) 63 | } 64 | 65 | return handler.process(ctx) 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Deploy Samples 2 | This project serves as a collection of example configurations focused on Cloud 3 | Deploy. 4 | 5 | Each folder contains its own independent sample application along with a README 6 | file that will document any prerequisites required to use the example. 7 | 8 | Please open a GitHub issue if you find any problems with the examples. 9 | 10 | ## Example configurations 11 | 12 | | Name | README | 13 | | :----------- | :------------------------------- | 14 | | Basic Deploy | [Link](./basic-deploy/README.md) | 15 | | Colors E2E | [Link](./colors-e2e/README.md) | 16 | 17 | ## Custom Targets 18 | 19 | | Sample Type | README | 20 | | :----------------------------------- | :------------------------------------------------------------------------ | 21 | | GitOps Custom Target | [Link](./custom-targets/git-ops/README.md) | 22 | | Helm Custom Target | [Link](./custom-targets/helm/README.md) | 23 | | Infrastructure Manager Custom Target | [Link](./custom-targets/infrastructure-manager/README.md) | 24 | | Terraform Custom Target | [Link](./custom-targets/terraform/README.md) | 25 | | Vertex AI Custom Target | [Link](./custom-targets/vertex-ai/README.md) | 26 | 27 | ## Postdeploy Hooks 28 | 29 | | Type | README | 30 | | :---------- | :------------------------------------------------- | 31 | | K8s Cleanup | [Link](./postdeploy-hooks/k8s-cleanup/README.md) | 32 | 33 | ## Verify 34 | 35 | | Type | README | 36 | | :---------------------------- | :---------------------------------------------------- | 37 | | Verify Evaluate Cloud Metrics | [Link](./verify-evaluate-cloud-metrics/README.md) | 38 | 39 | ## Licensing 40 | Code in this repository is licensed under the Apache 2.0. See [LICENSE](LICENSE). -------------------------------------------------------------------------------- /packages/cdenv/cdenv_test.go: -------------------------------------------------------------------------------- 1 | package cdenv 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestCheckDuplicatesValid(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | envVars []string 13 | wantVars map[string]string 14 | }{ 15 | { 16 | name: "Valid environment variables", 17 | envVars: []string{ 18 | "KEY1=VALUE1", 19 | "KEY2=VALUE2", 20 | }, 21 | wantVars: map[string]string{ 22 | "key1": "VALUE1", 23 | "key2": "VALUE2", 24 | }, 25 | }, 26 | } 27 | 28 | for _, test := range tests { 29 | t.Run(test.name, func(t *testing.T) { 30 | vars, err := CheckDuplicates(test.envVars) 31 | if err != nil { 32 | t.Errorf("checkDuplicates() error = %v", err) 33 | } 34 | if diff := cmp.Diff(vars, test.wantVars); diff != "" { 35 | t.Errorf("checkDuplicates() mismatch (-want +got):\n%s", diff) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestCheckDuplicatesInvalid(t *testing.T) { 42 | tests := []struct { 43 | name string 44 | envVars []string 45 | }{ 46 | { 47 | name: "Empty environment variables", 48 | envVars: []string{ 49 | "", 50 | "", 51 | }, 52 | }, 53 | { 54 | name: "Duplicate environment variable with same case", 55 | envVars: []string{ 56 | "KEY1=VALUE1", 57 | "KEY1=VALUE2", 58 | }, 59 | }, 60 | { 61 | name: "Duplicate environment variable with different cases", 62 | envVars: []string{ 63 | "KEY1=VALUE1", 64 | "key1=VALUE2", 65 | }, 66 | }, 67 | { 68 | name: "Empty environment variable value", 69 | envVars: []string{ 70 | "KEY1VALUE1=", 71 | "KEY2=VALUE2", 72 | }, 73 | }, 74 | { 75 | name: "Empty environment variable key", 76 | envVars: []string{ 77 | "=KEY1VALUE1", 78 | "KEY2=VALUE2", 79 | }, 80 | }, 81 | { 82 | name: "Incorrect env variable format - expected k=v", 83 | envVars: []string{ 84 | "KEY1VALUE1", 85 | "KEY2=VALUE2", 86 | }, 87 | }, 88 | } 89 | for _, test := range tests { 90 | t.Run(test.name, func(t *testing.T) { 91 | _, err := CheckDuplicates(test.envVars) 92 | if err == nil { 93 | t.Errorf("checkDuplicates() error = nil, want error") 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/executor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | // CommandExecutor contains command execution information. 12 | type CommandExecutor struct { 13 | // BinPath is the path of the binary being used for the command (e.g. the path 14 | // to the kubectl binary if the kubectl command is to be used). 15 | binPath string 16 | } 17 | 18 | // CreateCommandExecutor returns a CommandExecutor for the given binary. 19 | func CreateCommandExecutor(binPath string) *CommandExecutor { 20 | ce := &CommandExecutor{ 21 | binPath: binPath, 22 | } 23 | return ce 24 | } 25 | 26 | // execCommand runs the given command and returns the output. 27 | func (ce CommandExecutor) execCommand(args []string) (string, error) { 28 | fmt.Printf("Running the following command: %s %s\n", ce.binPath, args) 29 | cmd := exec.Command(ce.binPath, args...) 30 | // By default set locations to standard error and output (visible in cloud build logs) 31 | cmd.Stderr = os.Stderr 32 | cmd.Stdout = os.Stdout 33 | 34 | // Write error output to two locations simultaneously. This allows seeing the error output 35 | // as the execution is happening (by writing to standard error) and also allows gathering 36 | // all stderr at the end (by also writing to var stderr). 37 | var stderr bytes.Buffer 38 | errWriter := io.MultiWriter(&stderr, cmd.Stderr) 39 | cmd.Stderr = errWriter 40 | 41 | // Write error output to two locations simultaneously. This allows seeing the output 42 | // as the execution is happening (by writing to stdout) and also allows gathering 43 | // all stdout at the end (by also writing to var stdout). 44 | var stdout bytes.Buffer 45 | outWriter := io.MultiWriter(&stdout, cmd.Stdout) 46 | cmd.Stdout = outWriter 47 | 48 | // Start the command 49 | if err := cmd.Start(); err != nil { 50 | return "", fmt.Errorf("failed to start command, err is %w", err) 51 | } 52 | 53 | // Wait for everything to finish. 54 | if err := cmd.Wait(); err != nil { 55 | // Read the stdErr output 56 | errorOutput := stderr.Bytes() 57 | fullErr := fmt.Errorf("error running command: %w\n%s", err, errorOutput) 58 | return "", fullErr 59 | } 60 | return stdout.String(), nil 61 | } 62 | -------------------------------------------------------------------------------- /colors-e2e/colors-be/skaffold.yaml.template: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v4beta4 2 | kind: Config 3 | metadata: 4 | name: table-ui-demo-backend 5 | build: 6 | artifacts: 7 | - image: $IMAGE_REPO/colors-backend 8 | docker: 9 | dockerfile: Dockerfile 10 | manifests: 11 | rawYaml: 12 | - k8s.yaml 13 | verify: 14 | - name: load-test 15 | executionMode: 16 | kubernetesCluster: {} 17 | container: 18 | name: load-test-container 19 | image: fortio/fortio 20 | args: ["load", "-allow-initial-errors","-logger-force-color", "-t", "70s", "-qps", "100", "http://colors-be-scv:8080/color"] 21 | - name: verify-metrics 22 | container: 23 | name: verify-requests 24 | image: $IMAGE_REPO/verify-evaluate-cloud-metrics 25 | command: ["./verify-evaluate-cloud-metrics"] 26 | args: 27 | - --table-name=k8s_pod 28 | - --metric-type=custom.googleapis.com/requests/request_count 29 | - --predicates=resource.cluster_name=='$metricClusterName' 30 | - --refresh-period=0m30s 31 | - --sliding-window=15s 32 | - --time-to-monitor=2m 33 | - --trigger-duration=45s 34 | - --max-error-percentage=10 # verify that less than 10% of the requests are 5xx errors 35 | 36 | profiles: 37 | - name: CANARY # Profile configures the verify job to look at the canary deployment 38 | verify: 39 | - name: load-test 40 | executionMode: 41 | kubernetesCluster: {} 42 | container: 43 | name: load-test-container 44 | image: fortio/fortio 45 | args: ["load", "-allow-initial-errors","-logger-force-color", "-t", "70s", "-qps", "100", "http://colors-be-scv:8080/color"] 46 | - name: verify-metrics 47 | container: 48 | name: verify-requests 49 | image: $IMAGE_REPO/verify-evaluate-cloud-metrics 50 | command: ["./verify-evaluate-cloud-metrics"] 51 | args: 52 | - --table-name=k8s_pod 53 | - --metric-type=custom.googleapis.com/requests/request_count 54 | - --predicates=resource.cluster_name=='$metricClusterName',metric.deployment_name == 'be-canary' 55 | - --refresh-period=0m30s 56 | - --sliding-window=15s 57 | - --time-to-monitor=2m 58 | - --trigger-duration=45s 59 | - --max-error-percentage=10 # verify that less than 10% of the requests are 5xx errors 60 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/quickstart/clouddeploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: deploy.cloud.google.com/v1 15 | kind: DeliveryPipeline 16 | metadata: 17 | name: pipeline-cd 18 | serialPipeline: 19 | stages: 20 | - targetId: staging-environment 21 | profiles: [] 22 | - targetId: prod-environment 23 | profiles: [] 24 | --- 25 | 26 | apiVersion: deploy.cloud.google.com/v1 27 | kind: Target 28 | metadata: 29 | name: staging-environment 30 | customTarget: 31 | customTargetType: vertex-ai-pipeline 32 | deployParameters: 33 | customTarget/vertexAIPipelineJobConfiguration: "staging/pipelineJob.yaml" 34 | customTarget/projectID: "$STAGING_PROJECT_ID" 35 | customTarget/location: "$STAGING_REGION" 36 | customTarget/vertexAIPipelineJobParameterValues: '{ 37 | "preference_dataset": "$STAGING_PREF_DATA", 38 | "prompt_dataset": "$STAGING_PROMPT_DATA", 39 | "large_model_reference": "$LARGE_MODEL_REFERENCE", 40 | "model_display_name": "$MODEL_DISPLAY_NAME" 41 | }' 42 | 43 | 44 | --- 45 | 46 | apiVersion: deploy.cloud.google.com/v1 47 | kind: Target 48 | metadata: 49 | name: prod-environment 50 | customTarget: 51 | customTargetType: vertex-ai-pipeline 52 | deployParameters: 53 | customTarget/vertexAIPipelineJobConfiguration: "production/pipelineJob.yaml" 54 | customTarget/vertexAIPipelineJobParameterValues: '{ 55 | "preference_dataset": "$PROD_PREF_DATA", 56 | "prompt_dataset": "$PROD_PROMPT_DATA", 57 | "large_model_reference": "$LARGE_MODEL_REFERENCE", 58 | "model_display_name": "$MODEL_DISPLAY_NAME" 59 | }' 60 | customTarget/projectID: "$PROD_PROJECT_ID" 61 | customTarget/location: "$PROD_REGION" 62 | 63 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/model-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "cloud.google.com/go/storage" 19 | "context" 20 | "flag" 21 | "fmt" 22 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 23 | "os" 24 | ) 25 | 26 | func main() { 27 | if err := do(); err != nil { 28 | fmt.Printf("err: %v\n", err) 29 | os.Exit(1) 30 | } 31 | fmt.Println("Done!") 32 | } 33 | 34 | func do() error { 35 | ctx := context.Background() 36 | 37 | gcsClient, err := storage.NewClient(ctx) 38 | if err != nil { 39 | return fmt.Errorf("unable to create gcs client: %v", err) 40 | } 41 | 42 | flag.BoolVar(&addAliasesMode, "add-aliases-mode", false, "if enabled, adds aliases set in vertexAIAliases environment variable to the deployed model") 43 | flag.Parse() 44 | 45 | if addAliasesMode { 46 | ah, err := newAliasHandler(gcsClient) 47 | if err != nil { 48 | return fmt.Errorf("unable to create alias handler: %v", err) 49 | } 50 | return ah.process(ctx) 51 | } 52 | 53 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{"CANARY"}) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | params, err := determineParams() 59 | if err != nil { 60 | return fmt.Errorf("unable to parse params: %v", err) 61 | } 62 | 63 | aiPlatformRegion, err := regionFromModel(params.model) 64 | if err != nil { 65 | return fmt.Errorf("unable to parse region from model resource name: %v", err) 66 | } 67 | 68 | aiPlatformService, err := newAIPlatformService(ctx, aiPlatformRegion) 69 | if err != nil { 70 | return fmt.Errorf("unable to create aiplatform.Service object : %v", err) 71 | } 72 | 73 | handler, err := createRequestHandler(req, params, gcsClient, aiPlatformService) 74 | if err != nil { 75 | return fmt.Errorf("unable to create request handler: %v", err) 76 | } 77 | 78 | return handler.process(ctx) 79 | } 80 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/providers/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package providers interacts with the API of a Git provider. 16 | package providers 17 | 18 | import ( 19 | "fmt" 20 | "time" 21 | ) 22 | 23 | // GitProvider interface provides methods for interacting with the API of a Git Provider. 24 | type GitProvider interface { 25 | OpenPullRequest(src, dst, title, body string) (*PullRequest, error) 26 | MergePullRequest(prNo int) (*MergeResponse, error) 27 | } 28 | 29 | // PullRequest represents a pull request resource from a Git provider. 30 | type PullRequest struct { 31 | Number int 32 | } 33 | 34 | // MergeResponse represents the response from a Git provider when merging a pull request. 35 | type MergeResponse struct { 36 | Sha string 37 | } 38 | 39 | // CreateProvider returns an instance of the GitProvider. Returns an error if an unsupported 40 | // provider hostname is provided. 41 | func CreateProvider(hostname, repoName, owner, secret string) (GitProvider, error) { 42 | var provider GitProvider 43 | switch hostname { 44 | case "github.com": 45 | provider = &GitHubProvider{ 46 | Repository: repoName, 47 | Token: secret, 48 | Owner: owner, 49 | } 50 | case "gitlab.com": 51 | provider = &GitLabProvider{ 52 | Repository: repoName, 53 | Token: secret, 54 | Owner: owner, 55 | } 56 | default: 57 | return nil, fmt.Errorf("unsupported git provider: %s", hostname) 58 | } 59 | return provider, nil 60 | } 61 | 62 | func mergePullRequestWithRetries(prNo int, call func(prNo int) (*MergeResponse, error)) (*MergeResponse, error) { 63 | endTime := time.Now().Add(2 * time.Minute) 64 | startWait := time.Second * 2 65 | var mr *MergeResponse 66 | var err error 67 | for attempts := 1; time.Now().Before(endTime); attempts++ { 68 | mr, err = call(prNo) 69 | if err != nil { 70 | time.Sleep(startWait * time.Duration(attempts)) 71 | continue 72 | } 73 | return mr, err 74 | } 75 | return nil, err 76 | } 77 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/kubectl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestKubectlGetArgs(t *testing.T) { 11 | os.Setenv(releaseEnvKey, "myrelease") 12 | os.Setenv(pipelineEnvKey, "mypipeline") 13 | os.Setenv(targetEnvKey, "mytarget") 14 | os.Setenv(projectEnvKey, "myproject") 15 | os.Setenv(locationEnvKey, "us-central1") 16 | labels := "-l deploy.cloud.google.com/delivery-pipeline-id=mypipeline,deploy.cloud.google.com/target-id=mytarget,deploy.cloud.google.com/location=us-central1,deploy.cloud.google.com/project-id=myproject" 17 | labelsWithRelease := "-l deploy.cloud.google.com/release-id=myrelease,deploy.cloud.google.com/delivery-pipeline-id=mypipeline,deploy.cloud.google.com/target-id=mytarget,deploy.cloud.google.com/location=us-central1,deploy.cloud.google.com/project-id=myproject" 18 | 19 | for _, tc := range []struct { 20 | name string 21 | includeReleaseLabel bool 22 | resourceType string 23 | namespace string 24 | wantArgs []string 25 | }{ 26 | { 27 | name: "basic - no release nor namespace", 28 | includeReleaseLabel: false, 29 | resourceType: "foo", 30 | wantArgs: []string{ 31 | "get", 32 | "-o", 33 | "name", 34 | labels, 35 | "foo", 36 | }, 37 | }, 38 | { 39 | name: "with release", 40 | includeReleaseLabel: true, 41 | resourceType: "foo", 42 | wantArgs: []string{ 43 | "get", 44 | "-o", 45 | "name", 46 | labelsWithRelease, 47 | "foo", 48 | }, 49 | }, 50 | { 51 | name: "with namespace", 52 | includeReleaseLabel: false, 53 | resourceType: "foo", 54 | namespace: "mynamespace", 55 | wantArgs: []string{ 56 | "get", 57 | "-o", 58 | "name", 59 | labels, 60 | "--namespace=mynamespace", 61 | "foo", 62 | }, 63 | }, 64 | { 65 | name: "with namespace and release", 66 | includeReleaseLabel: true, 67 | resourceType: "foo", 68 | namespace: "mynamespace", 69 | wantArgs: []string{ 70 | "get", 71 | "-o", 72 | "name", 73 | labelsWithRelease, 74 | "--namespace=mynamespace", 75 | "foo", 76 | }, 77 | }, 78 | } { 79 | t.Run(tc.name, func(t *testing.T) { 80 | gotArgs := kubectlGetArgs(tc.includeReleaseLabel, tc.resourceType, tc.namespace) 81 | if diff := cmp.Diff(tc.wantArgs, gotArgs); diff != "" { 82 | t.Errorf("kubectlGetArgs() produced diff (-want, +got):\n%s", diff) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/pipeline-deployer/vertexai.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | 22 | "google.golang.org/api/aiplatform/v1" 23 | "google.golang.org/api/option" 24 | "sigs.k8s.io/yaml" 25 | ) 26 | 27 | // pipelineRequestFromManifest loads the file provided in `path` and returns the parsed CreatePipelineJobRequest 28 | // from the data. 29 | func pipelineRequestFromManifest(path string) (*aiplatform.GoogleCloudAiplatformV1CreatePipelineJobRequest, error) { 30 | data, err := os.ReadFile(path) 31 | if err != nil { 32 | return nil, fmt.Errorf("error reading manifest file: %v", err) 33 | } 34 | 35 | createPipelineRequest := &aiplatform.GoogleCloudAiplatformV1CreatePipelineJobRequest{} 36 | if err = yaml.Unmarshal(data, createPipelineRequest); err != nil { 37 | return nil, fmt.Errorf("unable to parse createPipelineJobRequest from manifest file: %v", err) 38 | } 39 | 40 | return createPipelineRequest, nil 41 | } 42 | 43 | // newAIPlatformService generates a Service that can make API calls in the specified region. 44 | func newAIPlatformService(ctx context.Context, region string) (*aiplatform.Service, error) { 45 | endPointOption := option.WithEndpoint(fmt.Sprintf("%s-aiplatform.googleapis.com", region)) 46 | regionalService, err := aiplatform.NewService(ctx, endPointOption) 47 | if err != nil { 48 | return nil, fmt.Errorf("unable to authenticate") 49 | } 50 | return regionalService, nil 51 | } 52 | 53 | // deployPipeline performs the deployPipeline request and awaits the resulting operation until it completes, it times out or an error occurs. 54 | func deployPipeline(ctx context.Context, aiPlatformService *aiplatform.Service, parent string, request *aiplatform.GoogleCloudAiplatformV1CreatePipelineJobRequest) error { 55 | fmt.Printf("PARENT: %s; REQUEST: %v", parent, request.PipelineJob) 56 | _, err := aiPlatformService.Projects.Locations.PipelineJobs.Create(parent, request.PipelineJob).Do() 57 | if err != nil { 58 | return fmt.Errorf("unable to deploy pipeline: %v", err) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /packages/cdenv/cdenv.go: -------------------------------------------------------------------------------- 1 | // Package cdenv contains Cloud Deploy environment variable keys and utility functions for environment 2 | // variables. 3 | package cdenv 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // Cloud Deploy environment variable keys. 11 | const ( 12 | RequestTypeEnvKey = "CLOUD_DEPLOY_REQUEST_TYPE" 13 | FeaturesEnvKey = "CLOUD_DEPLOY_FEATURES" 14 | // ProjectEnvKey contains the project number of the Cloud Deploy resource. 15 | ProjectEnvKey = "CLOUD_DEPLOY_PROJECT" 16 | // ProjectIDEnvKey contains the project ID of the Cloud Deploy resource. 17 | ProjectIDEnvKey = "CLOUD_DEPLOY_PROJECT_ID" 18 | LocationEnvKey = "CLOUD_DEPLOY_LOCATION" 19 | PipelineEnvKey = "CLOUD_DEPLOY_DELIVERY_PIPELINE" 20 | ReleaseEnvKey = "CLOUD_DEPLOY_RELEASE" 21 | RolloutEnvKey = "CLOUD_DEPLOY_ROLLOUT" 22 | TargetEnvKey = "CLOUD_DEPLOY_TARGET" 23 | PhaseEnvKey = "CLOUD_DEPLOY_PHASE" 24 | PercentageEnvKey = "CLOUD_DEPLOY_PERCENTAGE_DEPLOY" 25 | StorageTypeEnvKey = "CLOUD_DEPLOY_STORAGE_TYPE" 26 | // InputGCSEnvKey contains the GCS URI where the users prerendered artifacts are located. 27 | InputGCSEnvKey = "CLOUD_DEPLOY_INPUT_GCS_PATH" 28 | // OutputGCSEnvKey is provided by Cloud Deploy. It is the GCS URI to use to upload a results 29 | // file. 30 | OutputGCSEnvKey = "CLOUD_DEPLOY_OUTPUT_GCS_PATH" 31 | // SkaffoldGCSEnvKey contains the GCR URI where the Skaffold configuration was uploaded to. 32 | SkaffoldGCSEnvKey = "CLOUD_DEPLOY_SKAFFOLD_GCS_PATH" 33 | // ManifestGCSEnvKey contains the path to the manifest file relative to the rendered output uri. 34 | ManifestGCSEnvKey = "CLOUD_DEPLOY_MANIFEST_GCS_PATH" 35 | WorkloadTypeEnvKey = "CLOUD_DEPLOY_WORKLOAD_TYPE" 36 | CloudBuildServiceAccount = "CLOUD_DEPLOY_WP_CB_ServiceAccount" 37 | CloudBuildWorkerPool = "CLOUD_DEPLOY_WP_CB_WorkerPool" 38 | ) 39 | 40 | // CheckDuplicates expects environment variables in the k=v format. It 41 | // converts the environment string slice to a map and checks for duplicates 42 | // and malformed entries. 43 | func CheckDuplicates(environ []string) (map[string]string, error) { 44 | envMap := make(map[string]string) 45 | 46 | if len(environ) == 0 { 47 | return nil, fmt.Errorf("no environment variables found") 48 | } 49 | 50 | for _, envVar := range environ { 51 | pair := strings.SplitN(envVar, "=", 2) 52 | if len(pair) != 2 { 53 | return nil, fmt.Errorf("incorrect env variable format - expected k=v") 54 | } 55 | 56 | key := pair[0] 57 | value := pair[1] 58 | if key == "" { 59 | return nil, fmt.Errorf("empty environment variable key") 60 | } 61 | 62 | if value == "" { 63 | return nil, fmt.Errorf("empty environment variable value") 64 | } 65 | 66 | if _, exists := envMap[strings.ToLower(key)]; exists { 67 | return nil, fmt.Errorf("duplicate environment variable key: %s", key) 68 | } 69 | envMap[strings.ToLower(key)] = value 70 | } 71 | return envMap, nil 72 | } 73 | -------------------------------------------------------------------------------- /colors-e2e/colors-be/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | func main() { 16 | color := "red" // default color 17 | overrideColor := os.Getenv("OverrideColor") 18 | hostname := os.Getenv("HOSTNAME") 19 | faultPercentString := os.Getenv("FaultPercent") 20 | faultPercent := 0 21 | if faultPercentString != "" { 22 | parsedPercent, err := strconv.ParseInt(faultPercentString, 10, 32) 23 | if err != nil { 24 | log.Fatalf("cannot parse fault perecent %v: %v", faultPercent, err) 25 | } 26 | faultPercent = int(parsedPercent) 27 | } 28 | if overrideColor != "" { 29 | color = overrideColor 30 | } 31 | scvData, err := GetSerivceMetadata() 32 | if err != nil { 33 | log.Fatalf("cannot get service metadata: %v", err) 34 | } 35 | 36 | requestLogger, err := NewRequestLogger(context.Background(), scvData) 37 | if err != nil { 38 | log.Fatalf("cannot setup request logger") 39 | } 40 | 41 | createConstantLoad(context.Background(), "http://colors-be-scv:8080/color", 1) 42 | http.HandleFunc("/color", func(w http.ResponseWriter, r *http.Request) { 43 | var responseStatusGood bool = true 44 | result := struct { 45 | Color string `json:"color"` 46 | Name string `json:"name"` 47 | }{ 48 | Color: color, 49 | Name: hostname, 50 | } 51 | if rand.Intn(101) < faultPercent { 52 | responseStatusGood = false 53 | w.WriteHeader(500) 54 | } else { 55 | err := json.NewEncoder(w).Encode(result) 56 | if err != nil { 57 | log.Printf("error encoding response: %v\n", err) 58 | w.WriteHeader(500) 59 | responseStatusGood = false 60 | } 61 | } 62 | 63 | requestLogger.LogRequest(r.Context(), responseStatusGood) 64 | }) 65 | 66 | // Listen on port 8080. 67 | http.ListenAndServe(":8080", nil) 68 | } 69 | 70 | // createConstantLoad creates constant load against the endpoint forever 71 | func createConstantLoad(ctx context.Context, url string, qps int) { 72 | log.Printf("creating constant load against %v with QPS %v", url, qps) 73 | delay := 1000 / qps 74 | go func() { 75 | for { 76 | select { 77 | case <-ctx.Done(): 78 | return 79 | default: 80 | sendRequest(url) 81 | time.Sleep(time.Duration(delay) * time.Millisecond) 82 | } 83 | } 84 | }() 85 | } 86 | 87 | // sendRequest sends a get request to the endpoint and ignores the response 88 | func sendRequest(endpoint string) { 89 | client := &http.Client{} 90 | 91 | req, err := http.NewRequest("GET", endpoint, nil) 92 | if err != nil { 93 | log.Printf("error creating request: %v", err) 94 | return 95 | } 96 | req.Close = true 97 | response, err := client.Do(req) 98 | if err != nil { 99 | return 100 | } 101 | defer response.Body.Close() 102 | io.Copy(io.Discard, response.Body) 103 | } 104 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/model-deployer/polling.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sync" 21 | "time" 22 | 23 | "google.golang.org/api/aiplatform/v1" 24 | "k8s.io/apimachinery/pkg/util/wait" 25 | ) 26 | 27 | const ( 28 | // wait for 30 seconds for a response regarding an operation. 29 | lroOperationTimeout = 30 * time.Second 30 | // Polling duration, regardless of how long the lease is, we're going to poll for at most 30 mins. 31 | pollingTimeout = 30 * time.Minute 32 | ) 33 | 34 | // poll will return the status of an operation if it finished within "operationTimeout" or an error 35 | // indicating that the operation is incomplete. 36 | func poll(ctx context.Context, service *aiplatform.Service, op *aiplatform.GoogleLongrunningOperation) error { 37 | 38 | opService := aiplatform.NewProjectsLocationsOperationsService(service) 39 | 40 | _, err := opService.Get(op.Name).Do() 41 | if err != nil { 42 | return fmt.Errorf("unable to get operation") 43 | } 44 | 45 | pollFunc := getWaitFunc(opService, op.Name, ctx) 46 | err = wait.PollImmediateWithContext(ctx, lroOperationTimeout, pollingTimeout, pollFunc) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // getWaitFunc is a helper function that returns true if the specified operation has completed. 55 | func getWaitFunc(service *aiplatform.ProjectsLocationsOperationsService, name string, ctx context.Context) wait.ConditionWithContextFunc { 56 | return func(ctx context.Context) (done bool, err error) { 57 | op, err := service.Get(name).Do() 58 | if err != nil { 59 | return false, err 60 | } 61 | 62 | if op.Done { 63 | return true, nil 64 | } 65 | 66 | return false, nil 67 | } 68 | } 69 | 70 | // pollChan is a helper function that facilitates polling multiple long running operations in parallel 71 | func pollChan(ctx context.Context, service *aiplatform.Service, lros ...*aiplatform.GoogleLongrunningOperation) <-chan error { 72 | var wg sync.WaitGroup 73 | out := make(chan error) 74 | wg.Add(len(lros)) 75 | 76 | output := func(lro *aiplatform.GoogleLongrunningOperation) { 77 | out <- poll(ctx, service, lro) 78 | wg.Done() 79 | } 80 | 81 | for _, lro := range lros { 82 | go output(lro) 83 | } 84 | 85 | go func() { 86 | wg.Wait() 87 | close(out) 88 | }() 89 | return out 90 | } 91 | -------------------------------------------------------------------------------- /custom-targets/helm/helm-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | 22 | "cloud.google.com/go/storage" 23 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 24 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/cdenv" 25 | ) 26 | 27 | const ( 28 | // The name of the Helm deployer sample, this is passed back to Cloud Deploy 29 | // as metadata in the render and deploy results. 30 | helmDeployerSampleName = "clouddeploy-helm-sample" 31 | ) 32 | 33 | func main() { 34 | if err := do(); err != nil { 35 | fmt.Fprintf(os.Stderr, "err: %v\n", err) 36 | os.Exit(1) 37 | } 38 | fmt.Println("Done!") 39 | } 40 | 41 | func do() error { 42 | ctx := context.Background() 43 | gcsClient, err := storage.NewClient(ctx) 44 | if err != nil { 45 | return fmt.Errorf("unable to create cloud storage client: %v", err) 46 | } 47 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{}) 48 | if err != nil { 49 | return fmt.Errorf("unable to determine cloud deploy request: %v", err) 50 | } 51 | params, err := determineParams() 52 | if err != nil { 53 | return fmt.Errorf("unable to determine params: %v", err) 54 | } 55 | h, err := createRequestHandler(ctx, req, params, gcsClient) 56 | if err != nil { 57 | return err 58 | } 59 | return h.process(ctx) 60 | } 61 | 62 | // requestHandler interface provides methods for handling the Cloud Deploy request. 63 | type requestHandler interface { 64 | // Process processes the Cloud Deploy request. 65 | process(ctx context.Context) error 66 | } 67 | 68 | // createRequestHandler creates a requestHandler for the provided Cloud Deploy request. 69 | func createRequestHandler(ctx context.Context, cloudDeployRequest any, params *params, gcsClient *storage.Client) (requestHandler, error) { 70 | switch r := cloudDeployRequest.(type) { 71 | case *clouddeploy.RenderRequest: 72 | return &renderer{ 73 | req: r, 74 | params: params, 75 | gcsClient: gcsClient, 76 | }, nil 77 | 78 | case *clouddeploy.DeployRequest: 79 | return &deployer{ 80 | req: r, 81 | params: params, 82 | gcsClient: gcsClient, 83 | }, nil 84 | 85 | default: 86 | return nil, fmt.Errorf("received unsupported cloud deploy request type: %q", os.Getenv(cdenv.RequestTypeEnvKey)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /custom-targets/util/applysetters/walk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Skaffold Authors 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 | */ 16 | 17 | package applysetters 18 | 19 | import ( 20 | "fmt" 21 | 22 | "sigs.k8s.io/kustomize/kyaml/errors" 23 | "sigs.k8s.io/kustomize/kyaml/yaml" 24 | ) 25 | 26 | // visitor is implemented by structs which need to walk the configuration. 27 | // visitor is provided to accept to walk configuration 28 | type visitor interface { 29 | // visitScalar is called for each scalar field value on a resource 30 | // node is the scalar field value 31 | // path is the path to the field; path elements are separated by '.' 32 | visitScalar(node *yaml.RNode, path string) error 33 | 34 | // visitMapping is called for each Mapping field value on a resource 35 | // node is the mapping field value 36 | // path is the path to the field 37 | visitMapping(node *yaml.RNode, path string) error 38 | } 39 | 40 | // accept invokes the appropriate function on v for each field in object 41 | func accept(v visitor, object *yaml.RNode) error { 42 | // get the OpenAPI for the type if it exists 43 | return acceptImpl(v, object, "") 44 | } 45 | 46 | // acceptImpl implements accept using recursion 47 | func acceptImpl(v visitor, object *yaml.RNode, p string) error { 48 | switch object.YNode().Kind { 49 | case yaml.DocumentNode: 50 | // Traverse the child of the document 51 | return accept(v, yaml.NewRNode(object.YNode())) 52 | case yaml.MappingNode: 53 | if err := v.visitMapping(object, p); err != nil { 54 | return err 55 | } 56 | return object.VisitFields(func(node *yaml.MapNode) error { 57 | // Traverse each field value 58 | return acceptImpl(v, node.Value, p+"."+node.Key.YNode().Value) 59 | }) 60 | case yaml.SequenceNode: 61 | return VisitElements(object, func(node *yaml.RNode, i int) error { 62 | // Traverse each list element 63 | return acceptImpl(v, node, p+fmt.Sprintf("[%d]", i)) 64 | }) 65 | case yaml.ScalarNode: 66 | // Visit the scalar field 67 | return v.visitScalar(object, p) 68 | } 69 | return nil 70 | } 71 | 72 | // VisitElements calls fn for each element in a SequenceNode. 73 | // Returns an error for non-SequenceNodes 74 | func VisitElements(rn *yaml.RNode, fn func(node *yaml.RNode, i int) error) error { 75 | elements, err := rn.Elements() 76 | if err != nil { 77 | return errors.Wrap(err) 78 | } 79 | for i := range elements { 80 | if err := fn(elements[i], i); err != nil { 81 | return errors.Wrap(err) 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Deploy Kubernetes Clean Up Sample 2 | 3 | This contains source code for a container that can be used to clean up 4 | Kubernetes resources that were deployed by Cloud Deploy. 5 | 6 | **This is not an officially supported Google product, and it is not covered by a 7 | Google Cloud support contract. To report bugs or request features in a Google 8 | Cloud product, please contact 9 | [Google Cloud support](https://cloud.google.com/support).** 10 | 11 | By default, when deploying to Kubernetes clusters, the deploy phase of Cloud 12 | Deploy uses `kubectl apply` to send rendered manifests to the Kubernetes control 13 | plane. This means the control plane only sees resources that are listed in the 14 | manifest. If you remove or rename resources, the old resources will not get 15 | removed from the cluster. 16 | 17 | This postdeploy hook implements a solution to this by deleting resources that 18 | were deployed by a previous release, but which aren't part of this release's 19 | manifest. 20 | 21 | At a high level, the sample image: 22 | 23 | 1. Gets a list of kubernetes resources that were deployed by Cloud Deploy in 24 | the **current** release, filtered to the pipeline, target, project-id and 25 | location. 26 | 2. Gets a list of all kubernetes resources that were deployed by Cloud Deploy 27 | on the cluster, filtered to the pipeline, target, project-id and location. 28 | This includes resources deployed by **any** release associated with the 29 | current pipeline. 30 | 3. Does a diff and deletes any resources that were not deployed as part of the 31 | current release (i.e. deletes all the old resources). 32 | 33 | ## Configuration 34 | 35 | There are two flags that control what resources are deleted. These can be passed 36 | to the container via the `args` of the Skaffold custom action in the Skaffold 37 | configuration. See 38 | [the `skaffold.yaml` file from the quickstart](quickstart/configuration/skaffold.yaml#L22) 39 | for an example. 40 | 41 | ### `--namespace` 42 | 43 | This flag specifies a comma-separated list of namespaces that will be queried 44 | when looking for resources. By default, it will query all namespaces. 45 | 46 | ### `--resource-type` 47 | 48 | This flag specifies the list of resources to delete, and the order in which they 49 | will be deleted. 50 | 51 | By default, it deletes the following resource types (and in this order): 52 | 53 | * `service` 54 | * `cronjob.batch` 55 | * `job.batch` 56 | * `deployment.apps` 57 | * `statefulset.apps` 58 | * `pod` 59 | * `configmap` 60 | * `secret` 61 | * `horizontalpodautoscaler.autoscaling` 62 | 63 | To delete all resource types, use `--resource-type=all`. 64 | 65 | To make changes to the default (e.g., adding or removing resources), the 66 | simplest thing is to [copy the default list from the source code](main.go#L17) 67 | and add additional resources to it. 68 | 69 | ## Quickstart 70 | 71 | A quickstart that uses this sample is available 72 | [here](./quickstart/QUICKSTART.md). 73 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "os/exec" 23 | "regexp" 24 | ) 25 | 26 | const ( 27 | kubectlBin = "kubectl" 28 | gcloudBin = "gcloud" 29 | ) 30 | 31 | // gkeClusterRegex represents the regex that a GKE cluster resource name needs to match. 32 | var gkeClusterRegex = regexp.MustCompile("^projects/([^/]+)/locations/([^/]+)/clusters/([^/]+)$") 33 | 34 | // gcloudClusterCredentials runs `gcloud container clusters get-crendetials` to set up 35 | // the cluster credentials. 36 | func gcloudClusterCredentials(gkeCluster string) ([]byte, error) { 37 | m := gkeClusterRegex.FindStringSubmatch(gkeCluster) 38 | if len(m) == 0 { 39 | return nil, fmt.Errorf("invalid GKE cluster name: %s", gkeCluster) 40 | } 41 | args := []string{"container", "clusters", "get-credentials", m[3], fmt.Sprintf("--region=%s", m[2]), fmt.Sprintf("--project=%s", m[1])} 42 | return runCmd(gcloudBin, args, "", true) 43 | } 44 | 45 | // verifyResourceExists gets the Kubernetes resource if it exists. 46 | func verifyResourceExists(rt, rn, ns string) ([]byte, error) { 47 | args := []string{"get", rt, rn, fmt.Sprintf("-n=%s", ns)} 48 | return runCmd(kubectlBin, args, "", true) 49 | } 50 | 51 | // queryPath queries the JSON path of a Kubernetes resource. 52 | func queryPath(rt, rn, ns, path string) ([]byte, error) { 53 | args := []string{"get", rt, rn, fmt.Sprintf("-n=%s", ns), fmt.Sprintf("-o=jsonpath=%s", path)} 54 | return runCmd(kubectlBin, args, "", true) 55 | } 56 | 57 | // runCmd starts and waits for the provided command with args to complete. If the command 58 | // succeeds it returns the stdout of the command. 59 | func runCmd(binPath string, args []string, dir string, logCmd bool) ([]byte, error) { 60 | if logCmd { 61 | fmt.Printf("Running the following command: %s %s\n", binPath, args) 62 | } 63 | cmd := exec.Command(binPath, args...) 64 | cmd.Dir = dir 65 | 66 | var stderr bytes.Buffer 67 | errWriter := io.MultiWriter(&stderr, os.Stderr) 68 | cmd.Stderr = errWriter 69 | 70 | var stdout bytes.Buffer 71 | outWriter := io.MultiWriter(&stdout, os.Stdout) 72 | cmd.Stdout = outWriter 73 | 74 | if err := cmd.Start(); err != nil { 75 | return nil, fmt.Errorf("failed to start command: %v", err) 76 | } 77 | if err := cmd.Wait(); err != nil { 78 | return nil, fmt.Errorf("error running command: %v\n%s", err, stderr.Bytes()) 79 | } 80 | return stdout.Bytes(), nil 81 | } 82 | -------------------------------------------------------------------------------- /custom-targets/infrastructure-manager/im-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | 22 | config "cloud.google.com/go/config/apiv1" 23 | "cloud.google.com/go/storage" 24 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 25 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/cdenv" 26 | ) 27 | 28 | const ( 29 | // The name of the Infrastructure Manager deployer sample, this is passed back to 30 | // Cloud Deploy as metadata in the render and deploy results. 31 | imDeployerSampleName = "clouddeploy-infrastructure-manager-sample" 32 | ) 33 | 34 | func main() { 35 | if err := do(); err != nil { 36 | fmt.Printf("err: %v\n", err) 37 | os.Exit(1) 38 | } 39 | fmt.Println("Done!") 40 | } 41 | 42 | func do() error { 43 | ctx := context.Background() 44 | gcsClient, err := storage.NewClient(ctx) 45 | if err != nil { 46 | return fmt.Errorf("unable to create cloud storage client: %v", err) 47 | } 48 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{}) 49 | if err != nil { 50 | return fmt.Errorf("unable to determine cloud deploy request: %v", err) 51 | } 52 | params, err := determineParams() 53 | if err != nil { 54 | return fmt.Errorf("unable to determine params: %v", err) 55 | } 56 | h, err := createRequestHandler(ctx, req, params, gcsClient) 57 | if err != nil { 58 | return err 59 | } 60 | return h.process(ctx) 61 | } 62 | 63 | // requestHandler interface provides methods for handling the Cloud Deploy request. 64 | type requestHandler interface { 65 | // Process processes the Cloud Deploy request. 66 | process(ctx context.Context) error 67 | } 68 | 69 | // createRequestHandler creates a requestHandler for the provided Cloud Deploy request. 70 | func createRequestHandler(ctx context.Context, cloudDeployRequest interface{}, params *params, gcsClient *storage.Client) (requestHandler, error) { 71 | switch r := cloudDeployRequest.(type) { 72 | case *clouddeploy.RenderRequest: 73 | return &renderer{ 74 | req: r, 75 | params: params, 76 | gcsClient: gcsClient, 77 | }, nil 78 | 79 | case *clouddeploy.DeployRequest: 80 | imClient, err := config.NewClient(ctx) 81 | if err != nil { 82 | return nil, fmt.Errorf("unable to create infrastructure manager client: %v", err) 83 | } 84 | return &deployer{ 85 | req: r, 86 | params: params, 87 | gcsClient: gcsClient, 88 | imClient: imClient, 89 | }, nil 90 | 91 | default: 92 | return nil, fmt.Errorf("received unsupported cloud deploy request type: %q", os.Getenv(cdenv.RequestTypeEnvKey)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /custom-targets/helm/helm-deployer/params.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strconv" 21 | ) 22 | 23 | // Environment variable keys whose values determine the behavior of the Terraform deployer. 24 | // Cloud Deploy transforms a deploy parameter "customTarget/helmGKECluster" into an 25 | // environment variable of the form "CLOUD_DEPLOY_customTarget_helmGKECluster". 26 | const ( 27 | gkeClusterEnvkey = "CLOUD_DEPLOY_customTarget_helmGKECluster" 28 | configPathEnvKey = "CLOUD_DEPLOY_customTarget_helmConfigurationPath" 29 | namespaceEnvKey = "CLOUD_DEPLOY_customTarget_helmNamespace" 30 | templateLookupEnvKey = "CLOUD_DEPLOY_customTarget_helmTemplateLookup" 31 | templateValidateEnvKey = "CLOUD_DEPLOY_customTarget_helmTemplateValidate" 32 | upgradeTimeoutEnvKey = "CLOUD_DEPLOY_customTarget_helmUpgradeTimeout" 33 | ) 34 | 35 | // params contains the deploy parameter values passed into the execution environment. 36 | type params struct { 37 | // Name of the GKE cluster. 38 | gkeCluster string 39 | // Path to the helm chart in the Cloud Deploy release archive. If not provided then 40 | // defaults to "mychart" in the root directory of the archive. 41 | configPath string 42 | // Namespace scope of the request. 43 | namespace string 44 | // Whether to handle lookup functions when performing helm template for the informational 45 | // release manifest, requires connecting to the cluster at render time. 46 | templateLookup bool 47 | // Whether to validate the manifest produced by helm template against the cluster, 48 | // requires connecting to the cluster at render time. 49 | templateValidate bool 50 | // Timeout duration when performing helm upgrade. 51 | upgradeTimeout string 52 | } 53 | 54 | // determineParams returns the params provided in the execution environment via environment variables. 55 | func determineParams() (*params, error) { 56 | cluster := os.Getenv(gkeClusterEnvkey) 57 | if len(cluster) == 0 { 58 | return nil, fmt.Errorf("parameter %q is required", gkeClusterEnvkey) 59 | } 60 | 61 | templateLookup := false 62 | tl, ok := os.LookupEnv(templateLookupEnvKey) 63 | if ok { 64 | var err error 65 | templateLookup, err = strconv.ParseBool(tl) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to parse parameter %q: %v", templateLookupEnvKey, err) 68 | } 69 | } 70 | 71 | templateValidate := false 72 | tv, ok := os.LookupEnv(templateValidateEnvKey) 73 | if ok { 74 | var err error 75 | templateLookup, err = strconv.ParseBool(tv) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to parse parameter %q: %v", templateValidateEnvKey, err) 78 | } 79 | } 80 | 81 | return ¶ms{ 82 | gkeCluster: cluster, 83 | configPath: os.Getenv(configPathEnvKey), 84 | namespace: os.Getenv(namespaceEnvKey), 85 | templateLookup: templateLookup, 86 | templateValidate: templateValidate, 87 | upgradeTimeout: os.Getenv(upgradeTimeoutEnvKey), 88 | }, nil 89 | } 90 | -------------------------------------------------------------------------------- /colors-e2e/README.md: -------------------------------------------------------------------------------- 1 | # Colors E2E Demo 2 | This sample demonstrates an end-to-end CI/CD pipeline that leverages a large number of Cloud Deploy features, including: 3 | 4 | - Canary Deployments using pod-based traffic splitting 5 | - Integration with Cloud Monitoring to determine if deployments are successful using a verify job 6 | - Automated promotion 7 | - Setting up annotations based on build data 8 | - Parallel deployments 9 | - Deployment parameters 10 | 11 | ## Application architecture 12 | 13 | Colors is a simple application that contains a webpage that displays a stream of colors based on the configuration of the backend. 14 | 15 | The demo application consists of two services: colors-be and colors-fe. 16 | 17 | ### Colors-be 18 | * Acts as a backend API that returns a configured color (this can be set via an environment variable). 19 | * Logs metrics to Cloud Ops Suite to report the status of every API request that it serves 20 | * To simulate user traffic, generates constant load to the API endpoint 21 | * Reads an environment variable to inject faults in a percentage of its responses 22 | 23 | ### Colors-fe 24 | * Acts as the front end to the colors application and renders a webpage that shows the history of colors returned from calling colors-be on a periodic basis. 25 | * Also displays the value of select environment variables 26 | 27 | ## Setup 28 | 29 | ### Prerequisites 30 | To setup the demo you will need: 31 | * A GCP project 32 | * 4 GKE clusters representing dev, staging and two prod clusters 33 | * An artifact registry repository setup to store the images needed for this demo. GKE needs permission to read from this repository and Cloud Build (if using) or the user running the build needs permission to push images 34 | * Cloud Monitoring APIs enabled 35 | 36 | ### Steps 37 | 1. Update the variables in ‘update_files_with_vars.sh’ and run the script to update the configuration files to match your setup. 38 | 2. Build the verify-metrics-container image at https://github.com/GoogleCloudPlatform/cloud-deploy-samples/tree/main/verify-evaluate-cloud-metrics and upload it to the image repository that you created 39 | 3. Apply the Cloud Deploy configuration by running “gcloud deploy apply -f clouddeploy.yaml” 40 | 4. Deploy the backend by running “gcloud builds submit --config=colors-be/cloudbuild.yaml". This builds the image and creates a Cloud Deploy release with that artifact. 41 | 5. Deploy the front end by running “gcloud builds submit –config=colors-fe/cloudbuild.yaml”. This builds the image and creates a Cloud Deploy release with that artifact. 42 | 43 | ## Things to try 44 | 45 | * Setup port forwarding to set the front end in your various clusters 46 | ``` 47 | gcloud container clusters get-credentials --zone --project 48 | kubectl port-forward service/colors-fd-scv 8080:8080 --context=gke___ 49 | ``` 50 | You can do this multiple times with different local ports in order to view multiple clusters from the same machine at the same time 51 | 52 | * Update the override color in colors-be/k8s.yaml, trigger a deployment (run "gcloud builds submit –config=colors-fe/cloudbuild.yaml") and see how the change propagates through environments, especially where canary is configured. 53 | * Trigger a rollback via the UI to see that as well 54 | 55 | * Change the deploy parameters in the colors-fd pipeline in clouddeploy.yaml. For example, update envName to match what you would call the diffrent environments 56 | 57 | * Update the "faultPercent" deploy parameter in the colors-be pipeline in clouddeploy.yaml. Re-apply the file, trigger a deployment and see how the faults impact the deployment. Also look for the impact in the Cloud Monitoring dashboard 58 | -------------------------------------------------------------------------------- /packages/gcs/gcs.go: -------------------------------------------------------------------------------- 1 | // Package gcs provides functions for interacting with Google Cloud Storage. 2 | package gcs 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "cloud.google.com/go/storage" 15 | ) 16 | 17 | // ResultObjectSuffix is the Cloud Storage object suffix for the expected results file. 18 | const ResultObjectSuffix = "results.json" 19 | 20 | // Download downloads the Cloud Storage object for the specified URI to the provided local path. 21 | func Download(ctx context.Context, gcsClient *storage.Client, gcsURI, localPath string) (*os.File, error) { 22 | gcsObj, err := parseGCSURI(gcsURI) 23 | if err != nil { 24 | return nil, err 25 | } 26 | r, err := gcsClient.Bucket(gcsObj.bucket).Object(gcsObj.name).NewReader(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | defer r.Close() 31 | 32 | if err := os.MkdirAll(filepath.Dir(localPath), os.ModePerm); err != nil { 33 | return nil, err 34 | } 35 | file, err := os.Create(localPath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if _, err := io.Copy(file, r); err != nil { 41 | return nil, err 42 | } 43 | return file, nil 44 | } 45 | 46 | // UploadContent is used as a parameter for the various GCS upload functions that points 47 | // to the source of the content to upload. 48 | type UploadContent struct { 49 | // Content is this byte array. 50 | Data []byte 51 | // Content is in the file at this local path. 52 | LocalPath string 53 | } 54 | 55 | // Upload uploads the provided content to the specified Cloud Storage URI. 56 | func Upload(ctx context.Context, gcsClient *storage.Client, gcsURI string, content *UploadContent) error { 57 | // Determine the source of the content to upload. 58 | var contentData []byte 59 | switch { 60 | case len(content.Data) != 0 && len(content.LocalPath) != 0: 61 | return fmt.Errorf("unable to determine the content to upload to GCS, both data and a local path were provided") 62 | case len(content.Data) != 0: 63 | contentData = content.Data 64 | case len(content.LocalPath) != 0: 65 | var err error 66 | contentData, err = os.ReadFile(content.LocalPath) 67 | if err != nil { 68 | return err 69 | } 70 | default: 71 | return fmt.Errorf("unable to determine the content to upload to GCS") 72 | } 73 | 74 | gcsObjURI, err := parseGCSURI(gcsURI) 75 | if err != nil { 76 | return err 77 | } 78 | w := gcsClient.Bucket(gcsObjURI.bucket).Object(gcsObjURI.name).NewWriter(ctx) 79 | if _, err := w.Write(contentData); err != nil { 80 | return err 81 | } 82 | if err := w.Close(); err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | 88 | // gcsObjectURI is used to split the object Cloud Storage URI into the bucket and name. 89 | type gcsObjectURI struct { 90 | // bucket the GCS object is in. 91 | bucket string 92 | // name of the GCS object. 93 | name string 94 | } 95 | 96 | // parseGCSURI parses the Cloud Storage URI and returns the corresponding gcsObjectURI. 97 | func parseGCSURI(uri string) (gcsObjectURI, error) { 98 | var obj gcsObjectURI 99 | u, err := url.Parse(uri) 100 | if err != nil { 101 | return gcsObjectURI{}, fmt.Errorf("cannot parse URI %q: %w", uri, err) 102 | } 103 | if u.Scheme != "gs" { 104 | return gcsObjectURI{}, fmt.Errorf("URI scheme is %q, must be 'gs'", u.Scheme) 105 | } 106 | if u.Host == "" { 107 | return gcsObjectURI{}, errors.New("bucket name is empty") 108 | } 109 | obj.bucket = u.Host 110 | obj.name = strings.TrimLeft(u.Path, "/") 111 | if obj.name == "" { 112 | return gcsObjectURI{}, errors.New("object name is empty") 113 | } 114 | return obj, nil 115 | } 116 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/model-deployer/addaliases.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "cloud.google.com/go/storage" 22 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 23 | "google.golang.org/api/aiplatform/v1" 24 | cdapi "google.golang.org/api/clouddeploy/v1" 25 | ) 26 | 27 | // aliasAssigner is responsible for applying model aliases during a post-deploy operation. 28 | 29 | type aliasAssigner struct { 30 | gcsClient *storage.Client 31 | request *addAliasesRequest 32 | } 33 | 34 | // process applies model aliases during a post-deploy operation. 35 | func (aa aliasAssigner) process(ctx context.Context) error { 36 | cdService, err := cdapi.NewService(ctx) 37 | if err != nil { 38 | return fmt.Errorf("unable to create cloud deploy API service: %v", err) 39 | } 40 | 41 | releaseName := fmt.Sprintf("projects/%s/locations/%s/deliveryPipelines/%s/releases/%s", aa.request.project, aa.request.location, aa.request.pipeline, aa.request.release) 42 | 43 | release, err := cdService.Projects.Locations.DeliveryPipelines.Releases.Get(releaseName).Do() 44 | if err != nil { 45 | return fmt.Errorf("unable to fetch release to determine location of rendered manifest: %v", err) 46 | } 47 | 48 | ta, ok := release.TargetArtifacts[aa.request.target] 49 | if !ok { 50 | return fmt.Errorf("target artifact does not exist in release") 51 | } 52 | 53 | pa, ok := ta.PhaseArtifacts[aa.request.phase] 54 | if !ok { 55 | return fmt.Errorf("target phase artifact not found in release") 56 | } 57 | 58 | manifestGcsPath := fmt.Sprintf("%s/%s", ta.ArtifactUri, pa.ManifestPath) 59 | localManifest := "manifest.yaml" 60 | fmt.Printf("Downloading deploy input manifest from %q.\n", manifestGcsPath) 61 | 62 | deployRequest := &clouddeploy.DeployRequest{ 63 | ManifestGCSPath: manifestGcsPath, 64 | } 65 | 66 | fmt.Printf("Downloading rendered manifest.\n") 67 | if _, err := deployRequest.DownloadManifest(ctx, aa.gcsClient, localManifest); err != nil { 68 | fmt.Println("Failed to download rendered manifest.") 69 | return fmt.Errorf("failed to download local manifest: %v", err) 70 | } 71 | 72 | deployedModelRequest, err := deployModelFromManifest(localManifest) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | modelName := deployedModelRequest.DeployedModel.Model 78 | 79 | modelRegion, err := regionFromModel(modelName) 80 | if err != nil { 81 | return fmt.Errorf("unable to obtain region where deployed model is located: %v", err) 82 | } 83 | 84 | aiPlatformService, err := newAIPlatformService(ctx, modelRegion) 85 | if err != nil { 86 | return fmt.Errorf("unable to create aiplatform service: %v", err) 87 | } 88 | 89 | mergeVersionAliasRequest := &aiplatform.GoogleCloudAiplatformV1MergeVersionAliasesRequest{VersionAliases: aa.request.aliases} 90 | updatedModel, err := aiPlatformService.Projects.Locations.Models.MergeVersionAliases(modelName, mergeVersionAliasRequest).Do() 91 | if err != nil { 92 | return fmt.Errorf("unable to update model version aliases") 93 | } 94 | 95 | fmt.Printf("Successfully applied new aliases: %s. Current aliases are: %s\n", aa.request.aliases, updatedModel.VersionAliases) 96 | 97 | return nil 98 | 99 | } 100 | -------------------------------------------------------------------------------- /postdeploy-hooks/k8s-cleanup/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a container that can be used to clean up Kubernetes resources that were 2 | // deployed by Cloud Deploy. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "regexp" 11 | 12 | "cloud.google.com/go/storage" 13 | ) 14 | 15 | var ( 16 | namespace = flag.String("namespace", "", "Namespace(s) to filter on when finding resources to delete. "+ 17 | "For multiple namespaces, separate them with a comma. For example --namespace=foo,bar. By default "+ 18 | "resources will be deleted across all namespaces.") 19 | resourceType = flag.String("resource-type", 20 | "service,cronjob.batch,job.batch,deployment.apps,statefulset.apps,pod,configmap,secret,horizontalpodautoscaler.autoscaling", 21 | "Comma separated list of resource type(s) to filter on when finding "+ 22 | "resources to delete. See default list above of resources that will"+ 23 | "be deleted. To have ALL resources deleted pass in \"all\". "+ 24 | "You can also qualify the resource type by an API group if you want"+ 25 | "to specify resources only in a specific API group. For example --resource-type=deployments.apps") 26 | ) 27 | 28 | // gkeClusterRegex represents the regex that a GKE cluster resource name needs to match. 29 | var gkeClusterRegex = regexp.MustCompile("^projects/([^/]+)/locations/([^/]+)/clusters/([^/]+)$") 30 | 31 | const ( 32 | // The name of the postdeploy hook cleanup sample, this is passed back to 33 | // Cloud Deploy as metadata in the deploy results. 34 | cleanupSampleName = "clouddeploy-k8s-cleanup-sample" 35 | postdeployHookMetadataKey = "postdeploy-hook-source" 36 | ) 37 | 38 | func main() { 39 | flag.Parse() 40 | 41 | // Print the value of the command-line flags to aid debugging. 42 | fmt.Printf("Value of resource-type command-line flag: %s\n", *resourceType) 43 | fmt.Printf("Value of namespace command-line flag: %s \n", *namespace) 44 | 45 | if err := do(); err != nil { 46 | fmt.Printf("err: %v\n", err) 47 | os.Exit(1) 48 | } 49 | fmt.Println("Done!") 50 | os.Exit(0) 51 | } 52 | 53 | func do() error { 54 | // Step 1. Run gcloud get-credentials to set up the cluster credentials. 55 | gkeCluster := os.Getenv("GKE_CLUSTER") 56 | if err := gcloudClusterCredentials(gkeCluster); err != nil { 57 | return err 58 | } 59 | 60 | // Step 2. Get a list of resources to delete. 61 | kubectlExec := CreateCommandExecutor("kubectl") 62 | oldResources, err := kubectlExec.resourcesToDelete(*namespace, *resourceType) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // Step 3. Delete the resources. 68 | if err := kubectlExec.deleteResources(oldResources); err != nil { 69 | return err 70 | } 71 | 72 | // Step 4. Upload metadata. 73 | ctx := context.Background() 74 | deployHookResult := &postdeployHookResult{ 75 | Metadata: map[string]string{ 76 | postdeployHookMetadataKey: cleanupSampleName, 77 | }, 78 | } 79 | gcsClient, err := storage.NewClient(ctx) 80 | if err != nil { 81 | return fmt.Errorf("unable to create cloud storage client: %v", err) 82 | } 83 | if err := uploadResult(ctx, gcsClient, deployHookResult); err != nil { 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // gcloudClusterCredentials runs `gcloud container clusters get-crendetials` to set up 91 | // the cluster credentials. 92 | func gcloudClusterCredentials(gkeCluster string) error { 93 | gcloudExec := CreateCommandExecutor("gcloud") 94 | m := gkeClusterRegex.FindStringSubmatch(gkeCluster) 95 | if len(m) == 0 { 96 | return fmt.Errorf("invalid GKE cluster name: %s", gkeCluster) 97 | } 98 | 99 | args := []string{"container", "clusters", "get-credentials", m[3], fmt.Sprintf("--region=%s", m[2]), fmt.Sprintf("--project=%s", m[1])} 100 | _, err := gcloudExec.execCommand(args) 101 | if err != nil { 102 | return fmt.Errorf("unable to set up cluster credentials: %w", err) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /custom-targets/terraform/terraform-deployer/params.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strconv" 21 | ) 22 | 23 | // Environment variable keys whose values determine the behavior of the Terraform deployer. 24 | // Cloud Deploy transforms a deploy parameter "customTarget/tfBackendBucket" into an 25 | // environment variable of the form "CLOUD_DEPLOY_customTarget_tfBackendBucket". 26 | const ( 27 | backendBucketEnvKey = "CLOUD_DEPLOY_customTarget_tfBackendBucket" 28 | backendPrefixEnvKey = "CLOUD_DEPLOY_customTarget_tfBackendPrefix" 29 | configPathEnvKey = "CLOUD_DEPLOY_customTarget_tfConfigurationPath" 30 | variablePathEnvKey = "CLOUD_DEPLOY_customTarget_tfVariablePath" 31 | enableRenderPlanEnvKey = "CLOUD_DEPLOY_customTarget_tfEnableRenderPlan" 32 | lockTimeoutEnvKey = "CLOUD_DEPLOY_customTarget_tfLockTimeout" 33 | applyParallelismEnvKey = "CLOUD_DEPLOY_customTarget_tfApplyParallelism" 34 | ) 35 | 36 | // params contains the deploy parameter values passed into the execution environment. 37 | type params struct { 38 | // Name of the Cloud Storage bucket used to store the Terraform state. 39 | backendBucket string 40 | // Prefix to use for the Cloud Storage objects that represent the Terraform state. 41 | backendPrefix string 42 | // Path to the Terraform configuration in the Cloud Deploy Release archive. If not 43 | // provided then defaults to the root directory of the archive. 44 | configPath string 45 | // Path to a variable file relative to the Terraform configuration directory. 46 | variablePath string 47 | // Whether to generate a Terraform plan at render time for informational purposes, 48 | // i.e. provided in the Cloud Deploy Release inspector. Not used at apply time. 49 | enableRenderPlan bool 50 | // Duration to retry a state lock, when unset Terraform defaults to 0s. 51 | lockTimeout string 52 | // Parallelism to set when performing terraform apply, when unset Terraform 53 | // defaults to 10. 54 | applyParallelism int 55 | } 56 | 57 | // determineParams returns the params provided in the execution environment via environment variables. 58 | func determineParams() (*params, error) { 59 | backendBucket := os.Getenv(backendBucketEnvKey) 60 | if len(backendBucket) == 0 { 61 | return nil, fmt.Errorf("parameter %q is required", backendBucketEnvKey) 62 | } 63 | backendPrefix := os.Getenv(backendPrefixEnvKey) 64 | if len(backendPrefix) == 0 { 65 | return nil, fmt.Errorf("parameter %q is required", backendPrefixEnvKey) 66 | } 67 | 68 | enablePlan := false 69 | ep, ok := os.LookupEnv(enableRenderPlanEnvKey) 70 | if ok { 71 | var err error 72 | enablePlan, err = strconv.ParseBool(ep) 73 | if err != nil { 74 | return nil, fmt.Errorf("failed to parse parameter %q: %v", enableRenderPlanEnvKey, err) 75 | } 76 | } 77 | 78 | var applyParallelism int 79 | ap, ok := os.LookupEnv(applyParallelismEnvKey) 80 | if ok { 81 | var err error 82 | applyParallelism, err = strconv.Atoi(ap) 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to parse parameter %q: %v", applyParallelismEnvKey, err) 85 | } 86 | } 87 | 88 | return ¶ms{ 89 | backendBucket: backendBucket, 90 | backendPrefix: backendPrefix, 91 | configPath: os.Getenv(configPathEnvKey), 92 | variablePath: os.Getenv(variablePathEnvKey), 93 | enableRenderPlan: enablePlan, 94 | lockTimeout: os.Getenv(lockTimeoutEnvKey), 95 | applyParallelism: applyParallelism, 96 | }, nil 97 | } 98 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai/model-deployer/vertexai_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | "google.golang.org/api/aiplatform/v1" 6 | "testing" 7 | ) 8 | 9 | // Tests that deployModelFromManifest fails when given an incorrect path. Does not test correct path or incomplete file! 10 | func TestDeployModelFromManifestFails(t *testing.T) { 11 | _, err := deployModelFromManifest("") 12 | if err == nil { 13 | t.Errorf("Expected: error, Actual: %s", err) 14 | } 15 | 16 | _, err = deployModelFromManifest("testPath") 17 | if err == nil { 18 | t.Errorf("Expected: error, Actual: %s", err) 19 | } 20 | } 21 | 22 | // Tests that regionFromModel fails when give an empty string or an invalid model path 23 | func TestRegionFromModelFail(t *testing.T) { 24 | _, err := regionFromModel("") 25 | if err == nil { 26 | t.Errorf("Expected: error, Actual: %s", err) 27 | } 28 | 29 | _, err = regionFromModel("not a path") 30 | if err == nil { 31 | t.Errorf("Expected: error, Actual: %s", err) 32 | } 33 | 34 | _, err = regionFromModel("projects/scortabarria-internship/locations/test-location/") 35 | if err == nil { 36 | t.Errorf("Expected: error, Actual: %s", err) 37 | } 38 | 39 | _, err = regionFromModel("projects/scortabarria-internship/locations//models/test-model") 40 | if err == nil { 41 | t.Errorf("Expected: error, Actual: %s", err) 42 | } 43 | 44 | _, err = regionFromModel("projects/scortabarria-internship/locations/models/test-model") 45 | if err == nil { 46 | t.Errorf("Expected: error, Actual: %s", err) 47 | } 48 | } 49 | 50 | // Tests that the method regionFromModel works as intended when given a valid model 51 | func TestRegionFromModelPass(t *testing.T) { 52 | loc, err := regionFromModel("projects/scortabarria-internship/locations/test-location/models/test-model") 53 | if d := cmp.Diff(loc, "test-location"); d != "" || err != nil { 54 | t.Errorf("ERROR: %s", err) 55 | } 56 | } 57 | 58 | // Tests that regionFromEndpoint fails when give an empty string or an invalid endpoint path 59 | func TestRegionFromEndpointFail(t *testing.T) { 60 | _, err := regionFromEndpoint("") 61 | if err == nil { 62 | t.Errorf("Expected: error, Actual: %s", err) 63 | } 64 | 65 | _, err = regionFromEndpoint("not a path") 66 | if err == nil { 67 | t.Errorf("Expected: error, Actual: %s", err) 68 | } 69 | 70 | _, err = regionFromEndpoint("projects/scortabarria-internship/locations/test-location/") 71 | if err == nil { 72 | t.Errorf("Expected: error, Actual: %s", err) 73 | } 74 | 75 | _, err = regionFromEndpoint("projects/scortabarria-internship/locations//endpoints/test-endpoint") 76 | if err == nil { 77 | t.Errorf("Expected: error, Actual: %s", err) 78 | } 79 | 80 | _, err = regionFromEndpoint("projects/scortabarria-internship/locations//endpoints/test-endpoint") 81 | if err == nil { 82 | t.Errorf("Expected: error, Actual: %s", err) 83 | } 84 | } 85 | 86 | // Tests that the regionFromEndpoint successfully returns location from endpoint 87 | func TestRegionFromEndpointPass(t *testing.T) { 88 | loc, err := regionFromEndpoint("projects/scortabarria-internship/locations/test-location/endpoints/test-endpoint") 89 | if diff := cmp.Diff(loc, "test-location"); diff != "" || err != nil { 90 | t.Errorf("ERROR: %s", err) 91 | } 92 | 93 | } 94 | 95 | // Tests that minReplicaCoundFromConfig returns 0 when no minReplicaCount is specified in the configuration 96 | // file. If it is specified, it returns that value 97 | func TestMinReplicaCountFromConfig(t *testing.T) { 98 | deployedModel := &aiplatform.GoogleCloudAiplatformV1DeployedModel{} 99 | if num := minReplicaCountFromConfig(deployedModel); num != 0 { 100 | t.Errorf("Error: num was expected to be 0, Actual: %v", num) 101 | } 102 | 103 | deployedModel.DedicatedResources = &aiplatform.GoogleCloudAiplatformV1DedicatedResources{ 104 | MinReplicaCount: 5, 105 | } 106 | if num := minReplicaCountFromConfig(deployedModel); num != 5 { 107 | t.Errorf("Error: num was expected to be 5, Actual %v", num) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/git.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | const ( 22 | gitBin = "git" 23 | remote = "origin" 24 | ) 25 | 26 | // gitRepository holds the repository values for git commands. 27 | type gitRepository struct { 28 | dir string 29 | hostname string 30 | owner string 31 | repoName string 32 | email string 33 | username string 34 | } 35 | 36 | // newGitRepository returns a gitRepository to interact with a repository. 37 | func newGitRepository(hostname, owner, repoName, email, username string) *gitRepository { 38 | return &gitRepository{ 39 | hostname: hostname, 40 | owner: owner, 41 | repoName: repoName, 42 | email: email, 43 | username: username, 44 | } 45 | } 46 | 47 | // cloneRepo clones a Git repository to the local filesystem. 48 | func (g *gitRepository) cloneRepo(secret string) ([]byte, error) { 49 | args := []string{"clone", fmt.Sprintf("https://%s:%s@%s/%s/%s.git", g.owner, secret, g.hostname, g.owner, g.repoName)} 50 | g.dir = g.repoName 51 | return runCmd(gitBin, args, "", false) 52 | } 53 | 54 | // config sets up the git config with a username and email in the Git repository. 55 | func (g *gitRepository) config() error { 56 | uArgs := []string{"config", "user.name", fmt.Sprintf("%q", g.username)} 57 | if _, err := runCmd(gitBin, uArgs, g.dir, true); err != nil { 58 | return err 59 | } 60 | 61 | // We need to set some value for the email otherwise run into errors when writing commits. 62 | email := g.email 63 | if len(email) == 0 { 64 | email = "<>" 65 | } 66 | eArgs := []string{"config", "user.email", email} 67 | if _, err := runCmd(gitBin, eArgs, g.dir, true); err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | 73 | // checkoutBranch checkouts and resets an existing branch or creates a new one. 74 | func (g *gitRepository) checkoutBranch(branch string) ([]byte, error) { 75 | args := []string{"checkout", "-B", branch} 76 | return runCmd(gitBin, args, g.dir, true) 77 | } 78 | 79 | // add adds all the files in the working tree to the index. 80 | func (g *gitRepository) add() ([]byte, error) { 81 | args := []string{"add", "."} 82 | return runCmd(gitBin, args, g.dir, true) 83 | } 84 | 85 | // detectDiff gets the working tree status and uses the porcelain command to simplify scripting. 86 | func (g *gitRepository) detectDiff() ([]byte, error) { 87 | args := []string{"status", "--porcelain"} 88 | return runCmd(gitBin, args, g.dir, true) 89 | } 90 | 91 | // commit commits the changes in the index to the repository with the provided message. 92 | func (g *gitRepository) commit(msg string) ([]byte, error) { 93 | args := []string{"commit", "-a", "-m", msg} 94 | return runCmd(gitBin, args, g.dir, true) 95 | } 96 | 97 | // push pushes the changes a remote branch. 98 | func (g *gitRepository) push(branch string) ([]byte, error) { 99 | args := []string{"push", remote, branch} 100 | return runCmd(gitBin, args, g.dir, true) 101 | } 102 | 103 | // checkIfExists checks if a branch exists on the remote. 104 | func (g *gitRepository) checkIfExists(branch string) ([]byte, error) { 105 | args := []string{"ls-remote", "--heads", remote, fmt.Sprintf("refs/heads/%s", branch)} 106 | return runCmd(gitBin, args, g.dir, true) 107 | } 108 | 109 | // pull pulls changes from a remote branch. 110 | func (g *gitRepository) pull(branch string) ([]byte, error) { 111 | args := []string{"pull", remote, branch} 112 | return runCmd(gitBin, args, g.dir, true) 113 | } 114 | -------------------------------------------------------------------------------- /custom-targets/terraform/terraform-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a Cloud Deploy Custom Target for deploying 16 | // Google Cloud infrastructure resources deployments using Terraform. 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | 24 | "cloud.google.com/go/storage" 25 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 26 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/cdenv" 27 | ) 28 | 29 | const ( 30 | // The name of the Terraform deployer sample, this is passed back to Cloud Deploy 31 | // as metadata in the render and deploy results. 32 | tfDeployerSampleName = "clouddeploy-terraform-sample" 33 | ) 34 | 35 | func main() { 36 | if err := do(); err != nil { 37 | fmt.Printf("err: %v\n", err) 38 | os.Exit(1) 39 | } 40 | fmt.Println("Done!") 41 | } 42 | 43 | func do() error { 44 | ctx := context.Background() 45 | gcsClient, err := storage.NewClient(ctx) 46 | if err != nil { 47 | return fmt.Errorf("unable to create cloud storage client: %v", err) 48 | } 49 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{}) 50 | if err != nil { 51 | return fmt.Errorf("unable to determine cloud deploy request: %v", err) 52 | } 53 | params, err := determineParams() 54 | if err != nil { 55 | return fmt.Errorf("unable to determine params: %v", err) 56 | } 57 | if err := setTerraformEnvVars(); err != nil { 58 | return err 59 | } 60 | h, err := createRequestHandler(ctx, req, params, gcsClient) 61 | if err != nil { 62 | return err 63 | } 64 | return h.process(ctx) 65 | } 66 | 67 | // requestHandler interface provides methods for handling the Cloud Deploy request. 68 | type requestHandler interface { 69 | // Process processes the Cloud Deploy request. 70 | process(ctx context.Context) error 71 | } 72 | 73 | // createRequestHandler creates a requestHandler for the provided Cloud Deploy request. 74 | func createRequestHandler(ctx context.Context, cloudDeployRequest any, params *params, gcsClient *storage.Client) (requestHandler, error) { 75 | switch r := cloudDeployRequest.(type) { 76 | case *clouddeploy.RenderRequest: 77 | return &renderer{ 78 | req: r, 79 | params: params, 80 | gcsClient: gcsClient, 81 | }, nil 82 | 83 | case *clouddeploy.DeployRequest: 84 | return &deployer{ 85 | req: r, 86 | params: params, 87 | gcsClient: gcsClient, 88 | }, nil 89 | 90 | default: 91 | return nil, fmt.Errorf("received unsupported cloud deploy request type: %q", os.Getenv(cdenv.RequestTypeEnvKey)) 92 | } 93 | } 94 | 95 | // setTerraformEnvVars sets environment variables consumed by Terraform that are required to execute 96 | // the terraform commands. 97 | func setTerraformEnvVars() error { 98 | // Setting "TF_IN_AUTOMATION" to any value will adjust terraform cli output to avoid suggesting 99 | // commands to run, which is only helpful when a human is executing the commands. 100 | if err := os.Setenv("TF_IN_AUTOMATION", "clouddeploy"); err != nil { 101 | return fmt.Errorf("unable to set TF_IN_AUTOMATION environment variable: %v", err) 102 | } 103 | // Setting "TF_INPUT" to false will behave as if `-input=false` is specified when running any 104 | // terraform commands. Since these commands are executing in a CD tool we do not want to 105 | // allow input prompts at runtime. 106 | if err := os.Setenv("TF_INPUT", "false"); err != nil { 107 | return fmt.Errorf("unable to set TF_INPUT environment variable: %v", err) 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main implements a Cloud Deploy Custom Target for deploying to a Git repository. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 24 | "cloud.google.com/go/storage" 25 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 26 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/packages/cdenv" 27 | ) 28 | 29 | const ( 30 | // The name of the Git deployer sample, this is passed back to Cloud Deploy 31 | // as metadata in the deploy results. 32 | gitDeployerSampleName = "clouddeploy-git-ops-sample" 33 | ) 34 | 35 | func main() { 36 | if err := do(); err != nil { 37 | fmt.Printf("err: %v\n", err) 38 | os.Exit(1) 39 | } 40 | 41 | } 42 | 43 | func do() error { 44 | ctx := context.Background() 45 | gcsClient, err := storage.NewClient(ctx) 46 | if err != nil { 47 | return fmt.Errorf("unable to create cloud storage client: %v", err) 48 | } 49 | req, err := clouddeploy.DetermineRequest(ctx, gcsClient, []string{}) 50 | if err != nil { 51 | return fmt.Errorf("unable to determine cloud deploy request: %v", err) 52 | } 53 | params, err := determineParams() 54 | if err != nil { 55 | return fmt.Errorf("unable to determine params: %v", err) 56 | } 57 | h, err := createRequestHandler(ctx, req, params, gcsClient) 58 | if err != nil { 59 | return err 60 | } 61 | return h.process(ctx) 62 | } 63 | 64 | // requestHandler interface provides methods for handling the Cloud Deploy request. 65 | type requestHandler interface { 66 | // Process processes the Cloud Deploy request. 67 | process(ctx context.Context) error 68 | } 69 | 70 | // createRequestHandler creates a requestHandler for the provided Cloud Deploy request. 71 | func createRequestHandler(ctx context.Context, cloudDeployRequest any, params *params, gcsClient *storage.Client) (requestHandler, error) { 72 | // The git deployer only supports deploy. If a render request is received then a not supported result will be 73 | // uploaded to Cloud Storage in order to provide Cloud Deploy with context on why the render failed. 74 | switch r := cloudDeployRequest.(type) { 75 | case *clouddeploy.RenderRequest: 76 | fmt.Println("Received render request from Cloud Deploy, which is not supported. Uploading not supported render results") 77 | res := &clouddeploy.RenderResult{ 78 | ResultStatus: clouddeploy.RenderNotSupported, 79 | FailureMessage: fmt.Sprintf("Render is not supported by %s", gitDeployerSampleName), 80 | Metadata: map[string]string{ 81 | clouddeploy.CustomTargetSourceMetadataKey: gitDeployerSampleName, 82 | clouddeploy.CustomTargetSourceSHAMetadataKey: clouddeploy.GitCommit, 83 | }, 84 | } 85 | rURI, err := r.UploadResult(ctx, gcsClient, res) 86 | if err != nil { 87 | return nil, fmt.Errorf("error uploading not supported render results: %v", err) 88 | } 89 | fmt.Printf("Uploaded not supported render results to %s\n", rURI) 90 | return nil, fmt.Errorf("render not supported by %s", gitDeployerSampleName) 91 | 92 | case *clouddeploy.DeployRequest: 93 | smClient, err := secretmanager.NewClient(ctx) 94 | if err != nil { 95 | return nil, fmt.Errorf("unable to create secret manager client: %v", err) 96 | } 97 | 98 | return &deployer{ 99 | req: r, 100 | params: params, 101 | gcsClient: gcsClient, 102 | smClient: smClient, 103 | }, nil 104 | 105 | default: 106 | return nil, fmt.Errorf("received unsupported cloud deploy request type: %q", os.Getenv(cdenv.RequestTypeEnvKey)) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/providers/github.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package providers 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | ) 24 | 25 | // GitHubProvider implements the GitProvider interface for interacting with the Github API. 26 | type GitHubProvider struct { 27 | Repository string 28 | Token string 29 | Owner string 30 | } 31 | 32 | // OpenPullRequest calls the GitHub API for opening a pull request from a source branch to a destination branch. 33 | func (p *GitHubProvider) OpenPullRequest(src, dst, title, body string) (*PullRequest, error) { 34 | payload, err := json.Marshal(map[string]string{ 35 | "title": title, 36 | "head": src, 37 | "base": dst, 38 | "body": body, 39 | }) 40 | if err != nil { 41 | return nil, fmt.Errorf("unable to marshal json for pull request: %v", err) 42 | } 43 | reader := bytes.NewReader(payload) 44 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls", p.Owner, p.Repository), reader) 45 | if err != nil { 46 | return nil, fmt.Errorf("unable to create new request: %v", err) 47 | } 48 | 49 | req.Header.Add("Accept", "application/vnd.github+json") 50 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", p.Token)) 51 | req.Header.Add("X-GitHub-Api-Version", "2022-11-28") 52 | 53 | resp, err := http.DefaultClient.Do(req) 54 | if err != nil { 55 | return nil, fmt.Errorf("unable to make request: %v", err) 56 | } 57 | defer resp.Body.Close() 58 | var pr PullRequest 59 | r, err := io.ReadAll(resp.Body) 60 | if err != nil { 61 | return nil, fmt.Errorf("unable to read response body: %v", err) 62 | } 63 | if resp.StatusCode != http.StatusCreated { 64 | return nil, fmt.Errorf("create pull request body: %q, status got: %v want: %v", r, resp.StatusCode, http.StatusCreated) 65 | } 66 | if err := json.Unmarshal(r, &pr); err != nil { 67 | return nil, fmt.Errorf("unable to unmarshal open pull request response: %v", err) 68 | } 69 | 70 | return &pr, nil 71 | } 72 | 73 | // MergePullRequest calls the GitHub API for merging a pull request. 74 | func (p *GitHubProvider) MergePullRequest(prNo int) (*MergeResponse, error) { 75 | call := func(prNo int) (*MergeResponse, error) { 76 | payload, err := json.Marshal(map[string]string{ 77 | "merge_method": "merge", 78 | }) 79 | if err != nil { 80 | return nil, fmt.Errorf("unable to marshal json for merging pull request: %v", err) 81 | } 82 | reader := bytes.NewReader(payload) 83 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d/merge", p.Owner, p.Repository, prNo), reader) 84 | if err != nil { 85 | return nil, fmt.Errorf("unable to create new request: %v", err) 86 | } 87 | 88 | req.Header.Add("Accept", "application/vnd.github+json") 89 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", p.Token)) 90 | req.Header.Add("X-GitHub-Api-Version", "2022-11-28") 91 | 92 | resp, err := http.DefaultClient.Do(req) 93 | if err != nil { 94 | return nil, fmt.Errorf("unable to make request: %v", err) 95 | } 96 | defer resp.Body.Close() 97 | 98 | var mr MergeResponse 99 | r, err := io.ReadAll(resp.Body) 100 | if err != nil { 101 | return nil, fmt.Errorf("unable to read response body: %v", err) 102 | } 103 | if resp.StatusCode != http.StatusOK { 104 | return nil, fmt.Errorf("merge pull request body: %q, status got: %v want: %v", r, resp.StatusCode, http.StatusOK) 105 | } 106 | if err := json.Unmarshal(r, &mr); err != nil { 107 | return nil, fmt.Errorf("unable to unmarshal merge pull request response: %v", err) 108 | } 109 | 110 | return &mr, nil 111 | } 112 | 113 | return mergePullRequestWithRetries(prNo, call) 114 | } 115 | -------------------------------------------------------------------------------- /custom-targets/git-ops/git-deployer/providers/gitlab.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package providers 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | ) 24 | 25 | // GitLabProvider implements the GitProvider interface for interacting with the Gitlab API. 26 | type GitLabProvider struct { 27 | Repository string 28 | Token string 29 | Owner string 30 | } 31 | 32 | // gitLabMergeRequest represents the response when querying for a GitLab Merge request. 33 | type gitLabMergeRequest struct { 34 | InternalID int `json:"iid"` 35 | } 36 | 37 | // gitLabMergeResponse represents the response from a GitLab when merging a pull request. 38 | type gitLabMergeResponse struct { 39 | Sha string `json:"merge_commit_sha"` 40 | } 41 | 42 | // OpenPullRequest calls the GitLab API for opening a merge request from a source branch to a destination branch. 43 | func (p *GitLabProvider) OpenPullRequest(src, dst, title, body string) (*PullRequest, error) { 44 | payload, err := json.Marshal(map[string]string{ 45 | "title": title, 46 | "source_branch": src, 47 | "target_branch": dst, 48 | "description": body, 49 | }) 50 | if err != nil { 51 | return nil, fmt.Errorf("unable to marshal json for merge request: %v", err) 52 | } 53 | reader := bytes.NewReader(payload) 54 | req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://gitlab.com/api/v4/projects/%s%%2F%s/merge_requests", p.Owner, p.Repository), reader) 55 | if err != nil { 56 | return nil, fmt.Errorf("unable to create new request: %v", err) 57 | } 58 | 59 | req.Header.Add("Content-Type", "application/json") 60 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", p.Token)) 61 | 62 | resp, err := http.DefaultClient.Do(req) 63 | if err != nil { 64 | return nil, fmt.Errorf("unable to make request: %v", err) 65 | } 66 | defer resp.Body.Close() 67 | 68 | var mr gitLabMergeRequest 69 | r, err := io.ReadAll(resp.Body) 70 | if err != nil { 71 | return nil, fmt.Errorf("unable to read response body: %v", err) 72 | } 73 | if resp.StatusCode != http.StatusCreated { 74 | return nil, fmt.Errorf("create pull request body: %q, status got: %v want: %v", r, resp.StatusCode, http.StatusCreated) 75 | } 76 | if err := json.Unmarshal(r, &mr); err != nil { 77 | return nil, fmt.Errorf("unable to unmarshal open pull request response: %v", err) 78 | } 79 | 80 | return &PullRequest{Number: mr.InternalID}, nil 81 | } 82 | 83 | // MergePullRequest calls the Gitlab API for merging a merge request. 84 | func (p *GitLabProvider) MergePullRequest(prNo int) (*MergeResponse, error) { 85 | call := func(prNo int) (*MergeResponse, error) { 86 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("https://gitlab.com/api/v4/projects/%s%%2F%s/merge_requests/%d/merge", p.Owner, p.Repository, prNo), nil) 87 | if err != nil { 88 | return nil, fmt.Errorf("unable to create new request: %v", err) 89 | } 90 | 91 | req.Header.Add("Content-Type", "application/json") 92 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", p.Token)) 93 | 94 | resp, err := http.DefaultClient.Do(req) 95 | if err != nil { 96 | return nil, fmt.Errorf("unable to make request: %v", err) 97 | } 98 | defer resp.Body.Close() 99 | var mr gitLabMergeResponse 100 | r, err := io.ReadAll(resp.Body) 101 | if err != nil { 102 | return nil, fmt.Errorf("unable to read response body: %v", err) 103 | } 104 | if resp.StatusCode != http.StatusOK { 105 | return nil, fmt.Errorf("merge pull request body: %q, status got: %v want: %v", r, resp.StatusCode, http.StatusOK) 106 | } 107 | if err := json.Unmarshal(r, &mr); err != nil { 108 | return nil, fmt.Errorf("unable to unmarshal merge pull request response: %v", err) 109 | } 110 | return &MergeResponse{Sha: mr.Sha}, nil 111 | } 112 | 113 | return mergePullRequestWithRetries(prNo, call) 114 | } 115 | -------------------------------------------------------------------------------- /colors-e2e/clouddeploy.yaml.template: -------------------------------------------------------------------------------- 1 | ############# 2 | ### Pipelines 3 | ############# 4 | 5 | apiVersion: deploy.cloud.google.com/v1 6 | kind: DeliveryPipeline 7 | metadata: 8 | name: colors-fd 9 | annotations: 10 | app-src-url: https://github.com/GoogleCloudPlatform/cloud-deploy-samples/tree/main/colors-e2e 11 | serialPipeline: 12 | stages: 13 | - targetId: dev 14 | deployParameters: 15 | - values: 16 | envName: "dev env" 17 | - targetId: staging 18 | deployParameters: 19 | - values: 20 | envName: "staging env" 21 | - targetId: all-prod 22 | deployParameters: 23 | - values: 24 | envName: "prod env one" 25 | replicaCount: "20" 26 | matchTargetLabels: 27 | prod_type: one 28 | - values: 29 | envName: "prod env two" 30 | replicaCount: "5" 31 | matchTargetLabels: 32 | prod_type: two 33 | --- 34 | 35 | apiVersion: deploy.cloud.google.com/v1 36 | kind: DeliveryPipeline 37 | metadata: 38 | name: colors-be 39 | annotations: 40 | app-src-url: https://github.com/GoogleCloudPlatform/cloud-deploy-samples/tree/main/colors-e2e 41 | serialPipeline: 42 | stages: 43 | - targetId: dev 44 | strategy: 45 | standard: 46 | verify: true 47 | deployParameters: 48 | - values: 49 | faultPercent: "0" 50 | - targetId: staging 51 | strategy: 52 | standard: 53 | verify: true 54 | - targetId: all-prod 55 | deployParameters: 56 | - values: 57 | faultPercent: "30" 58 | strategy: 59 | canary: 60 | runtimeConfig: 61 | kubernetes: 62 | serviceNetworking: 63 | service: "colors-be-scv" 64 | deployment: "colors-be" 65 | customCanaryDeployment: 66 | phaseConfigs: 67 | - phaseId: "canary10" 68 | percentage: 10 69 | profiles: [ "CANARY" ] 70 | verify: true 71 | - phaseId: "canary30" 72 | percentage: 30 73 | profiles: [ "CANARY" ] 74 | verify: true 75 | - phaseId: "stable" 76 | percentage: 100 77 | verify: true 78 | --- 79 | 80 | ############# 81 | ### Targets 82 | ############# 83 | apiVersion: deploy.cloud.google.com/v1 84 | kind: Target 85 | metadata: 86 | name: dev 87 | gke: 88 | cluster: $DEV_CLUSTER 89 | deployParameters: 90 | metricClusterName: "dev" 91 | 92 | --- 93 | apiVersion: deploy.cloud.google.com/v1 94 | kind: Target 95 | metadata: 96 | name: staging 97 | gke: 98 | cluster: $STAGING_CLUSTER 99 | deployParameters: 100 | metricClusterName: "staging" 101 | 102 | --- 103 | apiVersion: deploy.cloud.google.com/v1 104 | kind: Target 105 | metadata: 106 | name: prod1 107 | labels: 108 | prodType: one 109 | gke: 110 | cluster: $PROD1_CLUSTER 111 | deployParameters: 112 | metricClusterName: "prod1" 113 | 114 | --- 115 | apiVersion: deploy.cloud.google.com/v1 116 | kind: Target 117 | metadata: 118 | name: prod2 119 | labels: 120 | prodType: two 121 | gke: 122 | cluster: $PROD2_CLUSTER 123 | deployParameters: 124 | metricClusterName: "prod2" 125 | 126 | --- 127 | apiVersion: deploy.cloud.google.com/v1 128 | kind: Target 129 | metadata: 130 | name: all-prod 131 | description: contains all production targets 132 | multiTarget: 133 | targetIds: [ prod1, prod2 ] 134 | requireApproval: true 135 | 136 | --- 137 | 138 | ############### 139 | ### Automations 140 | ############### 141 | 142 | apiVersion: deploy.cloud.google.com/v1 143 | kind: Automation 144 | serviceAccount: $COMPUTE_SERVICE_ACCOUNT 145 | description: Auto promote front door rollouts though pipeline 146 | metadata: 147 | name: colors-fd/dev-promote 148 | selector: 149 | - target: 150 | id: dev 151 | - target: 152 | id: staging 153 | rules: 154 | - promoteRelease: 155 | name: promote-release 156 | toTargetId: "@next" 157 | wait: 0m 158 | 159 | --- 160 | apiVersion: deploy.cloud.google.com/v1 161 | kind: Automation 162 | serviceAccount: $COMPUTE_SERVICE_ACCOUNT 163 | description: Auto promote backend rollouts though pipeline 164 | metadata: 165 | name: colors-be/dev-promote 166 | selector: 167 | - target: 168 | id: dev 169 | - target: 170 | id: staging 171 | rules: 172 | - promoteRelease: 173 | name: promote-release 174 | toTargetId: "@next" 175 | wait: 0m 176 | 177 | --- 178 | apiVersion: deploy.cloud.google.com/v1 179 | kind: Automation 180 | serviceAccount: $COMPUTE_SERVICE_ACCOUNT 181 | description: Once a rollout has gone to 30%, move it to 100% automatically after 1 min 182 | metadata: 183 | name: colors-be/advance-full 184 | selector: 185 | - target: 186 | id: all-prod 187 | rules: 188 | - advanceRollout: 189 | name: "advance-rollout" 190 | fromPhases: ["canary10", "canary30"] 191 | wait: 0m 192 | -------------------------------------------------------------------------------- /custom-targets/vertex-ai-pipeline/pipeline-deployer/render_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "cloud.google.com/go/storage" 9 | "github.com/GoogleCloudPlatform/cloud-deploy-samples/custom-targets/util/clouddeploy" 10 | ) 11 | 12 | // Tests that render works as expected. Does not test valid renderer. 13 | func TestRender(t *testing.T) { 14 | gcsClient, _ := storage.NewClient(context.Background()) 15 | newRenderer := &renderer{ 16 | params: ¶ms{}, 17 | gcsClient: gcsClient, 18 | req: &clouddeploy.RenderRequest{}, 19 | } 20 | _, err := newRenderer.render(context.Background()) 21 | if in := strings.Contains(err.Error(), "unable to download and unarchive render input"); !in { 22 | t.Errorf("Expected: unable to download and unarchive render input, Received: %s", err) 23 | } 24 | } 25 | 26 | // Tests that renderDeployModelRequest() handles error from empty renderer. Does not test valid renderer! 27 | func TestRenderCreatePipelineRequest(t *testing.T) { 28 | newRenderer := &renderer{ 29 | params: ¶ms{}, 30 | } 31 | _, err := newRenderer.renderCreatePipelineRequest() 32 | if in := strings.Contains(err.Error(), "cannot apply deploy parameters to configuration file"); !in { 33 | t.Errorf("Expected: cannot apply deploy parameters to configuration file, Received: %s", err) 34 | } 35 | 36 | newRenderer.params.configPath = "configuration/test.yaml" 37 | _, err = newRenderer.renderCreatePipelineRequest() 38 | if in := strings.Contains(err.Error(), "cannot apply deploy parameters to configuration file"); !in { 39 | t.Errorf("Expected: cannot apply deploy parameters to configuration file, Received: %s", err) 40 | } 41 | 42 | } 43 | 44 | // Tests that addCommonMetadata populates the RenderResult as expected 45 | func TestRendAddCommonMetadata(t *testing.T) { 46 | newRenderer := &renderer{} 47 | rendResult := &clouddeploy.RenderResult{} 48 | if myMap := rendResult.Metadata; myMap != nil { 49 | t.Errorf("Expected empty field, received: %s", myMap) 50 | } 51 | newRenderer.addCommonMetadata(rendResult) 52 | if _, exists := rendResult.Metadata[clouddeploy.CustomTargetSourceMetadataKey]; !exists { 53 | t.Errorf("Error: map missing %s key", clouddeploy.CustomTargetSourceMetadataKey) 54 | } 55 | if _, exists := rendResult.Metadata[clouddeploy.CustomTargetSourceSHAMetadataKey]; !exists { 56 | t.Errorf("Error: map missing %s key", clouddeploy.CustomTargetSourceSHAMetadataKey) 57 | } 58 | } 59 | 60 | // Tests that applyDeployParams fails when given an invalid path. Does not test valid path! 61 | func TestApplyDeployParamsFails(t *testing.T) { 62 | err := applyDeployParams("") 63 | if err == nil { 64 | t.Errorf("Expected: error, Actual: %s", err) 65 | } 66 | 67 | err = applyDeployParams("not a path") 68 | if err == nil { 69 | t.Errorf("Expected: error, Actual: %s", err) 70 | } 71 | } 72 | 73 | // Tests that determineConfigLocation fails when an invalid path is passed in but passes when no path is 74 | // given. This is due to the fact that the path is optional. 75 | func TestDetermineConfigLocation(t *testing.T) { 76 | path, shouldErr := determineConfigFileLocation("") 77 | if shouldErr != false { 78 | t.Errorf("Expected shouldErr to be false, Actual: %t", shouldErr) 79 | } 80 | if path != "/workspace/source/pipelineJob.yaml" { 81 | t.Errorf("Expected path to be /workspace/source/pipelineJob.yaml, received: %s", path) 82 | } 83 | 84 | path, shouldErr = determineConfigFileLocation(" ") 85 | if shouldErr != true { 86 | t.Errorf("Expected shouldErr to be true, received: %t", shouldErr) 87 | } 88 | if path != "/workspace/source/ " { 89 | t.Errorf("Expected path to be /workspace/source/ , received: %s", path) 90 | } 91 | 92 | path, shouldErr = determineConfigFileLocation("testPath") 93 | if shouldErr != true { 94 | t.Errorf("Expected shouldErr to be true, received: %t", shouldErr) 95 | } 96 | if path != "/workspace/source/testPath" { 97 | t.Errorf("Expected path to be /workspace/source/testPath, received: %s", path) 98 | } 99 | } 100 | 101 | // Tests that loadConfigurationFile acts as expected when a path or an empty string is passed in. Does not test valid path! 102 | func TestLoadConfigurationFile(t *testing.T) { 103 | content, err := loadConfigurationFile("") 104 | if err != nil || content != nil { 105 | t.Errorf("Expected: nil and nil, received: %s and %s", err, content) 106 | } 107 | 108 | content, err = loadConfigurationFile(" ") 109 | if content != nil || err == nil { 110 | t.Errorf("Expected: nil and error, received: %s and %s", content, err) 111 | } 112 | 113 | content, err = loadConfigurationFile("not a path") 114 | if content != nil || err == nil { 115 | t.Errorf("Expected: nil and error, received: %s and %s", content, err) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /custom-targets/helm/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Deploy Helm Deployer Sample 2 | This directory contains a sample implementation of a Cloud Deploy Custom Target for deploying to a Google Kubernetes Engine (GKE) cluster with Helm. 3 | 4 | **This is not an officially supported Google product, and it is not covered by a 5 | Google Cloud support contract. To report bugs or request features in a Google 6 | Cloud product, please contact [Google Cloud 7 | support](https://cloud.google.com/support).** 8 | 9 | # Quickstart 10 | A quickstart that uses this sample is available [here](./quickstart/QUICKSTART.md) 11 | 12 | # Configuration 13 | The configuration provided when creating a Cloud Deploy Release must contain a [Helm chart](https://helm.sh/docs/topics/charts/). 14 | 15 | # Deploy Parameters 16 | 17 | | Parameter | Required | Description | 18 | | --- | --- | --- | 19 | | customTarget/helmGKECluster| Yes | Name of the GKE cluster the Helm chart is deployed to, e.g. `projects/{project}/locations/{location}/clusters/{cluster}` | 20 | | customTarget/helmConfigurationPath | No | Path to the Helm chart in the Cloud Deploy release archive. If not provided then defaults to `mychart` in the root directory of the archive | 21 | | customTarget/helmNamespace| No | The namespace for the helm requests. Uses default namespace when not provided | 22 | | customTarget/helmTemplateLookup | No | Whether to handle lookup functions when performing `helm template` for the informational release manifest, requires connecting to the cluster at render time | 23 | | customTarget/helmTemplateValidate | No | Whether to validate the manifest produced by `helm template` against the cluster, requires connecting to the cluster at render time | 24 | | customTarget/helmUpgradeTimeout | No | Timeout duration when performing `helm upgrade`, if unset relies on Helm default | 25 | 26 | 27 | # Build the sample image and register a Custom Target Type for Helm 28 | The `build_and_register.sh` script within this `helm` directory can be used to build the Helm deployer image and register a Cloud Deploy custom target type that references the image. To use the script run the following command: 29 | 30 | ```shell 31 | ./build_and_register.sh -p $PROJECT_ID -r $REGION 32 | ``` 33 | 34 | The script does the following on your behalf: 35 | 1. Create an Artifact Registry Repository 36 | 2. Give the default compute service account access to the Repository 37 | 3. Build the image and push it to the Repository 38 | 4. Create a Cloud Storage bucket and within the bucket a skaffold configuration that references the image built 39 | 5. Apply a custom target type for Helm to Cloud Deploy that references the skaffold configuration in Cloud Storage 40 | 41 | # How the sample image works 42 | The Helm deployer sample image is built to handle both a render and deploy request from Cloud Deploy. 43 | 44 | ## Render 45 | The render process consists of the following steps: 46 | 47 | 1. Download the configuration provided at Release creation time and find the Helm chart based on the `customTarget/helmConfigurationPath` deploy parameter. 48 | 49 | 2. If either the `customTarget/helmTemplateLookup` or `customTarget/helmTemplateValidate` deploy parameter is set to `true` then get the cluster credentials. 50 | 51 | 3. Run `helm template` for the provided Helm chart using the Cloud Deploy Delivery Pipeline ID as the Helm Release name. 52 | 53 | a. If `customTarget/helmTemplateLookup` is `true` then `--dry-run=server` arg is used. 54 | 55 | b. If `customTarget/helmTemplateValidate` is `true` then `--validate` arg is used. 56 | 57 | c. If `customTarget/helmNamespace` is defined then `--namespace=${customTarget/helmNamespace}` arg is used. 58 | 59 | 4. Upload to Cloud Storage the manifest produced by `helm template` to be used as the [Cloud Deploy Release inspector](https://cloud.google.com/deploy/docs/view-release#view_release_artifacts) artifact. 60 | 61 | 5. Upload the configuration to Cloud Storage so the Helm chart is available at deploy time. 62 | 63 | ## Deploy 64 | The deploy process consists of the following steps: 65 | 66 | 1. Download the configuration that was uploaded to Cloud Storage during the render process. 67 | 68 | 2. Get the cluster credentials. 69 | 70 | 3. Run `helm upgrade` for the provided Helm chart using the Cloud Deploy Delivery Pipeline ID as the Helm Release name. 71 | 72 | a. If `customTarget/helmUpgradeTimeout` is set, e.g. `10m`, then `--timeout=10m` arg is used. 73 | 74 | b. If `customTarget/helmNamespace` is defined then `--namespace=${customTarget/helmNamespace}` arg is used. The namespace you specify is created if it does not exist. 75 | 76 | 4. Run `helm get manifest` to get the manifest applied by the Helm Release and upload it to Cloud Storage as a Cloud Deploy deploy artifact. 77 | 78 | a. If `customTarget/helmNamespace` is defined then `--namespace=${customTarget/helmNamespace}` arg is used. 79 | -------------------------------------------------------------------------------- /custom-targets/helm/helm-deployer/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "os/exec" 23 | "regexp" 24 | ) 25 | 26 | const ( 27 | helmBin = "helm" 28 | gcloudBin = "gcloud" 29 | ) 30 | 31 | // helmOptions configures the args provided to `helm`. 32 | type helmOptions struct { 33 | namespace string 34 | } 35 | 36 | // helmTemplateOptions configures the args provided to `helm template`. 37 | type helmTemplateOptions struct { 38 | helmOptions 39 | lookup bool 40 | validate bool 41 | } 42 | 43 | // helmTemplate runs `helm template` for the provided release name and chart path with the 44 | // provided options. The output from this command is not written to stdout. Returns the 45 | // manifest in YAML format. 46 | func helmTemplate(releaseName, chartPath string, opts *helmTemplateOptions) ([]byte, error) { 47 | args := []string{"template", releaseName, chartPath, "--include-crds"} 48 | if opts.lookup { 49 | args = append(args, "--dry-run=server") 50 | } 51 | if opts.validate { 52 | args = append(args, "--validate") 53 | } 54 | if len(opts.helmOptions.namespace) > 0 { 55 | args = append(args, fmt.Sprintf("--namespace=%s", opts.helmOptions.namespace)) 56 | } 57 | return runCmd(helmBin, args, true) 58 | } 59 | 60 | // helmUpgradeOptions configures the args provided to `helm upgrade`. 61 | type helmUpgradeOptions struct { 62 | helmOptions 63 | timeout string 64 | } 65 | 66 | // helmUpgrade runs `helm upgrade` for the provided release and chart path with the 67 | // provided options. 68 | func helmUpgrade(releaseName, chartPath string, opts *helmUpgradeOptions) ([]byte, error) { 69 | args := []string{"upgrade", releaseName, chartPath, "--install", "--wait", "--wait-for-jobs"} 70 | if len(opts.timeout) != 0 { 71 | args = append(args, fmt.Sprintf("--timeout=%s", opts.timeout)) 72 | } 73 | if len(opts.helmOptions.namespace) > 0 { 74 | args = append(args, fmt.Sprintf("--namespace=%s", opts.helmOptions.namespace)) 75 | args = append(args, "--create-namespace") 76 | } 77 | return runCmd(helmBin, args, false) 78 | } 79 | 80 | // helmGetManifest runs `helm get manifest` for the provided release name. The output 81 | // from this command is not written to stdout. 82 | func helmGetManifest(releaseName string, opts *helmOptions) ([]byte, error) { 83 | args := []string{"get", "manifest", releaseName} 84 | if len(opts.namespace) > 0 { 85 | args = append(args, fmt.Sprintf("--namespace=%s", opts.namespace)) 86 | } 87 | return runCmd(helmBin, args, true) 88 | } 89 | 90 | // gkeClusterRegex represents the regex that a GKE cluster resource name needs to match. 91 | var gkeClusterRegex = regexp.MustCompile("^projects/([^/]+)/locations/([^/]+)/clusters/([^/]+)$") 92 | 93 | // gcloudClusterCredentials runs `gcloud container clusters get-credentials` to set up 94 | // the cluster credentials. 95 | func gcloudClusterCredentials(gkeCluster string) ([]byte, error) { 96 | m := gkeClusterRegex.FindStringSubmatch(gkeCluster) 97 | if len(m) == 0 { 98 | return nil, fmt.Errorf("invalid GKE cluster name: %s", gkeCluster) 99 | } 100 | args := []string{"container", "clusters", "get-credentials", m[3], fmt.Sprintf("--region=%s", m[2]), fmt.Sprintf("--project=%s", m[1])} 101 | return runCmd(gcloudBin, args, false) 102 | } 103 | 104 | // runCmd starts and waits for the provided command with args to complete. If the command 105 | // succeeds it returns the stdout of the command. 106 | func runCmd(binPath string, args []string, closeOSStdout bool) ([]byte, error) { 107 | fmt.Printf("Running the following command: %s %s\n", binPath, args) 108 | cmd := exec.Command(binPath, args...) 109 | 110 | var stderr bytes.Buffer 111 | errWriter := io.MultiWriter(&stderr, os.Stderr) 112 | cmd.Stderr = errWriter 113 | 114 | var stdout bytes.Buffer 115 | if closeOSStdout { 116 | cmd.Stdout = &stdout 117 | } else { 118 | cmd.Stdout = io.MultiWriter(&stdout, os.Stdout) 119 | } 120 | 121 | if err := cmd.Start(); err != nil { 122 | return nil, fmt.Errorf("failed to start command: %v", err) 123 | } 124 | if err := cmd.Wait(); err != nil { 125 | return nil, fmt.Errorf("error running command: %v\n%s", err, stderr.Bytes()) 126 | } 127 | return stdout.Bytes(), nil 128 | } 129 | -------------------------------------------------------------------------------- /custom-targets/util/build_and_register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2023 Google LLC 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 | # https:#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 | 16 | set -e 17 | 18 | if [[ ! -v _CT_IMAGE_NAME || ! -v _CT_TYPE_NAME || ! -v _CT_CUSTOM_ACTION_NAME || ! -v _CT_GCS_DIRECTORY || ! -v _CT_SKAFFOLD_CONFIG_NAME ]]; then 19 | echo "This script is not meant to be used on its own. Please launch it from one of the custom target directories." 20 | exit 1 21 | fi 22 | 23 | usage() { 24 | echo "usage: build_and_register.sh -p -r " 25 | } 26 | 27 | boldout() { 28 | echo $(tput bold)$(tput setaf 2)">> $@"$(tput sgr0) 29 | } 30 | 31 | while getopts "p:r:" arg; do 32 | case "${arg}" in 33 | p) 34 | PROJECT="${OPTARG}" 35 | ;; 36 | r) 37 | REGION="${OPTARG}" 38 | ;; 39 | *) 40 | usage 41 | exit 1 42 | ;; 43 | esac 44 | done 45 | 46 | if [[ ! -v PROJECT || ! -v REGION ]]; then 47 | usage 48 | exit 1 49 | fi 50 | 51 | AR_REPO=$REGION-docker.pkg.dev/$PROJECT/cd-custom-targets 52 | if ! gcloud -q artifacts repositories describe --location "$REGION" --project "$PROJECT" cd-custom-targets > /dev/null 2>&1; then 53 | boldout "Creating Artifact Registry repository: ${AR_REPO}" 54 | gcloud -q artifacts repositories create --location "$REGION" --project "$PROJECT" --repository-format docker cd-custom-targets 55 | fi 56 | 57 | boldout "Granting the default compute service account access to ${AR_REPO}" 58 | gcloud -q artifacts repositories add-iam-policy-binding \ 59 | --project "${PROJECT}" --location "${REGION}" cd-custom-targets \ 60 | --member=serviceAccount:$(gcloud -q projects describe $PROJECT --format="value(projectNumber)")-compute@developer.gserviceaccount.com \ 61 | --role="roles/artifactregistry.reader" > /dev/null 62 | 63 | BUCKET_NAME="${PROJECT}-${REGION}-custom-targets" 64 | if ! gsutil ls "gs://${BUCKET_NAME}" > /dev/null 2>&1; then 65 | boldout "Creating a storage bucket to hold the custom target configuration" 66 | gcloud -q storage buckets create --project "${PROJECT}" --location "${REGION}" "gs://${BUCKET_NAME}" 67 | fi 68 | 69 | boldout "Building the Custom Target image in Cloud Build." 70 | boldout "This will take approximately 10 minutes" 71 | 72 | # get the commit hash to pass to the build 73 | COMMIT_SHA=$(git rev-parse --verify HEAD) 74 | CLOUDBUILD_YAML="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )/cloudbuild.yaml" 75 | # Get the name of the directory where this script is located. 76 | SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 77 | PARENT_DIR="$(cd "$SOURCE_DIR/../../" && pwd)" 78 | # Using `beta` because the non-beta command won't stream the build logs 79 | gcloud -q beta builds submit --project="$PROJECT" --region="$REGION" \ 80 | --substitutions=_AR_REPO_NAME=cd-custom-targets,_IMAGE_NAME=${_CT_IMAGE_NAME},COMMIT_SHA="${COMMIT_SHA}",_DOCKERFILE_PATH="${_CT_DOCKERFILE_LOCATION}" \ 81 | --config="${CLOUDBUILD_YAML}" \ 82 | "${PARENT_DIR}" 83 | 84 | IMAGE_SHA=$(gcloud -q artifacts docker images describe "${AR_REPO}/${_CT_IMAGE_NAME}:latest" --project "${PROJECT}" --format 'get(image_summary.digest)') 85 | 86 | TMPDIR=$(mktemp -d) 87 | trap 'rm -rf -- "${TMPDIR}"' EXIT 88 | 89 | boldout "Uploading the custom target definition to gs://${BUCKET_NAME}" 90 | cat >"${TMPDIR}/skaffold.yaml" <"${TMPDIR}/clouddeploy.yaml" <> "${TMPDIR}/clouddeploy.yaml" 113 | fi 114 | cat >>"${TMPDIR}/clouddeploy.yaml" < 17 | 18 | 19 | 20 | My Color Application 21 | 22 | 23 | 24 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{range .ConfigValues}} 41 | 42 | 43 | 44 | 45 | {{end}} 46 | 47 |
NameValue
{{.Name}}{{.Value}}
48 |
49 | 50 | 51 |
52 |

API Request Value Stream

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
TimeNameColor
64 |
65 |
66 | 67 | 68 | 86 | 87 | 88 | `)) 89 | hostname := os.Getenv("HOSTNAME") 90 | remoteColorService := os.Getenv("AppClrScv") 91 | 92 | // Define a handler to return the webpage 93 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 94 | // Render the template. 95 | tmpl.Execute(w, TemplateModel{ConfigValues: GetAppValues()}) 96 | }) 97 | 98 | // Define the route to return the color data queried by the website 99 | http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) { 100 | if remoteColorService == "" { 101 | ReturnColorData(hostname, "red", w) 102 | } else { 103 | name, color, err := getColorName("http://" + remoteColorService + "/color") 104 | if err != nil { 105 | http.Error(w, err.Error(), http.StatusInternalServerError) 106 | } 107 | ReturnColorData(name, color, w) 108 | } 109 | }) 110 | 111 | // Listen on port 8080. 112 | http.ListenAndServe(":8080", nil) 113 | } 114 | 115 | type NameValue struct { 116 | Name string `json:"name"` 117 | Value string `json:"value"` 118 | } 119 | 120 | type TemplateModel struct { 121 | ConfigValues []NameValue 122 | } 123 | 124 | // GetAppValues returns the 'App Values' which are all env vars that start with the string 'App' 125 | func GetAppValues() []NameValue { 126 | var result []NameValue 127 | for _, keyValueStr := range os.Environ() { 128 | keyValue := strings.SplitN(keyValueStr, "=", 2) 129 | key := keyValue[0] 130 | value := keyValue[1] 131 | idx := strings.Index(key, "App") 132 | if idx >= 0 { 133 | result = append(result, NameValue{Name: key[idx+3:], Value: value}) 134 | } 135 | } 136 | 137 | return result 138 | } 139 | 140 | // ReturnColorData writes the provided color data to the ResponseWriter 141 | func ReturnColorData(name string, color string, w http.ResponseWriter) { 142 | people := []struct { 143 | Name string `json:"name"` 144 | Time string `json:"time"` 145 | Color string `json:"color"` 146 | }{ 147 | {name, time.Now().Format("2006-01-02 15:04:05"), color}, 148 | } 149 | json.NewEncoder(w).Encode(people) 150 | } 151 | 152 | // getColorName gets a color from the backend 153 | func getColorName(endpoint string) (string, string, error) { 154 | client := &http.Client{} 155 | 156 | req, err := http.NewRequest("GET", endpoint, nil) 157 | if err != nil { 158 | return "", "", err 159 | } 160 | req.Close = true 161 | response, err := client.Do(req) 162 | if err != nil { 163 | return "", "", err 164 | } 165 | 166 | if response.StatusCode != 200 { 167 | return "", "", fmt.Errorf("Error getting response: %d", response.StatusCode) 168 | } 169 | 170 | var data struct { 171 | Name string `json:"name"` 172 | Color string `json:"color"` 173 | } 174 | if err := json.NewDecoder(response.Body).Decode(&data); err != nil { 175 | return "", "", err 176 | } 177 | 178 | return data.Name, data.Color, nil 179 | } 180 | --------------------------------------------------------------------------------