Repository: GoogleCloudPlatform/spinnaker-for-gcp Branch: master Commit: 26455a6d348a Files: 84 Total size: 259.1 KB Directory structure: gitextract_n7i9yuwk/ ├── .gitignore ├── README.md ├── apptest/ │ └── tester/ │ ├── Dockerfile │ ├── build-and-run-tests.sh │ ├── spinnaker-test-job.yaml │ ├── tester.sh │ └── tests/ │ └── basic-suite.yaml ├── ci/ │ ├── CLOUD_BUILD.md │ ├── Dockerfile │ ├── JENKINS.md │ ├── README.md │ ├── cloudbuild.yaml │ └── install.bash ├── samples/ │ └── helloworldwebapp/ │ ├── cleanup_app_and_pipelines.sh │ ├── create_app_and_pipelines.sh │ ├── install.md │ └── templates/ │ ├── pipelines/ │ │ ├── deployprod_json.template │ │ └── deploystaging_json.template │ └── repo/ │ ├── Dockerfile │ ├── cloudbuild_yaml.template │ ├── config/ │ │ ├── prod/ │ │ │ ├── namespace.yaml │ │ │ ├── replicaset_yaml.template │ │ │ └── service.yaml │ │ └── staging/ │ │ ├── namespace.yaml │ │ ├── replicaset_yaml.template │ │ └── service.yaml │ └── src/ │ └── main.go ├── scripts/ │ ├── cli/ │ │ ├── install_hal.sh │ │ ├── install_spin.sh │ │ └── update_hal.sh │ ├── experimental/ │ │ └── configure_for_workload_identity.sh │ ├── expose/ │ │ ├── backend-config.yml │ │ ├── configure_endpoint.sh │ │ ├── configure_hal_security.sh │ │ ├── configure_iap.md │ │ ├── configure_iap.sh │ │ ├── deck-ingress.yml │ │ ├── iap_policy.json │ │ ├── launch_configure_iap.sh │ │ ├── openapi.yml │ │ └── set_iap_properties.sh │ ├── install/ │ │ ├── instructions.txt │ │ ├── provision-spinnaker.md │ │ ├── quick-install.yml │ │ ├── setup.sh │ │ ├── setup_properties.sh │ │ └── spinnakerAuditLog/ │ │ ├── config_json.template │ │ ├── index_js.template │ │ └── package.json │ └── manage/ │ ├── add_gae_account.sh │ ├── add_gce_account.sh │ ├── add_gke_account.sh │ ├── add_missing_properties.sh │ ├── apply_config.sh │ ├── check_cluster_config.sh │ ├── check_duplicate_dirs.sh │ ├── check_git_config.sh │ ├── check_project_mismatch.sh │ ├── cluster_utils.sh │ ├── connect_to_redis.sh │ ├── connect_unsecured.sh │ ├── deploy_application_manifest.sh │ ├── generate_deletion_script.sh │ ├── grant_iap_access.sh │ ├── instructions.txt │ ├── landing_page_base.md │ ├── landing_page_secured.md │ ├── landing_page_unsecured.md │ ├── list_samples.sh │ ├── pull_config.sh │ ├── push_and_apply.sh │ ├── push_config.sh │ ├── restore_backup_to_cloud_shell.sh │ ├── restore_config_utils.sh │ ├── service_utils.sh │ ├── update_console.sh │ ├── update_halyard_daemon.sh │ ├── update_landing_page.sh │ ├── update_management_environment.sh │ └── update_spinnaker_version.sh └── templates/ ├── spinnaker_application_manifest_bottom.yaml ├── spinnaker_application_manifest_middle_secured.yaml ├── spinnaker_application_manifest_middle_unsecured.yaml └── spinnaker_application_manifest_top.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ InstallHalyard.sh samples/helloworldwebapp/templates/pipelines/deployprod.json samples/helloworldwebapp/templates/pipelines/deploystaging.json scripts/expose/*_expanded.* scripts/install/properties scripts/install/properties.* scripts/install/InstallHalyard.sh scripts/install/spinnakerAuditLog/config.json scripts/install/spinnakerAuditLog/index.js scripts/manage/*_expanded.* scripts/manage/delete-all_*.* .idea ================================================ FILE: README.md ================================================ # Install and manage Spinnaker on Google Cloud Platform Spinnaker on Google Cloud Platform is a tool for easily installing a production-ready instance of Spinnaker, and for managing that instance over time. ## Do I want to use this solution? This solution is for… * Anyone who wants an easy path to install open-source Spinnaker, in a production-ready configuration, on Google Cloud Platform * Anyone who wants to "kick the tires" of Spinnaker, to decide if it's the right CD solution for their needs * Administrators who will manage one or more long-running instances of Spinnaker, including adding additional administrators, adding accounts, upgrading, and so on This solution gives you... * Google recommendations and best practices for installing and running Spinnaker on GCP * Pre-integration with many other services that Spinnaker is commonly used with * Sample applications and other helpers for a smoother experience ## What is this solution? Spinnaker for Google Cloud Platform is a solution for installing and managing Spinnaker on Google Cloud Platform. It consists of an installation and management console, Spinnaker and its microservices, and sample applications. ### What is Spinnaker? Spinnaker is an open source, multi-cloud continuous delivery platform for releasing software changes with high velocity and confidence. If you would like to learn more about Spinnaker, please visit the [Spinnaker website](https://www.spinnaker.io). ### What is Deck? Deck is the Spinnaker UI. You access Deck in one of the following ways: * Via port forwarding The management console provides a command for forwarding port 8080, and a button to click to access Deck via that port. * Over the internet, on a publicly available domain This domain is secured with [Identity-Aware Proxy](https://cloud.google.com/iap). ### The management console The management console makes it easy for you to do the following: * Install Spinnaker Spinnaker for Google Cloud Platform makes it easy to get a working version of open-source Spinnaker running on Google Kubernetes Engine. After it's installed, you can make it available to your users. The installation flow begins in the management console after you start the solution. * Manage Spinnaker Use this same management console to manage/operate your Spinnaker installation, including adding administrators, and creating accounts for deploying to additional GKE clusters or other providers. The management flow begins after you finish installing Spinnaker. You can also open it directly via a link from the GKE Applications page in the Google Cloud Console. The management console uses Cloud Shell, with instructions shown in a guide on the right-hand side of the window. The guide shows the commands that will be run, and you can click those commands to copy them into Cloud Shell and run them there. ### What is Cloud Shell? [Cloud Shell](https://cloud.google.com/shell) is a tool in Google Cloud Platform that provides command-line access to GCP. ### How do I find and restore the instructions? * If the instructions in the right-hand pane disappear, just enter the following command in Cloud Shell: ```bash cloudshell launch-tutorial ~/spinnaker-for-gcp/scripts/install/provision-spinnaker.md ``` * If you need to find your way back to the management console, you can relaunch it by following the instructions under Install Spinnaker on Google Cloud Platform. * Refer back to this document if you get lost. ## Am I billed for this? You are billed for Google Cloud Platform resources that are installed as part of Spinnaker for Google Cloud Platform. * [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) * [Cloud Memorystore for Redis](https://cloud.google.com/memorystore/docs/redis/) * [Google Cloud Load Balancing](https://cloud.google.com/load-balancing/) ...and possibly other resources, depending on the options you select when you install and configure Spinnaker. You can use the [Google Cloud Platform Pricing Calculator](https://cloud.google.com/products/calculator/) to estimate the cost of this solution. [Learn more about Google Cloud pricing](https://cloud.google.com/pricing/) & [free trial](https://cloud.google.com/developers/startups/). ## Install and use Spinnaker on Google Cloud Platform You access this solution by clicking the **Go to Spinnaker for Google Cloud Platform** button on the [Spinnaker for GCP page](https://console.cloud.google.com/marketplace/details/google-cloud-platform/spinnaker) in Marketplace. After you've installed Spinnaker for Google Cloud Platform, you can access Spinnaker and the management console from [Google Cloud Console](https://console.cloud.google.com/kubernetes/application). > Note: Spinnaker for Google Cloud Platform doesn't support regional clusters. > If you intend to [install Spinnaker on an existing > cluster](#install_spinnaker_on_existing_cluster), it must be zonal. > Note: Google recommends that you deploy your resources using an account other > than `spinnaker-install-account`. That account is used to install your > spinnaker instance, and resources deployed using that account are installed > into the spinnaker namespace by default. This namespace is not indexed, so > your deployments will time out before they are deemed stable. ### Install Spinnaker on Google Cloud Platform 1. Start the solution from the [Spinnaker for GCP Marketplace page](https://console.cloud.google.com/marketplace/details/google-cloud-platform/spinnaker) by clicking the **Go to Spinnaker for Google Cloud Platform** button. 1. When prompted to Open in Cloud Shell, click **Proceed**. Cloud Shell opens, along with a file tree showing the files in the Spinnaker repository, and instructions. ![The management console](resources/initial_mgmt_console.png) > **Important:** If you've launched the management console at least once before, > you might be prompted, in the shell, to resume with the clone you created > before, update that clone, or clone a new copy of the repository. The first > option is best (`cd` into the existing directory). Don't clone a new copy. The spinnaker-for-gcp repository is cloned into your Cloud Shell. 1. Follow the instructions shown on the screen. The flow in the management console guides you through the installation process, presenting you with commands, which you can copy to the Cloud Shell prompt and then execute by pressing **Enter**. The commands run scripts that automate the process of installing Spinnaker on GKE. If the instruction pane disappears at any time, you can restore it using the following command, from Cloud Shell: ```bash cloudshell launch-tutorial ~/spinnaker-for-gcp/scripts/install/provision-spinnaker.md ``` ### Access Spinnaker After you've installed Spinnaker, you can execute a command to forward ports, which allows you to access the Deck UI and start using Spinnaker. You can share the port-forwarding command with your users, and if they have access to the GKE cluster, they can reach Deck (the Spinnaker UI) on port 8080. Alternatively, you can expose Spinnaker over the public internet, secured using [Identity-Aware Proxy](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp#expose_iap). Both alternatives are described below. #### Access Spinnaker by forwarding ports You can run a command in Cloud Shell in the management console, to forward ports so you can access Spinnaker from localhost:8080. 1. Click to copy the `connect_unsecured.sh` command in the management console, and press **Enter**. This forwards the local port 8080 to port 9000 (the port Deck uses) on the pod running Deck. 1. Click the "Connect to Spinnaker…" link. This highlights the Preview button. 1. Click the highlighted preview button, and select **Preview on port 8080**. ![Click to preview on port 8080](resources/preview_on_8080.png) > **Note:**There is a "Connect to Spinnaker" link displayed. If you click it, > it highlights the preview button, which you then click to select the port. Deck, the Spinnaker user interface, opens in your browser. The Spinnaker documentation site has instructions for using Spinnaker. Back in the management console, there are a few other things you can do: * Make Spinnaker securely available to your teams without having to forward ports * View the Spinnaker audit log * View logs from Spinnaker microservices * Click **Next** to move on to the Spinnaker management portion of the solution. * Share the port-forwarding command with your users If they have access to the GKE cluster, they can reach Deck (the Spinnaker UI) on port 8080. ### Give your users access to Spinnaker over the internet The console includes a command that helps you create a secure endpoint from which to expose Spinnaker to your users, securely, over the internet. > **Note:** If you need to keep Spinnaker private, you can set up port > forwarding for your users. 1. Navigate to step 2 of the installation flow in the Management console ("Connect to Spinnaker"). 1. Under "Expose Spinnaker publicly," click the button to copy the command to the command line, and press **Enter**. The script creates a new endpoint from which to serve your Spinnaker instance. After the script finishes, the guidance in the console changes to show instructions for setting up OAuth so that your users can access this endpoint. 1. Follow those on-screen instructions. Make sure when you create your OAuth credentials that you copy the generated client ID and secret. You'll need to provide them when prompted by the script. > **Note:** This process can take up to an hour, even if it appears that the > script has finished. You now have a Spinnaker endpoint that you can share with your users, who authenticate into it using OAuth2. A link to Spinnaker is displayed in the management console. There is also a link on the GKE applications page for this Spinnaker instance. ### Manage Spinnaker Use the management console to manage your spinnaker instance, including the following actions: * Add administrators (operators) * Add cloud provider accounts A provider is the cloud environment (for example, Google Compute Engine) where you deploy your applications * Upgrade Spinnaker * Invoke Halyard commands to configure Spinnaker * Invoke spin commands to manage Spinnaker resources, like applications and pipelines 1. Access the management portion of this console. Use one of the following options: **If the console is already open:** 1. At the end of the installation flow, click **Next**. 1. Copy the command on the Next steps page and press **Enter**. The instructions pane changes to start the management process. ![Start managing Spinnaker from within the console](resources/start_management_pane.png) **If the console is not already open:** 1. Go to the Google Kubernetes Engine applications page. 1. Open the Spinnaker application. The application description includes a link: Open Management Environment in Cloud Shell. 1. Click that link to open the management console, which now starts with the management/admin functionality. ![Start managing Spinnaker from the GKE Applications page](resources/open_tool_from_gke_application.png) 1. Select your GCP project, and click Start. #### Add administrators for your Spinnaker instance You can give access to more operators, who can then use the management console. 1. On the [IAM permissions page](https://console.developers.google.com/iam-admin/iam), grant the person the 'Owner' role on the GCP project where you've installed Spinnaker. 1. If you are serving Spinnaker on an IAP-secured endpoint, and if the person to whom you're giving operator rights doesn't already have *user* access, use the following command (which is also on step 5 of the management part of the console): ```bash ~/spinnaker-for-gcp/scripts/manage/grant_iap_access.sh ``` ...and follow the instructions on the Cloud Shell command line. #### Add cloud provider accounts You can use the management console to add accounts for as many cloud providers [as Spinnaker supports](https://www.spinnaker.io/setup/install/providers/). You'll need one for each cloud on which your users intend to deploy applications. For example, if they will deploy applications to Google Compute Engine and AWS, you'll add a provider account for each. The management console includes the following command, for adding a GKE account: `~/spinnaker-for-gcp/scripts/manage/add_gke_account.sh` And for Google Compute Engine: `~/spinnaker-for-gcp/scripts/manage/add_gce_account.sh` And for Google App Engine: `~/spinnaker-for-gcp/scripts/manage/add_gae_account.sh` You can run these commands from the management console or enter them in Cloud Shell against an existing Spinnaker instance. #### Run Halyard commands You can invoke [any hal command](https://www.spinnaker.io/reference/halyard/commands) to configure and administer your Spinnaker installation. To do so, just invoke the command from the Cloud Shell in the management console, *after* you've installed Spinnaker #### Upgrade Spinnaker 1. Find out the version you want to upgrade to. The [Versions page](https://www.spinnaker.io/community/releases/versions) lists the stable versions available. 1. In the console, navigate to the management flow: `~/spinnaker-for-gcp/scripts/manage/update_console.sh` 1. Click **Next** until you see the screen titled "Scripts for Common Commands." 1. Under "Upgrade Spinnaker," copy the first command to the shell, and press **Enter**. That command is... ```bash cloudshell edit \ ~/spinnaker-for-gcp/scripts/install/properties ``` 1. Edit the Spinnaker version in the `properties` file that is displayed. ```bash export SPINNAKER_VERSION=1.19.3 ``` The [Spinnaker Versions page](https://www.spinnaker.io/community/releases/versions) shows the latest versions avaiable. 1. Use the following command to invoke Halyard to apply the changes: ```bash ~/spinnaker-for-gcp/scripts/manage/update_spinnaker_version.sh ``` ## Restart the management console If you need to restart the console for any reason (for example, you closed the tab or window), you can restart it in the same way that you [started it](https://console.cloud.google.com/marketplace/details/google-cloud-platform/spinnaker). You can also launch it from the [GKE Applications page](https://console.cloud.google.com/kubernetes/application) in the Google Cloud Console, if you've previously installed Spinnaker for Google Cloud Platform. When you restart the console, it prompts you to resume from where you left off, if you want. ## Upgrade the management console 1. In the management console, navigate to step 3, "Scripts for Common Command," and scroll to the bottom of the page. 1. Run the command shown under "Upgrade Management Environment." The management console is upgraded to include the latest changes. ## Remove Spinnaker for Google Cloud Platform > **Warning:** If you installed Spinnaker on pre-existing infrastructure (GKE > cluster, Redis, service accounts), this script deletes those items. If you > want to keep them, edit the generated cleanup script > (`~/spinnaker-for-gcp/scripts/manage/generate_deletion_script.sh`) to comment > out the specific deletion commands for items you want to keep. If you want to remove Spinnaker for any reason: 1. Open the management console and click Next until you get to the "Delete Spinnaker" page. 1. Copy the command to the Cloud Shell terminal, and press **Enter**. All resources that were created for this Spinnaker instance, and any existing resources on which you might have deployed, are deleted. ## Sample Applications The Spinnaker for Google Cloud Platform solution comes with sample applications to help you get started with Spinnaker. To install them: 1. In the management console, click **Next** until you get to the step titled "Use Spinnaker." 1. Under **Install sample applications and pipelines**, click the button to paste the command, and press **Enter**. Cloud Shell returns a list of available sample apps, numbered. 1. Press the number corresponding to the application you want, or the number corresponding to "Quit" to exit without installing any. 1. Press Enter The tutorial pane now displays guidance for the sample application. 1. To exit the sample app and return to the management portion of the console, click **Start** and then **Next**, then scroll to the bottom of the "Start a new build" page, and run the command under "Return to Spinnaker console." ## Other considerations ### Spinnaker for GCP architecture Spinnaker and its microservices are installed on GKE using the following architecture: ![Architecture of Spinnaker on GCP](resources/spinnaker-k8s-app-architecture.png) ### Install Spinnaker on an existing cluster You can install your Spinnaker instance or instances on pre-existing infrastructure, instead of having this solution create it new. The cluster must have the following: * IP aliases enabled, because this uses a hosted Redis instance * Full Cloud Platform scope for its nodes if you're using the project default service account Before you run the installation script, do the following: 1. Copy and run the following command (which is also available in step 1 of the installation flow): ```bash cloudshell edit \ ~/spinnaker-for-gcp/scripts/install/properties ``` The properties file is opened in the file editor. 1. Edit this section of the `properties` file to identify the Kubernetes cluster on which to install Spinnaker: ```bash # If cluster does not exist, it will be created. export GKE_CLUSTER=$DEPLOYMENT_NAME export ZONE=us-west1-b export REGION=us-west1 ``` 1. Similarly, edit other properties to identify other existing infrastructure and accounts that you want to use, if applicable. For example an existing Cloud Memorystore Redis instance, or a bucket or a service account. In each case, if the infrastructure doesn't exist, the installation script creates it for you. ### Manage multiple Spinnaker installations If you run multiple Spinnaker instances, they must be on separate clusters, and therefore in different Kubernetes contexts. > **Important:** If you're trying to install multiple Spinnaker instances, don't clone multiple copies of the spinnaker-for-gcp repo. To manage one of those installations: 1. Get your credentials. ```bash gcloud container get-credentials ``` 1. Switch to the appropriate Kubernetes context. ```bash kubectl config use-context ``` 1. Pull the configuration stored in that cluster. ```bash ~/spinnaker-for-gcp/scripts/manage/pull_config.sh ``` The config now in `~/spinnaker-for-gcp/scripts/install/properties` is the one for that Spinnaker instance. Perform the usual management tasks available to you, including running `hal` commands. Spinnaker applies those commands to the Spinnaker instance in the chosen context. ================================================ FILE: apptest/tester/Dockerfile ================================================ FROM gcr.io/cloud-marketplace-tools/testrunner:0.1.2 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ gettext \ jq \ uuid-runtime \ wget \ curl \ && rm -rf /var/lib/apt/lists/* RUN wget -q -O /bin/kubectl \ https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubectl \ && chmod 755 /bin/kubectl COPY tests/basic-suite.yaml /tests/basic-suite.yaml COPY tester.sh /tester.sh WORKDIR / ENTRYPOINT ["/tester.sh"] ================================================ FILE: apptest/tester/build-and-run-tests.sh ================================================ #!/bin/bash # # Would expect this to be deleted once these tests are properly integrated with GCP Marketplace Verification Pipeline. bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } if [ -z "$PROJECT_ID" ]; then PROJECT_ID=$(gcloud info --format='value(config.project)') fi if [ -z "$PROJECT_ID" ]; then bold "Please set PROJECT_ID env var." exit 1 fi export PROJECT_ID docker build -t gcr.io/$PROJECT_ID/spinnaker-c2d-tests . docker push gcr.io/$PROJECT_ID/spinnaker-c2d-tests:latest kubectl delete job spinnaker-test-job envsubst < spinnaker-test-job.yaml | kubectl apply -f - kubectl wait --for condition=complete job spinnaker-test-job kubectl logs -l job-name=spinnaker-test-job ================================================ FILE: apptest/tester/spinnaker-test-job.yaml ================================================ # Would expect this to be deleted once these tests are properly integrated with GCP Marketplace Verification Pipeline. apiVersion: batch/v1 kind: Job metadata: name: spinnaker-test-job spec: template: spec: containers: - name: spinnaker-tester-container image: gcr.io/$PROJECT_ID/spinnaker-c2d-tests restartPolicy: Never ================================================ FILE: apptest/tester/tester.sh ================================================ #!/bin/bash # # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -xeo pipefail shopt -s nullglob export CLOUDDRIVER_ADDR="spin-clouddriver.spinnaker" export GATE_ADDR="spin-gate.spinnaker" for test in /tests/*; do testrunner -logtostderr "--test_spec=${test}" done ================================================ FILE: apptest/tester/tests/basic-suite.yaml ================================================ actions: - name: Clouddriver is up and healthy bashTest: script: curl -k "http://{{ .Env.CLOUDDRIVER_ADDR }}:7002/health" | jq -r .status expect: stdout: equals: 'UP' exitCode: equals: 0 - name: Gate returns credentials and they include default account bashTest: script: curl -k "http://{{ .Env.GATE_ADDR }}:8084/credentials" | jq [.[].name] expect: stdout: contains: '"spinnaker-install-account"' exitCode: equals: 0 ================================================ FILE: ci/CLOUD_BUILD.md ================================================ # Using Cloud Build to install Spinnaker for GCP ## A note about Shared VPC support You can't use Cloud Build to install Spinnaker for GCP with a shared VPC. For Shared VPC support, conduct the [setup in Cloud Shell](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp). ## Service account To install Spinnaker for GCP, you need to grant the [Cloud Build service account](https://console.cloud.google.com/cloud-build/settings) the following roles: - Cloud Functions Developer - roles/cloudfunctions.developer - Compute Network Viewer - roles/compute.networkViewer - Kubernetes Engine Admin - roles/container.admin - Create Service Accounts - roles/iam.serviceAccountCreator - Pub/Sub Editor - roles/pubsub.editor - Cloud Memorystore Redis Admin - roles/redis.admin - Service Usage Admin - roles/serviceusage.serviceUsageAdmin - Source Repository Administrator - roles/source.admin - Storage Admin - roles/storage.admin - Project IAM Admin - roles/resourcemanager.projectIamAdmin - Service Account User - roles/iam.serviceAccountUser You can grant these roles using the IAM UI or with [gcloud](https://cloud.google.com/sdk/gcloud/reference/projects/add-iam-policy-binding). ## Enable Cloud Resource Manager API For Cloud Build to successfully retrieve IAM policies, you must enable the Cloud Resource Manager API. Visit this URL, substituting your project id. https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=[PROJECT_ID] ## Properties file To get Cloud Build to install Spinnaker for GCP, you need to generate a properties file: 1. Run [setup_properties.sh](../scripts/install/setup_properties.sh). 1. Copy that file to the directory containing the Cloud Build YAML, so the installation script can access it while executing the Cloud Build job. ## Submitting a Build Cloud Builds can be triggered using [gcloud](https://cloud.google.com/cloud-build/docs/running-builds/start-build-manually), [build triggers](https://cloud.google.com/cloud-build/docs/running-builds/automate-builds), or [GitHub app triggers](https://cloud.google.com/cloud-build/docs/create-github-app-triggers). The solution in this repository installs Spinnaker for GCP using a gcloud-triggered build. Follow these steps to start a build: 1. Create a new directory. The contents of this directory will be submitted to Cloud Build. 2. Place the generated properties file into that directory. 3. Copy the [cloudbuild.yaml](cloudbuild.yaml) file into the directory and edit the `user.name` and `user.email` used in the Git configuration steps. ```yaml - name: gcr.io/cloud-builders/git args: ['config', '--global', 'user.name', ''] - name: gcr.io/cloud-builders/git args: ['config', '--global', 'user.email', ''] ``` 4. Copy the [Dockerfile](Dockerfile) and [install.bash](install.bash) file into the directory. 5. Submit the build to Cloud Build: `gcloud builds submit --timeout "25m" --config cloudbuild.yaml --project PROJECT_ID .` Cloud Build will execute the job, installing Spinnaker for GCP. If you make any changes to the properties file, re-run the job. Additional instructions for how to access or manage the deployed Spinnaker application are available [here](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp#access_spinnaker). ================================================ FILE: ci/Dockerfile ================================================ FROM gcr.io/cloud-builders/gcloud RUN apt-get -q update && apt-get install -qqy \ jq \ gettext-base ENTRYPOINT [] ================================================ FILE: ci/JENKINS.md ================================================ # Using Jenkins to install Spinnaker for GCP You can use Jekins to install Spinnaker for GCP. The Jenkins agent executing the job must be installed on a Unix-like operating system. The following section assumes you have an existing Jenkins server. If not, consider one of the [Jenkins solutions on Google Cloud](https://cloud.google.com/jenkins/). ## Jenkins on GCP If your Jenkins server is running on GCP, follow [best practices](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#best_practices) for managing its service account. Your Jenkins server must have full access to all Google Cloud APIs to successfully install Spinnaker for GCP. See the [Compute Engine documentation](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#changeserviceaccountandscopes) for guidance on how to modify an instance's Google Cloud API access scopes. You can't use Jenkins to install Spinnaker for GCP with a shared VPC. For Shared VPC support, conduct the [setup in Cloud Shell](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp). ## Dependencies There are several dependencies that must be available to the Jenkins server before it can be used to install Spinnaker for GCP. ### Google Cloud SDK The Google Cloud SDK is required to provision GCP resources. Install a [versioned archive](https://cloud.google.com/sdk/docs/downloads-versioned-archives) to the Jenkins server. ### Git Git is required for backing up and restoring the Spinnaker for GCP configuration. Install Git on the Jenkins server by running `sudo apt-get install git-all` ### `kubectl` `kubectl` is required to manage the cluster Spinnaker for GCP will be installed on. Install `kubectl` on the Jenkins server by running `sudo apt-get install kubectl` ### jq jq is required for processing JSON. Install jq to the Jenkins server by running `sudo apt-get install jq` ### AnsiColor Plugin The [AnsiColor Jenkins Plugin](https://plugins.jenkins.io/ansicolor) is required for properly rendering stdout while installing Spinnaker for GCP. Once the plugin has been installed, enable `Color ANSI Console Output` in the build configuration and set the `ANSI color map` to `xterm`. ## Service account The Jenkins server must be configured with a GCP service account with the following roles: - Cloud Functions Developer - roles/cloudfunctions.developer - Compute Network Viewer - roles/compute.networkViewer - Kubernetes Engine Admin - roles/container.admin - Create Service Accounts - roles/iam.serviceAccountCreator - Pub/Sub Editor - roles/pubsub.editor - Cloud Memorystore Redis Admin - roles/redis.admin - Service Usage Admin - roles/serviceusage.serviceUsageAdmin - Source Repository Administrator - roles/source.admin - Storage Admin - roles/storage.admin - Project IAM Admin - roles/resourcemanager.projectIamAdmin - Service Account User - roles/iam.serviceAccountUser These roles can be enabled through the IAM UI or with [gcloud](https://cloud.google.com/sdk/gcloud/reference/projects/add-iam-policy-binding). ## Enable Cloud Resource Manager API The Cloud Resource Manager API must be enabled for Jenkins to successfully retrieve IAM policies. Enable it for your project by visiting the below URL and substituting your project id. https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=[PROJECT_ID] ## Properties file To get Jenkins to install Spinnaker for GCP, you need to generate a properties file and make it available to Jenkins: 1. Run [setup_properties.sh](../scripts/install/setup_properties.sh). 1. Make the resulting properties file available to the installation script as it executes the Jenkins job. In this example, the properties file has been uploaded to the Jenkins server using the [Credentials plug-in](https://wiki.jenkins.io/display/JENKINS/Credentials+Plugin). It is made accessible to the job by binding the file to the PROPERTIES variable. Alternatively, you can use a secrets-management solution (like [Hashicorp Vault](https://www.vaultproject.io/)) and configure Jenkins to read it from there. ### Configure the Jenkins job Once the dependencies are fulfilled, follow these steps to configure a job to install Spinnaker for GCP. 1. Create a `New Item` from the Jenkins menu and select a `Freestyle project`. 1. From the configuration screen, enable `Color ANSI Console Output` and set the `ANSI color map` to `xterm`. 1. Under `Build Environment`, enable `Delete workspace before build starts`. 1. Add an `Execute Shell` build step. 1. Configure the build step to retrieve and execute the `setup.sh` script. ```shell #!/usr/bin/env bash set -e git clone https://github.com/GoogleCloudPlatform/spinnaker-for-gcp.git git config --global user.name "jenkins-user" git config --global user.email "jenkins-user@example.com" PARENT_DIR=$WORKSPACE PROPERTIES_FILE=$PROPERTIES CI=true $WORKSPACE/spinnaker-for-gcp/scripts/install/setup.sh ``` In the above example, the Git `user.name` and `user.email` must be configured before you run `setup.sh`. Git operations can also be managed using the [Jenkins Git plugin](https://plugins.jenkins.io/git). `setup.sh` requires several variables to be passed in: - `PARENT_DIR`: The absolute path for the Jenkins workspace. Jenkins makes this available via `$WORKSPACE`. - `PROPERTIES_FILE`: This is the absolute path to your generated Spinnaker for GCP properties file. - `CI`: This must be set to `true` when running `setup.sh` outside of Cloud Shell. 1. Execute the job to install Spinnaker for GCP. If you change the properties file, apply the change by re-running the job. Additional instructions for how to access or manage the deployed Spinnaker application are available [here](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp#access_spinnaker). ================================================ FILE: ci/README.md ================================================ # Installing Spinnaker for GCP on a Continous Integration Server You can install Spinnaker for GCP using a continous integration Server. A CI server can be used to conduct the initial installation and to apply updates when the Spinnaker for GCP properties file changes. Solutions for [Google Cloud Build](CLOUD_BUILD.md) and [Jenkins](JENKINS.md) are available. ================================================ FILE: ci/cloudbuild.yaml ================================================ steps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-f', 'Dockerfile', '-t', 'installer', '.'] - name: gcr.io/cloud-builders/git args: ['clone', 'https://github.com/GoogleCloudPlatform/spinnaker-for-gcp.git'] - name: gcr.io/cloud-builders/git args: ['config', '--global', 'user.name', ''] - name: gcr.io/cloud-builders/git args: ['config', '--global', 'user.email', ''] - name: 'installer' args: ['bash', './install.bash'] env: - 'TERM=xterm' ================================================ FILE: ci/install.bash ================================================ #!/bin/bash set -e PARENT_DIR=/workspace PROPERTIES_FILE=/workspace/properties CI=true /workspace/spinnaker-for-gcp/scripts/install/setup.sh ================================================ FILE: samples/helloworldwebapp/cleanup_app_and_pipelines.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } cd ~/cloudshell_open/spinnaker-for-gcp/ source scripts/install/properties scripts/manage/check_project_mismatch.sh read -p ". $(tput bold)You are about to delete all resources from the helloworldwebapp application and pipelines. This step is not reversible. Do you wish to continue (Y/n)? $(tput sgr0)" yn case $yn in [Yy]* ) ;; "" ) ;; * ) exit;; esac bold "Deleting Cloud Source Repository..." gcloud source repos delete spinnaker-for-gcp-helloworldwebapp rm -rf ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp bold "Deleting helloworldwebapp-prod and helloworldwebapp-staging Kubernetes resources..." kubectl delete -f samples/helloworldwebapp/templates/repo/config/staging/namespace.yaml kubectl delete -f samples/helloworldwebapp/templates/repo/config/prod/namespace.yaml bold "Deleting Cloud Build trigger..." for trigger in $(gcloud alpha builds triggers list --filter triggerTemplate.repoName=spinnaker-for-gcp-helloworldwebapp --format 'get(id)'); do gcloud alpha builds triggers delete -q $trigger done bold "Deleting Kubernetes manifests..." gsutil -m rm -r gs://$BUCKET_NAME/helloworldwebapp-manifests bold "Deleting GCR images..." for digest in $(gcloud container images list-tags gcr.io/${PROJECT_ID}/spinnaker-for-gcp-helloworldwebapp --format='get(digest)'); do gcloud container images delete -q --force-delete-tags "gcr.io/${PROJECT_ID}/spinnaker-for-gcp-helloworldwebapp@${digest}" done bold "Deleting Spinnaker helloworldwebapp application and pipelines..." set -x ~/spin pipeline delete -a helloworldwebapp -n "Deploy to Staging" ~/spin pipeline delete -a helloworldwebapp -n "Deploy to Production" ~/spin application delete helloworldwebapp { set +x ;} 2> /dev/null bold "Finished cleaning up helloworldwebapp resources." ================================================ FILE: samples/helloworldwebapp/create_app_and_pipelines.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh pushd ~/cloudshell_open/spinnaker-for-gcp/samples/helloworldwebapp if ! ~/spin app list &> /dev/null ; then bold "Spinnaker instance is not reachable via the Spin CLI. Please make sure the Spinnaker \ instance is reachable with port-forwarding or is exposed publicly. To port-forward the Spinnaker UI, run this command: ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/connect_unsecured.sh If you would instead like to expose the service with a domain behind Identity-Aware Proxy, \ run this command: ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_endpoint.sh " exit 1 fi if [ ! -d ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp ]; then bold 'Creating GCR repo "spinnaker-for-gcp-helloworldwebapp" in Spinnaker project...' gcloud source repos create spinnaker-for-gcp-helloworldwebapp mkdir -p ~/$PROJECT_ID gcloud source repos clone spinnaker-for-gcp-helloworldwebapp ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp fi bold 'Adding/Updating Kubernetes config files, sample Go application, and cloud build files in sample repo...' cp -r templates/repo/config ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/ cp -r templates/repo/src ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/ cp templates/repo/Dockerfile ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/ cat templates/repo/cloudbuild_yaml.template | envsubst '$BUCKET_NAME' > ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/cloudbuild.yaml cat ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/staging/replicaset_yaml.template | envsubst > ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/staging/replicaset.yaml rm ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/staging/replicaset_yaml.template cat ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/prod/replicaset_yaml.template | envsubst > ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/prod/replicaset.yaml rm ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp/config/prod/replicaset_yaml.template pushd ~/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp git add * git commit -m "Add source, build, and manifest files." git push popd if [ -z $(gcloud alpha builds triggers list --filter triggerTemplate.repoName=spinnaker-for-gcp-helloworldwebapp --format 'get(id)') ]; then bold "Creating Cloud Build build trigger for helloworld app..." gcloud alpha builds triggers create cloud-source-repositories \ --repo spinnaker-for-gcp-helloworldwebapp \ --branch-pattern master \ --build-config cloudbuild.yaml \ --included-files "src/**,config/**" fi bold "Creating helloworldwebapp Spinnaker application..." ~/spin app save --application-name helloworldwebapp --cloud-providers kubernetes --owner-email $IAP_USER bold 'Creating "Deploy to Staging" Spinnaker pipeline...' cat templates/pipelines/deploystaging_json.template | envsubst > templates/pipelines/deploystaging.json ~/spin pi save -f templates/pipelines/deploystaging.json export DEPLOY_STAGING_PIPELINE_ID=$(~/spin pi get -a helloworldwebapp -n 'Deploy to Staging' | jq -r '.id') bold 'Creating "Deploy to Prod" Spinnaker pipeline...' cat templates/pipelines/deployprod_json.template | envsubst > templates/pipelines/deployprod.json ~/spin pi save -f templates/pipelines/deployprod.json popd ================================================ FILE: samples/helloworldwebapp/install.md ================================================ # Install and run sample application and pipelines ## Introduction Try out Spinnaker using the sample application provided with your Spinnaker instance. It comes with... * A sample "hello world" Go application * A Cloud Build trigger to build an image from source * Sample Spinnaker pipelines to deploy the image and validate the application in a progression from staging environment to production To proceed, make sure the Spinnaker instance is reachable with port-forwarding or is exposed publicly. Select the project containing your Spinnaker instance, then click **Start**, below. ## Create application and pipelines Run this command to create the required resources: ```bash ~/cloudshell_open/spinnaker-for-gcp/samples/helloworldwebapp/create_app_and_pipelines.sh ``` ### Resources created: The source code is hosted in a repository in [Cloud Source Repository](https://source.cloud.google.com/{{project-id}}/spinnaker-for-gcp-helloworldwebapp) in the same project as your Spinnaker cluster. This repository contains a few other items: * Kubernetes configs for the application These are used to deploy the application and validate the service. * A [Cloud Build config](https://source.cloud.google.com/{{project-id}}/spinnaker-for-gcp-helloworldwebapp/+/master:cloudbuild.yaml) This builds the image and copies the Kubernetes configs to the Spinnaker GCS bucket. * A [Cloud Build trigger](https://console.developers.google.com/cloud-build/triggers?project={{project-id}}) This executes the Cloud Build config when any source code or manifest files are changed under src/** or config/** in the repository. Cloud Build creates an [image](https://gcr.io/{{project-id}}/spinnaker-for-gcp-helloworldwebapp) from source and tags that image with the short commit hash. The script also creates two Kubernetes namespaces... * **helloworldwebapp-staging** * **helloworldwebapp-prod** ...and the **helloworldwebapp-service** service in each of those namespaces, in the [Spinnaker Kubernetes cluster](https://console.developers.google.com/kubernetes/discovery?project={{project-id}}). These services expose the Go application for staging and prod environments. This process creates two Spinnaker pipelines under the **helloworldwebapp** Spinnaker application: * **Deploy to Staging** This triggers on a newly completed GCB build, and deploys the image to the **helloworldwebapp-staging** namespace. It then runs a validation job to check the health status of the service. * **Deploy to Production** This starts on a successful **Deploy to Staging** run and Blue/Green deploys the tested image to **helloworldwebapp-prod** namespace. It then runs the health validation job. On success, the old replicaset is scaled down after a 5 minute wait period. On failure, the old replicaset is re-enabled and the new replicaset is disabled. A Pub/Sub notification of the failure is sent via the preconfigured Pub/Sub publisher. You can navigate to your Spinnaker UI to see these pipelines. ## Start a new build To build and deploy an image, just change some [source code](https://source.cloud.google.com/{{project-id}}/spinnaker-for-gcp-helloworldwebapp/+/master:src/main.go) or [manifest files](https://source.cloud.google.com/{{project-id}}/spinnaker-for-gcp-helloworldwebapp/+/master:config/) and push the change to the master branch. The repository is already cloned to your home directory. Make some changes to the source code... ```bash cloudshell edit ~/{{project-id}}/spinnaker-for-gcp-helloworldwebapp/src/main.go ``` ...and commit the changes: ```bash cd ~/{{project-id}}/spinnaker-for-gcp-helloworldwebapp git commit -am "Cool new features" git push ``` The new commit triggers the chain of events... 1. Cloud Build builds the image. 2. The **Deploy to Staging** pipeline deploys the image to staging and validates it. 3. The **Deploy to Production** pipeline promotes the image to production and validates it. Visit the Spinnaker UI to verify that the pipelines complete successfully. After the pipelines finish, the [**helloworldwebapp-services**](https://console.developers.google.com/kubernetes/discovery?project={{project-id}}) hosting the Go application will now be up and healthy. Click on the **endpoints** for each service to see a "Hello World" page! ### Clean-up Run this command to delete all the resources created above: ```bash ~/cloudshell_open/spinnaker-for-gcp/samples/helloworldwebapp/cleanup_app_and_pipelines.sh && cd ~/cloudshell_open/spinnaker-for-gcp ``` ### Return to Spinnaker console Run this command to return to the management environment: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_console.sh ``` ================================================ FILE: samples/helloworldwebapp/templates/pipelines/deployprod_json.template ================================================ { "application": "helloworldwebapp", "description": "When staging deployment and validation completes, Blue/Green deploy new image to production environment and validate.", "expectedArtifacts": [ { "displayName": "Prod Replicaset", "id": "d9013e3f-e9cd-4f18-ace6-ef14369b7fec", "matchArtifact": { "id": "b4557686-0d7e-4163-8cb5-f7e7f1310fa8", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/prod-replicaset.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Hello World WebApp Image", "id": "4f4d38de-80c3-4bc1-a807-c565bc4024ee", "matchArtifact": { "id": "9aa4d777-1d4e-44f9-8f62-baa33a6a8040", "name": "gcr.io/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp", "type": "docker/image" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Prod Namespace", "id": "730af75f-50ad-49ab-b754-ec8ae75938f8", "matchArtifact": { "id": "1d82dc39-76e1-4a96-8f9c-7f68b3edfa67", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/prod-namespace.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Prod Service", "id": "76641671-4d6b-4aee-94b4-dba73fbabfcd", "matchArtifact": { "id": "63b7838b-045b-4681-9a54-852cdb22efd0", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/prod-service.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true } ], "keepWaitingPipelines": false, "limitConcurrent": true, "name": "Deploy to Production", "notifications": [ { "level": "pipeline", "publisherName": "$PUBSUB_NOTIFICATION_PUBLISHER", "type": "pubsub", "when": [ "pipeline.failed" ] } ], "parameterConfig": [], "stages": [ { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "expectedArtifacts": [], "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "d9013e3f-e9cd-4f18-ace6-ef14369b7fec", "manifests": [], "moniker": { "app": "helloworldwebapp" }, "name": "Blue/Green Deploy Replicaset to Production", "refId": "1", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [ "4f4d38de-80c3-4bc1-a807-c565bc4024ee" ], "requisiteStageRefIds": [ "8" ], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": true, "options": { "enableTraffic": true, "namespace": "helloworldwebapp-prod", "services": [ "service helloworldwebapp-service" ], "strategy": "redblack" } }, "type": "deployManifest" }, { "failPipeline": true, "instructions": "Continue with deployment?", "judgmentInputs": [], "name": "Manual Judgment", "notifications": [], "refId": "2", "requisiteStageRefIds": [], "type": "manualJudgment" }, { "account": "spinnaker-install-account", "app": "helloworldwebapp", "cloudProvider": "kubernetes", "completeOtherBranchesThenFail": false, "continuePipeline": true, "failPipeline": false, "location": "helloworldwebapp-prod", "manifestName": "job validate-deployment", "mode": "static", "name": "Clean up Validation Job", "options": { "cascading": true }, "refId": "4", "requisiteStageRefIds": [ "5" ], "type": "deleteManifest" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "completeOtherBranchesThenFail": false, "continuePipeline": true, "failPipeline": false, "manifestArtifactAccount": "gcs-install-account", "manifests": [ { "apiVersion": "batch/v1", "kind": "Job", "metadata": { "name": "validate-deployment", "namespace": "helloworldwebapp-prod" }, "spec": { "backoffLimit": 0, "template": { "spec": { "containers": [ { "command": [ "/bin/sh", "-c", "curl --max-time 120 helloworldwebapp-service" ], "image": "appropriate/curl:latest", "name": "validate-deployment" } ], "restartPolicy": "Never" } } } } ], "moniker": { "app": "helloworldwebapp" }, "name": "Validate New Prod", "refId": "5", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requisiteStageRefIds": [ "1" ], "skipExpressionEvaluation": false, "source": "text", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "730af75f-50ad-49ab-b754-ec8ae75938f8", "moniker": { "app": "helloworldwebapp" }, "name": "Deploy Namespace", "refId": "7", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [], "requisiteStageRefIds": [ "2" ], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "76641671-4d6b-4aee-94b4-dba73fbabfcd", "moniker": { "app": "helloworldwebapp" }, "name": "Deploy Service", "refId": "8", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [], "requisiteStageRefIds": [ "7" ], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" }, { "name": "Wait before Scale Down", "refId": "9", "requisiteStageRefIds": [ "17" ], "type": "wait", "waitTime": 300 }, { "account": "spinnaker-install-account", "app": "helloworldwebapp", "cloudProvider": "kubernetes", "cluster": "replicaSet helloworldwebapp-frontend", "criteria": "second_newest", "kind": "replicaSet", "location": "helloworldwebapp-prod", "mode": "dynamic", "name": "Scale Down Old Prod", "refId": "10", "replicas": "0", "requisiteStageRefIds": [ "9" ], "type": "scaleManifest" }, { "failPipeline": true, "instructions": "Validation Failed - Rollback to Old Prod?", "judgmentInputs": [], "name": "Rollback on Failure", "notifications": [], "refId": "11", "requisiteStageRefIds": [ "16" ], "type": "manualJudgment" }, { "account": "spinnaker-install-account", "app": "helloworldwebapp", "cloudProvider": "kubernetes", "cluster": "replicaSet helloworldwebapp-frontend", "criteria": "second_newest", "kind": "replicaSet", "location": "helloworldwebapp-prod", "mode": "dynamic", "name": "Enable Old Prod", "refId": "12", "requisiteStageRefIds": [ "11" ], "type": "enableManifest" }, { "account": "spinnaker-install-account", "app": "helloworldwebapp", "cloudProvider": "kubernetes", "cluster": "replicaSet helloworldwebapp-frontend", "criteria": "newest", "kind": "replicaSet", "location": "helloworldwebapp-prod", "mode": "dynamic", "name": "Disable New Prod", "refId": "13", "requisiteStageRefIds": [ "12" ], "type": "disableManifest" }, { "name": "Validation Failed - Fail Pipeline", "preconditions": [ { "context": { "expression": "${ #stage(\"Validate New Prod\")['status'].toString() == 'SUCCEEDED'}" }, "failPipeline": true, "type": "expression" } ], "refId": "14", "requisiteStageRefIds": [ "13" ], "stageEnabled": { "expression": "${ #stage(\"Validate New Prod\")['status'].toString() != 'SUCCEEDED'}", "type": "expression" }, "type": "checkPreconditions" }, { "completeOtherBranchesThenFail": false, "continuePipeline": false, "failPipeline": false, "name": "Validation Succeeded", "preconditions": [ { "context": { "expression": "${ #stage(\"Validate New Prod\")['status'].toString() == 'SUCCEEDED'}" }, "failPipeline": false, "type": "expression" } ], "refId": "15", "requisiteStageRefIds": [ "4" ], "type": "checkPreconditions" }, { "completeOtherBranchesThenFail": false, "continuePipeline": false, "failPipeline": false, "name": "Validation Failed", "preconditions": [ { "context": { "expression": "${ #stage(\"Validate New Prod\")['status'].toString() != 'SUCCEEDED'}" }, "failPipeline": true, "type": "expression" } ], "refId": "16", "requisiteStageRefIds": [ "4" ], "type": "checkPreconditions" }, { "name": "Old Prod Version Present", "preconditions": [ { "cloudProvider": "kubernetes", "context": { "cluster": "replicaSet helloworldwebapp-frontend", "comparison": ">", "credentials": "spinnaker-install-account", "expected": 1, "moniker": { "app": "helloworldwebapp", "cluster": "replicaSet helloworldwebapp-frontend" }, "regions": [ "helloworldwebapp-prod" ] }, "failPipeline": false, "type": "clusterSize" } ], "refId": "17", "requisiteStageRefIds": [ "15" ], "type": "checkPreconditions" } ], "triggers": [ { "application": "helloworldwebapp", "enabled": true, "pipeline": "$DEPLOY_STAGING_PIPELINE_ID", "status": [ "successful" ], "type": "pipeline" } ] } ================================================ FILE: samples/helloworldwebapp/templates/pipelines/deploystaging_json.template ================================================ { "application": "helloworldwebapp", "description": "On GCB build completion, deploy new image to staging environment and validate.", "expectedArtifacts": [ { "displayName": "Staging Replicaset", "id": "04429e2c-b48c-48e7-ac9b-6fdc4e3d7f59", "matchArtifact": { "id": "f52080c9-c9ce-4406-a869-e04af6c01389", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/staging-replicaset.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Hello World WebApp Image", "id": "4f4d38de-80c3-4bc1-a807-c565bc4024ee", "matchArtifact": { "id": "9aa4d777-1d4e-44f9-8f62-baa33a6a8040", "name": "gcr.io/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp", "type": "docker/image" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Staging Namespace", "id": "097c2e8e-295f-4020-85d9-18ed18f6133b", "matchArtifact": { "id": "4be2cb5b-bfee-43cc-a2e8-c544777f3436", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/staging-namespace.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true }, { "displayName": "Staging Service", "id": "24f64da7-9eac-46df-8b0d-f7092b6a03e4", "matchArtifact": { "id": "09755b68-001a-4d61-bb17-985546618e5f", "name": "gs://$BUCKET_NAME/helloworldwebapp-manifests/.*/staging-service.yaml", "type": "gcs/object" }, "useDefaultArtifact": false, "usePriorArtifact": true } ], "keepWaitingPipelines": false, "limitConcurrent": true, "name": "Deploy to Staging", "parameterConfig": [], "stages": [ { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "expectedArtifacts": [], "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "04429e2c-b48c-48e7-ac9b-6fdc4e3d7f59", "manifests": [], "moniker": { "app": "helloworldwebapp" }, "name": "Deploy Replicaset to Staging", "refId": "1", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [ "4f4d38de-80c3-4bc1-a807-c565bc4024ee" ], "requisiteStageRefIds": [ "8" ], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": true, "options": { "enableTraffic": true, "namespace": "helloworldwebapp-staging", "services": [ "service helloworldwebapp-service" ], "strategy": "highlander" } }, "type": "deployManifest" }, { "account": "spinnaker-install-account", "app": "helloworldwebapp", "cloudProvider": "kubernetes", "completeOtherBranchesThenFail": false, "continuePipeline": true, "failPipeline": false, "location": "helloworldwebapp-staging", "manifestName": "job validate-deployment", "mode": "static", "name": "Clean up Validation Job", "options": { "cascading": true }, "refId": "2", "requisiteStageRefIds": [ "5" ], "type": "deleteManifest" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "completeOtherBranchesThenFail": false, "continuePipeline": true, "failPipeline": false, "manifestArtifactAccount": "gcs-install-account", "manifests": [ { "apiVersion": "batch/v1", "kind": "Job", "metadata": { "name": "validate-deployment", "namespace": "helloworldwebapp-staging" }, "spec": { "backoffLimit": 0, "template": { "spec": { "containers": [ { "command": [ "/bin/sh", "-c", "curl --max-time 120 helloworldwebapp-service" ], "image": "appropriate/curl:latest", "name": "validate-deployment" } ], "restartPolicy": "Never" } } } } ], "moniker": { "app": "helloworldwebapp" }, "name": "Validate Staging", "refId": "5", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requisiteStageRefIds": [ "1" ], "skipExpressionEvaluation": false, "source": "text", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" }, { "comments": "Fails the pipeline if the validation failed.", "name": "Check Validation status", "preconditions": [ { "context": { "expression": "${ #stage(\"Validate Staging\")['status'].toString() == 'SUCCEEDED'}" }, "failPipeline": true, "type": "expression" } ], "refId": "6", "requisiteStageRefIds": [ "2" ], "type": "checkPreconditions" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "097c2e8e-295f-4020-85d9-18ed18f6133b", "moniker": { "app": "helloworldwebapp" }, "name": "Deploy Namespace", "refId": "7", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [], "requisiteStageRefIds": [], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" }, { "account": "spinnaker-install-account", "cloudProvider": "kubernetes", "manifestArtifactAccount": "gcs-install-account", "manifestArtifactId": "24f64da7-9eac-46df-8b0d-f7092b6a03e4", "moniker": { "app": "helloworldwebapp" }, "name": "Deploy Service", "refId": "8", "relationships": { "loadBalancers": [], "securityGroups": [] }, "requiredArtifactIds": [], "requisiteStageRefIds": [ "7" ], "skipExpressionEvaluation": false, "source": "artifact", "trafficManagement": { "enabled": false, "options": { "enableTraffic": false, "services": [] } }, "type": "deployManifest" } ], "triggers": [ { "attributeConstraints": { "status": "SUCCESS" }, "enabled": true, "expectedArtifactIds": [ "4f4d38de-80c3-4bc1-a807-c565bc4024ee" ], "payloadConstraints": {}, "pubsubSystem": "google", "subscriptionName": "gcb-account", "type": "pubsub" } ] } ================================================ FILE: samples/helloworldwebapp/templates/repo/Dockerfile ================================================ FROM alpine COPY src/gopath/bin/helloworldwebapp /go/bin/helloworldwebapp ENTRYPOINT /go/bin/helloworldwebapp ================================================ FILE: samples/helloworldwebapp/templates/repo/cloudbuild_yaml.template ================================================ steps: - name: 'gcr.io/cloud-builders/go' args: [ 'install', '$PROJECT_ID/helloworldwebapp' ] env: [ 'PROJECT_ROOT=$PROJECT_ID/helloworldwebapp' ] dir: 'src' - name: 'ubuntu' entrypoint: 'bash' args: - '-c' - | mkdir config-all # rename config files to be appended with the environment, e.g. staging-service.yaml for env in config/*; do if [ -d $env ]; then for file in $env/*; do cp $file config-all/$(basename $env)-$(basename $file) done fi done - name: 'gcr.io/cloud-builders/docker' args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:$SHORT_SHA', '.' ] images: - 'gcr.io/$PROJECT_ID/$REPO_NAME:$SHORT_SHA' artifacts: objects: location: gs://$BUCKET_NAME/helloworldwebapp-manifests/$SHORT_SHA paths: [ 'config-all/*' ] ================================================ FILE: samples/helloworldwebapp/templates/repo/config/prod/namespace.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: helloworldwebapp-prod ================================================ FILE: samples/helloworldwebapp/templates/repo/config/prod/replicaset_yaml.template ================================================ --- apiVersion: apps/v1 kind: ReplicaSet metadata: annotations: traffic.spinnaker.io/load-balancers: '["service helloworldwebapp-service"]' labels: app: helloworldwebapp name: helloworldwebapp-frontend namespace: helloworldwebapp-prod spec: replicas: 3 selector: matchLabels: app: helloworldwebapp template: metadata: labels: app: helloworldwebapp spec: containers: - image: gcr.io/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp # will be modified on deployment to point at a digest of an image name: helloworldwebapp ================================================ FILE: samples/helloworldwebapp/templates/repo/config/prod/service.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: helloworldwebapp-service namespace: helloworldwebapp-prod spec: ports: - protocol: TCP port: 80 selector: frontedBy: helloworldwebapp-prod # will be applied to backends by Spinnaker type: LoadBalancer loadBalancerIP: "" ================================================ FILE: samples/helloworldwebapp/templates/repo/config/staging/namespace.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: helloworldwebapp-staging ================================================ FILE: samples/helloworldwebapp/templates/repo/config/staging/replicaset_yaml.template ================================================ --- apiVersion: apps/v1 kind: ReplicaSet metadata: annotations: traffic.spinnaker.io/load-balancers: '["service helloworldwebapp-service"]' labels: app: helloworldwebapp name: helloworldwebapp-frontend namespace: helloworldwebapp-staging spec: replicas: 3 selector: matchLabels: app: helloworldwebapp template: metadata: labels: app: helloworldwebapp spec: containers: - image: gcr.io/$PROJECT_ID/spinnaker-for-gcp-helloworldwebapp # will be modified on deployment to point at a digest of an image name: helloworldwebapp ================================================ FILE: samples/helloworldwebapp/templates/repo/config/staging/service.yaml ================================================ --- apiVersion: v1 kind: Service metadata: name: helloworldwebapp-service namespace: helloworldwebapp-staging spec: ports: - protocol: TCP port: 80 selector: frontedBy: helloworldwebapp-staging # will be applied to backends by Spinnaker type: LoadBalancer loadBalancerIP: "" ================================================ FILE: samples/helloworldwebapp/templates/repo/src/main.go ================================================ package main import ( "io" "net/http" ) func hello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "

Hello World

") } func main() { http.HandleFunc("/", hello) http.ListenAndServe(":80", nil) } ================================================ FILE: scripts/cli/install_hal.sh ================================================ #!/usr/bin/env bash HALYARD_DAEMON_PID_FILE=~/hal/halyard/pid function kill_daemon() { pkill -F $HALYARD_DAEMON_PID_FILE } if [ -f "$HALYARD_DAEMON_PID_FILE" ]; then HALYARD_DAEMON_PID=$(cat $HALYARD_DAEMON_PID_FILE) set +e ps $HALYARD_DAEMON_PID &> /dev/null exit_code=$? set -e if [ "$exit_code" == "0" ]; then kill_daemon fi fi # Just in case the pid file doesn't match the daemon that's actually listening on the port. pkill -f '/opt/halyard/lib/halyard-web' || true pkill -f "$HOME/hal/halyard/lib/halyard-web" || true curl -O https://raw.githubusercontent.com/spinnaker/halyard/master/install/debian/InstallHalyard.sh sudo bash InstallHalyard.sh --user $USER -y $@ retVal=$? if [ $retVal == 13 ]; then exit 13 fi mkdir -p ~/hal/log sudo mv /etc/bash_completion.d/hal ~/hal/hal_completion sudo mv /usr/local/bin/hal ~/hal sudo mv /usr/local/bin/update-halyard ~/hal sudo rm -rf ~/hal/halyard/ && sudo mv /opt/halyard ~/hal sudo rm -rf ~/hal/spinnaker/ && sudo mv /opt/spinnaker ~/hal sed -i 's:^. /etc/bash_completion.d/hal:# . /etc/bash_completion.d/hal\n. ~/hal/hal_completion\nalias hal=~/hal/hal:' ~/.bashrc sed -i s:/opt/halyard:~/hal/halyard:g ~/hal/hal sed -i s:/var/log/spinnaker/halyard:~/hal/log:g ~/hal/hal sudo sed -i s:/opt/spinnaker:~/hal/spinnaker:g ~/hal/halyard/bin/halyard sed -i 's:rm -rf /opt/halyard:rm -rf ~/hal/halyard:g' ~/hal/update-halyard sed -i "s:^ HAL_USER=.*$: HAL_USER=$(cat ~/hal/spinnaker/config/halyard-user):g" ~/hal/update-halyard sed -i s:/etc/bash_completion.d/hal:~/hal/hal_completion: ~/hal/update-halyard ================================================ FILE: scripts/cli/install_spin.sh ================================================ #!/usr/bin/env bash curl -LO https://storage.googleapis.com/spinnaker-artifacts/spin/$(curl -s https://storage.googleapis.com/spinnaker-artifacts/spin/latest)/linux/amd64/spin chmod +x spin mv spin ~ grep -q '^alias spin=~/spin' ~/.bashrc || echo 'alias spin=~/spin' >> ~/.bashrc mkdir -p ~/.spin # If there is no properties file, generate a new ~/.spin/config relying on port-forwarding. if [ ! -f "$HOME/cloudshell_open/spinnaker-for-gcp/scripts/install/properties" ]; then cat >~/.spin/config <~/.spin/config < /dev/null exit_code=$? set -e if [ "$exit_code" == "0" ]; then kill_daemon fi fi # Just in case the pid file doesn't match the daemon that's actually listening on the port. pkill -f '/opt/halyard/lib/halyard-web' || true pkill -f "$HOME/hal/halyard/lib/halyard-web" || true HAL_USER=$(cat ~/hal/spinnaker/config/halyard-user) if [ -z "$HAL_USER" ]; then echo >&2 "Unable to derive halyard user, likely a corrupted install. Aborting." exit 1 fi sudo groupadd halyard || true sudo groupadd spinnaker || true sudo usermod -G halyard -a $HAL_USER || true sudo usermod -G spinnaker -a $HAL_USER || true sudo mkdir -p /var/log/spinnaker/halyard sudo chown $HAL_USER:halyard /var/log/spinnaker/halyard sudo chmod 755 /var/log/spinnaker /var/log/spinnaker/halyard sudo HAL_USER=$HAL_USER ~/hal/update-halyard $@ retVal=$? if [ $retVal == 13 ]; then exit 13 fi mkdir -p ~/hal/log sudo mv /usr/local/bin/hal ~/hal sudo rm -rf ~/hal/halyard/ && sudo mv /opt/halyard ~/hal sudo mv /usr/local/bin/update-halyard ~/hal sed -i 's:^. /etc/bash_completion.d/hal:# . /etc/bash_completion.d/hal\n. ~/hal/hal_completion\nalias hal=~/hal/hal:' ~/.bashrc sed -i s:/opt/halyard:~/hal/halyard:g ~/hal/hal sed -i s:/var/log/spinnaker/halyard:~/hal/log:g ~/hal/hal sudo sed -i s:/opt/spinnaker:~/hal/spinnaker:g ~/hal/halyard/bin/halyard sed -i 's:rm -rf /opt/halyard:rm -rf ~/hal/halyard:g' ~/hal/update-halyard sed -i "s:^ HAL_USER=.*$: HAL_USER=$(cat ~/hal/spinnaker/config/halyard-user):g" ~/hal/update-halyard sed -i s:/etc/bash_completion.d/hal:~/hal/hal_completion: ~/hal/update-halyard ================================================ FILE: scripts/experimental/configure_for_workload_identity.sh ================================================ #!/usr/bin/env bash # Prior to running this script, please ensure that you are running these versions or later: # export SPINNAKER_VERSION=release-1.17.x-latest-validated # export HALYARD_VERSION=1.26.0 # # This script is intended to be run after the initial setup.sh script completes and Spinnaker is up # and running (without Workload Identity enabled). # # The expected workflow is as follows: # - Generate the properties file by running the setup_properties.sh script # - Modify the properties file to specify the above 2 Spinnaker/Halyard versions (or later versions) # - Run setup.sh # - Once Spinnaker is up and running, run this (configure_for_workload_identity.sh) script # # Note that this script results in each Spinnaker pod still using the default Kubernetes service account, # and the default service account in the halyard and spinnaker namespaces being bound to one Google # service account (spinnaker-wi-acct). If you want to specify a different Kubernetes service account # for any service, you can do so via the `serviceAccountName` setting described here: # https://www.spinnaker.io/reference/halyard/custom/#kubernetes # You would also need to make the appropriate bindings between that Kubernetes service account and a # Google service account. # # The roles assigned are sufficient for deployment to GKE. If you intend to deploy to GCE or GAE, you # will need to assign the appropriate roles to the spinnaker-wi-acct Google service account, similar # to what we do in these helper scripts for the non-Workload Identity setup: # https://github.com/GoogleCloudPlatform/spinnaker-for-gcp/blob/master/scripts/manage/add_gce_account.sh # https://github.com/GoogleCloudPlatform/spinnaker-for-gcp/blob/master/scripts/manage/add_gae_account.sh bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } PROPERTIES_FILE="$HOME/cloudshell_open/spinnaker-for-gcp/scripts/install/properties" source "$PROPERTIES_FILE" bold "Enabling workload identity on cluster $GKE_CLUSTER in project $PROJECT_ID..." gcloud beta container clusters update $GKE_CLUSTER \ --zone=$ZONE \ --identity-namespace=$PROJECT_ID.svc.id.goog \ --project=$PROJECT_ID unset CLUSTER_STATUS while [ "$CLUSTER_STATUS" != "RUNNING" ]; do CLUSTER_STATUS=$(gcloud container clusters describe $GKE_CLUSTER \ --zone=$ZONE \ --format="value(status)" \ --project=$PROJECT_ID) sleep 5 echo -n . done echo KSA_NAME=default GSA_NAME=spinnaker-wi-acct GSA_DISPLAY_NAME="Spinnaker Workload Identity service account" bold "Creating Google service account $GSA_NAME..." gcloud iam service-accounts create $GSA_NAME \ --display-name="$GSA_DISPLAY_NAME" \ --project=$PROJECT_ID GSA_EMAIL=$(gcloud iam service-accounts list \ --filter="displayName:$GSA_DISPLAY_NAME" \ --format="value(email)" \ --project=$PROJECT_ID) while [ -z "$GSA_EMAIL" ]; do GSA_EMAIL=$(gcloud iam service-accounts list \ --filter="displayName:$GSA_DISPLAY_NAME" \ --format="value(email)" \ --project=$PROJECT_ID) sleep 5 echo -n . done echo bold "Assigning required roles to $GSA_DISPLAY_NAME..." K8S_REQUIRED_ROLES=(cloudbuild.builds.editor container.admin logging.logWriter monitoring.admin pubsub.admin storage.admin) EXISTING_ROLES=$(gcloud projects get-iam-policy $PROJECT_ID \ --filter="bindings.members:$GSA_EMAIL" \ --format="value(bindings.role)" \ --flatten="bindings[].members") for r in "${K8S_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $EXISTING_ROLES | grep $r)" ]; then bold "Assigning role $r..." gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$GSA_EMAIL" \ --role="roles/$r" \ --format="none" fi done bold "Creating Cloud IAM policy binding between Kubernetes service account halyard/$KSA_NAME and Google service account $GSA_NAME..." gcloud iam service-accounts add-iam-policy-binding \ --role="roles/iam.workloadIdentityUser" \ --member="serviceAccount:$PROJECT_ID.svc.id.goog[halyard/$KSA_NAME]" \ --project=$PROJECT_ID \ $GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com bold "Creating Cloud IAM policy binding between Kubernetes service account spinnaker/$KSA_NAME and Google service account $GSA_NAME..." gcloud iam service-accounts add-iam-policy-binding \ --role="roles/iam.workloadIdentityUser" \ --member="serviceAccount:$PROJECT_ID.svc.id.goog[spinnaker/$KSA_NAME]" \ --project=$PROJECT_ID \ $GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com bold "Annotating Kubernetes service account halyard/$KSA_NAME with Google service account to use ($GSA_NAME)..." kubectl annotate serviceaccount \ --namespace halyard \ $KSA_NAME \ iam.gke.io/gcp-service-account=$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com bold "Annotating Kubernetes service account spinnaker/$KSA_NAME with Google service account to use ($GSA_NAME)..." kubectl annotate serviceaccount \ --namespace spinnaker \ $KSA_NAME \ iam.gke.io/gcp-service-account=$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com NODE_POOL_NAME=$(gcloud container clusters describe $GKE_CLUSTER \ --zone=$ZONE \ --format="value(nodePools[0].name)" \ --project=$PROJECT_ID) bold "Enabling GKE_METADATA_SERVER on node pool $NODE_POOL_NAME..." gcloud beta container node-pools update $NODE_POOL_NAME \ --cluster=$GKE_CLUSTER \ --zone=$ZONE \ --workload-metadata-from-node=GKE_METADATA_SERVER \ --project=$PROJECT_ID ================================================ FILE: scripts/expose/backend-config.yml ================================================ apiVersion: cloud.google.com/v1beta1 kind: BackendConfig metadata: name: config-default namespace: spinnaker spec: iap: enabled: true oauthclientCredentials: secretName: $SECRET_NAME ================================================ FILE: scripts/expose/configure_endpoint.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } pushd ~/cloudshell_open/spinnaker-for-gcp/scripts source ./install/properties ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh DOMAIN_NAME_LENGTH=$(echo -n $DOMAIN_NAME | wc -m) if [ "$DOMAIN_NAME_LENGTH" -gt "63" ]; then echo "Domain name $DOMAIN_NAME is greater than 63 characters. Please specify a \ domain name not longer than 63 characters. The domain name is specified in the \ $HOME/cloudshell_open/spinnaker-for-gcp/scripts/install/properties file." exit 1 fi export IP_ADDR=$(gcloud compute addresses list --filter="name=$STATIC_IP_NAME" \ --format="value(address)" --global --project $PROJECT_ID) if [ -z "$IP_ADDR" ]; then bold "Creating static IP address $STATIC_IP_NAME..." gcloud compute addresses create $STATIC_IP_NAME --global --project $PROJECT_ID export IP_ADDR=$(gcloud compute addresses list --filter="name=$STATIC_IP_NAME" \ --format="value(address)" --global --project $PROJECT_ID) else bold "Using existing static IP address $STATIC_IP_NAME ($IP_ADDR)..." fi if [ $DOMAIN_NAME = "$DEPLOYMENT_NAME.endpoints.$PROJECT_ID.cloud.goog" ]; then EXISTING_SERVICE_NAME=$(gcloud endpoints services list \ --filter="serviceName=$DOMAIN_NAME" --format="value(serviceName)" \ --project $PROJECT_ID) if [ -z "$EXISTING_SERVICE_NAME" ]; then gcurl() { curl -s -H "Authorization:Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Goog-User-Project: $PROJECT_ID" $* } bold "Creating service $DOMAIN_NAME..." gcurl -X POST -d \ "{\"serviceName\":\"$DOMAIN_NAME\",\"producerProjectId\":\"$PROJECT_ID\"}" \ https://servicemanagement.googleapis.com/v1/services/ while [ -z "$SERVICE_NAME" ]; do SERVICE_NAME=$(gcloud endpoints services list \ --filter="serviceName:$DOMAIN_NAME" \ --format="value(serviceName)") sleep 5 echo -n . done echo else bold "Using existing service $EXISTING_SERVICE_NAME..." fi # The service can exist without an endpoint configuration. The presence of the # service configuration title is sufficient to indicate that we have configured # the endpoint. EXISTING_SERVICE_CONFIGURATION_NAME=$(gcloud endpoints services list \ --filter="serviceName=$DOMAIN_NAME" --format="value(serviceConfig.title)" \ --project $PROJECT_ID) if [ -z "$EXISTING_SERVICE_CONFIGURATION_NAME" ]; then bold "Deploying service endpoint configuration for $DOMAIN_NAME..." cat expose/openapi.yml | envsubst > expose/openapi_expanded.yml gcloud endpoints services deploy expose/openapi_expanded.yml --project $PROJECT_ID else bold "Using existing service endpoint configuration for $DOMAIN_NAME..." fi else CURRENT_IP_ADDR=$(dig +short $DOMAIN_NAME) if [ -z "$CURRENT_IP_ADDR" ]; then CURRENT_IP_ADDR="UNRESOLVABLE" fi bold "Using existing domain $DOMAIN_NAME ($CURRENT_IP_ADDR)..." if [ $CURRENT_IP_ADDR != $IP_ADDR ]; then bold "** This domain currently resolves to $CURRENT_IP_ADDR ** You must configure $DOMAIN_NAME's DNS settings such that it instead resolves to $IP_ADDR" fi fi EXISTING_MANAGED_CERT=$(gcloud beta compute ssl-certificates list \ --filter="name=$MANAGED_CERT" --format="value(name)" --project $PROJECT_ID) if [ -z "$EXISTING_MANAGED_CERT" ]; then bold "Creating managed SSL certificate $MANAGED_CERT for domain $DOMAIN_NAME..." gcloud beta compute ssl-certificates create $MANAGED_CERT --domains $DOMAIN_NAME --global \ --project $PROJECT_ID else bold "Using existing managed SSL certificate $EXISTING_MANAGED_CERT..." fi ./expose/launch_configure_iap.sh popd ================================================ FILE: scripts/expose/configure_hal_security.sh ================================================ ~/hal/hal config security api edit --override-base-url https://$DOMAIN_NAME/gate ~/hal/hal config security ui edit --override-base-url https://$DOMAIN_NAME ~/hal/hal config security authn iap edit --audience $AUD_CLAIM ~/hal/hal config security authn iap enable ================================================ FILE: scripts/expose/configure_iap.md ================================================ # Expose Spinnaker ### Configure OAuth consent screen Go to the [OAuth consent screen](https://console.developers.google.com/apis/credentials/consent?project=$PROJECT_ID). Enter an *Application name* (e.g. My Spinnaker) and your *Email address*, and click *Save*. ### Create OAuth credentials Go to the [Credentials page](https://console.developers.google.com/apis/credentials/oauthclient?project=$PROJECT_ID) and create an *OAuth client ID*. Select *Application type: Web application* and click *Create*. Ensure that you note the generated *Client ID* and *Client secret* for your new credentials, as you will need to provide them to the script in the next step. ### Expose Spinnaker and allow for secure access via IAP ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_iap.sh ``` There will be one final IAP configuration step described in the terminal. This phase could take 30-60 minutes. **Spinnaker will be inaccessible during this time.** ## Conclusion Connect to your Spinnaker installation [here](https://$DOMAIN_NAME). ### View Spinnaker Audit Log View the who, what, when and where of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project=$PROJECT_ID&resource=cloud_function&logName=projects%2F$PROJECT_ID%2Flogs%2F$CLOUD_FUNCTION_NAME&minLogLevel=200). ================================================ FILE: scripts/expose/configure_iap.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } pushd ~/cloudshell_open/spinnaker-for-gcp/scripts source ./install/properties ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh EXISTING_SECRET_NAME=$(kubectl get secret -n spinnaker \ --field-selector metadata.name=="$SECRET_NAME" \ -o json | jq .items[0].metadata.name) if [ $EXISTING_SECRET_NAME == 'null' ]; then bold "Creating Kubernetes secret $SECRET_NAME..." read -p 'Enter your OAuth credentials Client ID: ' CLIENT_ID read -p 'Enter your OAuth credentials Client secret: ' CLIENT_SECRET cat >~/.spin/config < expose/configure_iap_expanded.md cloudshell launch-tutorial expose/configure_iap_expanded.md popd ================================================ FILE: scripts/expose/openapi.yml ================================================ swagger: "2.0" info: title: Spinnaker for GCP - $PROJECT_ID version: 1.0.0 host: $DOMAIN_NAME x-google-endpoints: - name: $DOMAIN_NAME target: $IP_ADDR x-google-allow: all basePath: / paths: {} ================================================ FILE: scripts/expose/set_iap_properties.sh ================================================ #!/usr/bin/env bash if [ -z $CLIENT_ID ]; then SECRET_JSON=$(kubectl get secret -n spinnaker $SECRET_NAME -o json) export CLIENT_ID=$(echo $SECRET_JSON | jq -r .data.client_id | base64 -d) export CLIENT_SECRET=$(echo $SECRET_JSON | jq -r .data.client_secret | base64 -d) fi export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") bold "Querying for backend service id..." export BACKEND_SERVICE_ID=$(gcloud compute backend-services list --project $PROJECT_ID \ --filter="iap.oauth2ClientId:$CLIENT_ID AND description:spinnaker/spin-deck" --format="value(id)") while [ -z "$BACKEND_SERVICE_ID" ]; do bold "Waiting for backend service to be provisioned..." sleep 30 export BACKEND_SERVICE_ID=$(gcloud compute backend-services list --project $PROJECT_ID \ --filter="iap.oauth2ClientId:$CLIENT_ID AND description:spinnaker/spin-deck" --format="value(id)") done export AUD_CLAIM=/projects/$PROJECT_NUMBER/global/backendServices/$BACKEND_SERVICE_ID ================================================ FILE: scripts/install/instructions.txt ================================================ +-------------------------------------------------------------------------------------------------------+ | | | To reopen the installation instructions in the right-hand pane at any time, enter: | | | | cloudshell launch-tutorial ~/cloudshell_open/spinnaker-for-gcp/scripts/install/provision-spinnaker.md | | | +-------------------------------------------------------------------------------------------------------+ ================================================ FILE: scripts/install/provision-spinnaker.md ================================================ # Install Spinnaker ## Select GCP project Select the project in which you'll install Spinnaker, then click **Start**, below. ## Spinnaker Installation Click the **Copy to Cloud Shell** button for each command below, then press **Enter** to run each commmand. ### Configure Git If you haven't already configured Git, use the commands below to do so now. Replace `[EMAIL_ADDRESS]` with your Git email address, and replace `[USERNAME]` with your Git username. ```bash git config --global user.email "[EMAIL_ADDRESS]" git config --global user.name "[USERNAME]" ``` ### Configure the environment Now let's provision Spinnaker within your project {{project-id}}. ```bash PROJECT_ID={{project-id}} ~/cloudshell_open/spinnaker-for-gcp/scripts/install/setup_properties.sh ``` After that script finishes, you can use the command below to open the properties file for your Spinnaker installation. This is optional. ```bash cloudshell edit ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ``` **Proceed with caution**. If you edit this file, the installation might not work as expected. ### Begin the installation **This will take some time** ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/install/setup.sh ``` Watch the Cloud Shell command line to see when it completes, then click **Next** to continue to the next step. ## Connect to Spinnaker You'll now run commands to... * connect to Spinnaker * open the Spinnaker UI (Deck) in a browser window You have two choices: * forward port 8080 to tunnel to Spinnaker from your Cloud Shell * expose Deck securely via a public IP ### Forward the port to Deck, and connect Don't use the `hal deploy connect` command. Instead, use the following command only. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/connect_unsecured.sh ``` To connect to the Deck UI, click on the Preview button above and select "Preview on port 8080": ![Image](https://github.com/GoogleCloudPlatform/spinnaker-for-gcp/raw/master/scripts/manage/preview_button.png) ### Expose Spinnaker publicly If you would like to connect to Spinnaker without relying on port forwarding, we can expose it via a secure domain behind the [Identity-Aware Proxy](https://cloud.google.com/iap/). Note that this phase could take 30-60 minutes. **Spinnaker will be inaccessible during this time.** ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_endpoint.sh ``` ## Next steps: manage Spinnaker Now that you've installed Spinnaker on Google Kubernetes Engine, and accessed it via port forwarding or made it available over the public internet, you'll use this same console to manage your Spinnaker instance. You can open this console by navigating to the Kubernetes Application on the [Applications](https://console.developers.google.com/kubernetes/application?project={{project-id}}) view. The application's *Next Steps* section contains the relevant links and operator instructions. You can... * Use [Halyard](https://www.spinnaker.io/reference/halyard/) to further configure Spinnaker * Add provider accounts * Upgrade Spinnaker * Add more operators To start managing Spinnaker: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_console.sh ``` ================================================ FILE: scripts/install/quick-install.yml ================================================ apiVersion: v1 kind: Namespace metadata: name: halyard --- apiVersion: v1 kind: Namespace metadata: name: spinnaker --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: spinnaker-admin roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: default namespace: halyard - kind: ServiceAccount name: default namespace: spinnaker --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: halyard-pv-claim namespace: halyard labels: app: halyard-storage-claim spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: standard --- apiVersion: apps/v1 kind: StatefulSet metadata: name: spin-halyard namespace: halyard labels: app: spin stack: halyard spec: serviceName: spin-halyard replicas: 1 selector: matchLabels: app: spin stack: halyard template: metadata: labels: app: spin stack: halyard spec: securityContext: runAsGroup: 1000 runAsUser: 1000 fsGroup: 1000 containers: - name: halyard-daemon image: us-docker.pkg.dev/spinnaker-community/docker/halyard:$HALYARD_VERSION imagePullPolicy: Always command: - /bin/sh args: - -c # We persist the files on a PersistentVolume. To have sane defaults, # we initialise those files from a ConfigMap if they don't already exist. - "test -f /home/spinnaker/.hal/config || cp -R /home/spinnaker/staging/.hal/. /home/spinnaker/.hal/ && /opt/halyard/bin/halyard" readinessProbe: exec: command: - wget - -q - --spider - http://localhost:8064/health resources: requests: cpu: 10m memory: 256Mi ports: - containerPort: 8064 volumeMounts: - name: persistentconfig mountPath: /home/spinnaker/.hal - name: halconfig mountPath: /home/spinnaker/staging/.hal/config subPath: config - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/service-settings/deck.yml subPath: deck.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/service-settings/gate.yml subPath: gate.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/service-settings/fiat.yml subPath: fiat.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/service-settings/redis.yml subPath: redis.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/profiles/clouddriver-local.yml subPath: clouddriver-local.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/profiles/echo-local.yml subPath: echo-local.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/profiles/front50-local.yml subPath: front50-local.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/profiles/gate-local.yml subPath: gate-local.yml - name: halconfig mountPath: /home/spinnaker/staging/.hal/default/profiles/igor-local.yml subPath: igor-local.yml volumes: - name: halconfig configMap: name: halconfig - name: persistentconfig persistentVolumeClaim: claimName: halyard-pv-claim --- apiVersion: v1 kind: Service metadata: name: spin-halyard namespace: halyard spec: ports: - port: 8064 targetPort: 8064 protocol: TCP selector: app: spin stack: halyard --- apiVersion: v1 kind: ConfigMap metadata: name: halconfig namespace: halyard data: deck.yml: | host: 0.0.0.0 env: API_HOST: http://spin-gate.spinnaker:8084 fiat.yml: | enabled: false skipLifeCycleManagement: true gate.yml: | host: 0.0.0.0 clouddriver-local.yml: | kubernetes.v2.managedBySuffix: for-gcp echo-local.yml: | rest: enabled: true endpoints: - wrap: true flatten: false url: https://$REGION-$PROJECT_ID.cloudfunctions.net/$CLOUD_FUNCTION_NAME username: $AUDIT_LOG_UNAME password: $AUDIT_LOG_PW eventName: spinnaker_events front50-local.yml: | spinnaker.s3.versioning: false gate-local.yml: | redis.configuration.secure: true igor-local.yml: | locking: enabled: true redis.yml: | overrideBaseUrl: redis://$REDIS_INSTANCE_HOST:6379 skipLifeCycleManagement: true config: | currentDeployment: default deploymentConfigurations: - name: default version: $SPINNAKER_VERSION providers: appengine: enabled: false accounts: [] aws: enabled: false accounts: [] bakeryDefaults: baseImages: [] defaultKeyPairTemplate: '{{name}}-keypair' defaultRegions: - name: us-west-2 defaults: iamRole: BaseIAMRole ecs: enabled: false accounts: [] azure: enabled: false accounts: [] bakeryDefaults: templateFile: azure-linux.json baseImages: [] dcos: enabled: false accounts: [] clusters: [] dockerRegistry: enabled: false accounts: [] google: enabled: false accounts: [] bakeryDefaults: templateFile: gce.json baseImages: [] zone: us-central1-f network: default useInternalIp: false kubernetes: enabled: true accounts: - name: spinnaker-install-account requiredGroupMembership: [] providerVersion: V2 permissions: {} dockerRegistries: [] configureImagePullSecrets: true serviceAccount: true cacheThreads: 1 namespaces: [] omitNamespaces: - halyard - kube-public - kube-system - spinnaker kinds: [] omitKinds: [] customResources: [] cachingPolicies: [] oAuthScopes: [] primaryAccount: spinnaker-install-account oracle: enabled: false accounts: [] bakeryDefaults: templateFile: oci.json baseImages: [] cloudfoundry: enabled: false accounts: [] deploymentEnvironment: size: SMALL type: Distributed accountName: spinnaker-install-account imageVariant: SLIM updateVersions: true consul: enabled: false vault: enabled: false customSizing: {} sidecars: {} initContainers: {} hostAliases: {} affinity: {} tolerations: {} nodeSelectors: {} gitConfig: upstreamUser: spinnaker livenessProbeConfig: enabled: false haServices: clouddriver: enabled: false disableClouddriverRoDeck: false echo: enabled: false persistentStorage: persistentStoreType: gcs azs: {} gcs: project: $PROJECT_ID bucket: $BUCKET_NAME rootFolder: front50 redis: {} s3: rootFolder: front50 oracle: {} features: auth: false fiat: false chaos: false entityTags: false artifacts: true metricStores: datadog: enabled: false tags: [] prometheus: enabled: false add_source_metalabels: true stackdriver: enabled: false newrelic: enabled: false tags: [] period: 30 enabled: false notifications: slack: enabled: false twilio: enabled: false baseUrl: https://api.twilio.com/ github-status: enabled: false timezone: $TIMEZONE ci: jenkins: enabled: false masters: [] travis: enabled: false masters: [] wercker: enabled: false masters: [] concourse: enabled: false masters: [] gcb: enabled: true accounts: - name: gcb-account permissions: {} project: $PROJECT_ID subscriptionName: $GCB_PUBSUB_SUBSCRIPTION repository: artifactory: enabled: false searches: [] security: apiSecurity: ssl: enabled: false overrideBaseUrl: /gate uiSecurity: ssl: enabled: false authn: oauth2: enabled: false client: {} resource: {} userInfoMapping: {} saml: enabled: false userAttributeMapping: {} ldap: enabled: false x509: enabled: false iap: enabled: false enabled: false authz: groupMembership: service: EXTERNAL google: roleProviderType: GOOGLE github: roleProviderType: GITHUB file: roleProviderType: FILE ldap: roleProviderType: LDAP enabled: false artifacts: bitbucket: enabled: false accounts: [] gcs: enabled: true accounts: - name: gcs-install-account oracle: enabled: false accounts: [] github: enabled: false accounts: [] gitlab: enabled: false accounts: [] http: enabled: false accounts: [] helm: enabled: false accounts: [] s3: enabled: false accounts: [] maven: enabled: false accounts: [] templates: [] pubsub: enabled: true google: enabled: true pubsubType: GOOGLE subscriptions: - name: gcr-pub-sub project: $PROJECT_ID subscriptionName: $GCR_PUBSUB_SUBSCRIPTION ackDeadlineSeconds: 10 messageFormat: GCR publishers: - name: $PUBSUB_NOTIFICATION_PUBLISHER project: $PROJECT_ID topicName: $PUBSUB_NOTIFICATION_TOPIC content: NOTIFICATIONS canary: enabled: true serviceIntegrations: - name: google enabled: true accounts: - name: my-google-account project: $PROJECT_ID bucket: $BUCKET_NAME rootFolder: kayenta supportedTypes: - METRICS_STORE - CONFIGURATION_STORE - OBJECT_STORE gcsEnabled: true stackdriverEnabled: true - name: prometheus enabled: false accounts: [] - name: datadog enabled: false accounts: [] - name: signalfx enabled: false accounts: [] - name: aws enabled: false accounts: [] s3Enabled: false reduxLoggerEnabled: true defaultJudge: NetflixACAJudge-v1.0 stagesEnabled: true templatesEnabled: true showAllConfigsEnabled: true plugins: plugins: [] enabled: false downloadingEnabled: false pluginConfigurations: plugins: {} webhook: trust: enabled: false telemetry: enabled: false endpoint: https://stats.spinnaker.io instanceId: connectionTimeoutMillis: 3000 readTimeoutMillis: 5000 --- apiVersion: batch/v1 kind: Job metadata: name: hal-deploy-apply namespace: halyard labels: app: job stack: hal-deploy spec: template: metadata: labels: app: job stack: hal-deploy spec: restartPolicy: OnFailure containers: - name: hal-deploy-apply # todo use a custom image image: us-docker.pkg.dev/spinnaker-community/docker/halyard:$HALYARD_VERSION command: - /bin/sh args: - -c - "hal deploy apply --daemon-endpoint http://spin-halyard.halyard:8064" ================================================ FILE: scripts/install/setup.sh ================================================ #!/usr/bin/env bash err() { echo "$*" >&2; } [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh check_for_required_binaries PARENT_DIR=$PARENT_DIR $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_git_config.sh || exit 1 [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" source "$PROPERTIES_FILE" check_for_shared_vpc $CI PARENT_DIR=$PARENT_DIR PROPERTIES_FILE=$PROPERTIES_FILE $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh OPERATOR_SA_EMAIL=$(gcloud config list account --format "value(core.account)" --project $PROJECT_ID) SETUP_EXISTING_ROLES=$(gcloud projects get-iam-policy --filter bindings.members:$OPERATOR_SA_EMAIL $PROJECT_ID \ --flatten bindings[].members --format="value(bindings.role)") if [ -z "$SETUP_EXISTING_ROLES" ]; then bold "Unable to verify that the service account \"$OPERATOR_SA_EMAIL\" has the required IAM roles." bold "\"$OPERATOR_SA_EMAIL\" requires the IAM role \"Project IAM Admin\" to proceed." exit 1 fi if [ -z "$(echo $SETUP_EXISTING_ROLES | grep roles/owner)" ]; then SETUP_REQUIRED_ROLES=(cloudfunctions.developer compute.networkViewer container.admin iam.serviceAccountCreator iam.serviceAccountUser pubsub.editor redis.admin serviceusage.serviceUsageAdmin source.admin storage.admin) MISSING_ROLES="" for r in "${SETUP_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $SETUP_EXISTING_ROLES | grep $r)" ]; then if [ -z "$MISSING_ROLES" ]; then MISSING_ROLES="$r" else MISSING_ROLES="$MISSING_ROLES, $r" fi fi done if [ -n "$MISSING_ROLES" ]; then bold "The service account in use, \"$OPERATOR_SA_EMAIL\", is missing the following required role(s): $MISSING_ROLES." bold "Add the required role(s) and try re-running the script." exit 1 fi fi REQUIRED_APIS="cloudbuild.googleapis.com cloudfunctions.googleapis.com container.googleapis.com endpoints.googleapis.com iap.googleapis.com monitoring.googleapis.com redis.googleapis.com sourcerepo.googleapis.com" NUM_REQUIRED_APIS=$(wc -w <<< "$REQUIRED_APIS") NUM_ENABLED_APIS=$(gcloud services list --project $PROJECT_ID \ --filter="config.name:($REQUIRED_APIS)" \ --format="value(config.name)" | wc -l) if [ $NUM_ENABLED_APIS != $NUM_REQUIRED_APIS ]; then bold "Enabling required APIs ($REQUIRED_APIS) in $PROJECT_ID..." bold "This phase will take a few minutes (progress will not be reported during this operation)." bold bold "Once the required APIs are enabled, the remaining components will be installed and configured. The entire installation may take 10 minutes or more." gcloud services --project $PROJECT_ID enable $REQUIRED_APIS fi if [ "$PROJECT_ID" != "$NETWORK_PROJECT" ]; then # Cloud Memorystore for Redis requires the Redis instance to be deployed in the Shared VPC # host project: https://cloud.google.com/memorystore/docs/redis/networking#limited_and_unsupported_networks if [ ! $(has_service_enabled $NETWORK_PROJECT redis.googleapis.com) ]; then bold "Enabling redis.googleapis.com in $NETWORK_PROJECT..." gcloud services --project $NETWORK_PROJECT enable redis.googleapis.com fi fi source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/cluster_utils.sh CLUSTER_EXISTS=$(check_for_existing_cluster) if [ -n "$CLUSTER_EXISTS" ]; then check_existing_cluster_location bold "Retrieving credentials for GKE cluster $GKE_CLUSTER..." gcloud container clusters get-credentials $GKE_CLUSTER --zone $ZONE --project $PROJECT_ID bold "Checking for Spinnaker application in cluster $GKE_CLUSTER..." SPINNAKER_APPLICATION_LIST_JSON=$(kubectl get applications -n spinnaker -l app.kubernetes.io/name=spinnaker --output json) SPINNAKER_APPLICATION_COUNT=$(echo $SPINNAKER_APPLICATION_LIST_JSON | jq '.items | length') if [ -n "$SPINNAKER_APPLICATION_COUNT" ] && [ "$SPINNAKER_APPLICATION_COUNT" != "0" ]; then bold "The GKE cluster $GKE_CLUSTER already contains an installed Spinnaker application." if [ "$SPINNAKER_APPLICATION_COUNT" == "1" ]; then EXISTING_SPINNAKER_APPLICATION_NAME=$(echo $SPINNAKER_APPLICATION_LIST_JSON | jq -r '.items[0].metadata.name') if [ "$EXISTING_SPINNAKER_APPLICATION_NAME" == "$DEPLOYMENT_NAME" ]; then bold "Name of existing Spinnaker application matches name specified in properties file; carrying on with installation..." else bold "Please choose another cluster." exit 1 fi else # Should never be more than 1 deployment in a cluster, but protect against it just in case. bold "Please choose another cluster." exit 1 fi fi fi NETWORK_SUBNET_MODE=$(gcloud compute networks list --project $NETWORK_PROJECT \ --filter "name=$NETWORK" \ --format "value(x_gcloud_subnet_mode)") if [ -z "$NETWORK_SUBNET_MODE" ]; then bold "Network $NETWORK was not found in project $NETWORK_PROJECT." exit 1 elif [ "$NETWORK_SUBNET_MODE" = "LEGACY" ]; then bold "Network $NETWORK is a legacy network. This installation requires a" \ "non-legacy network. Please specify a non-legacy network in" \ "$PROPERTIES_FILE and re-run this script." exit 1 fi # Verify that the subnet exists in the network. SUBNET_CHECK=$(gcloud compute networks subnets list --project=$NETWORK_PROJECT \ --network=$NETWORK --filter "region: ($REGION) AND name: ($SUBNET)" \ --format "value(name)") if [ -z "$SUBNET_CHECK" ]; then bold "Subnet $SUBNET was not found in network $NETWORK" \ "in project $NETWORK_PROJECT. Please specify an existing subnet in" \ "$PROPERTIES_FILE and re-run this script. You can verify" \ "what subnetworks exist in this network by running:" bold " gcloud compute networks subnets list --project $NETWORK_PROJECT --network=$NETWORK --filter \"region: ($REGION)\"" exit 1 fi SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') if [ -z "$SA_EMAIL" ]; then bold "Creating service account $SERVICE_ACCOUNT_NAME..." gcloud iam service-accounts --project $PROJECT_ID create \ $SERVICE_ACCOUNT_NAME \ --display-name $SERVICE_ACCOUNT_NAME while [ -z "$SA_EMAIL" ]; do SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') sleep 5 done else bold "Using existing service account $SERVICE_ACCOUNT_NAME..." fi bold "Assigning required roles to $SERVICE_ACCOUNT_NAME..." K8S_REQUIRED_ROLES=(cloudbuild.builds.editor container.admin logging.logWriter monitoring.admin pubsub.admin storage.admin) EXISTING_ROLES=$(gcloud projects get-iam-policy --filter bindings.members:$SA_EMAIL $PROJECT_ID \ --flatten bindings[].members --format="value(bindings.role)") for r in "${K8S_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $EXISTING_ROLES | grep $r)" ]; then bold "Assigning role $r..." gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SA_EMAIL \ --role roles/$r \ --format=none fi done export REDIS_INSTANCE_HOST=$(gcloud redis instances list \ --project $NETWORK_PROJECT --region $REGION \ --filter="name=projects/$NETWORK_PROJECT/locations/$REGION/instances/$REDIS_INSTANCE" \ --format="value(host)") if [ -z "$REDIS_INSTANCE_HOST" ]; then bold "Creating redis instance $REDIS_INSTANCE in project $NETWORK_PROJECT..." gcloud redis instances create $REDIS_INSTANCE --project $NETWORK_PROJECT \ --region=$REGION --zone=$ZONE --network=$NETWORK_REFERENCE \ --redis-config=notify-keyspace-events=gxE export REDIS_INSTANCE_HOST=$(gcloud redis instances list \ --project $NETWORK_PROJECT --region $REGION \ --filter="name=projects/$NETWORK_PROJECT/locations/$REGION/instances/$REDIS_INSTANCE" \ --format="value(host)") else bold "Using existing redis instance $REDIS_INSTANCE ($REDIS_INSTANCE_HOST)..." fi # TODO: Could verify ACLs here. In the meantime, error messages should suffice. gsutil ls $BUCKET_URI if [ $? != 0 ]; then bold "Creating bucket $BUCKET_URI..." gsutil mb -p $PROJECT_ID -l $REGION $BUCKET_URI gsutil versioning set on $BUCKET_URI else bold "Using existing bucket $BUCKET_URI..." fi if [ -z "$CLUSTER_EXISTS" ]; then bold "Creating GKE cluster $GKE_CLUSTER..." # $GKE_RELEASE_CHANNEL is new as of 2021-08-13, so fall back to # $GKE_CLUSTER_VERSION if it doesn't exist. if [ -z "$GKE_RELEASE_CHANNEL" ]; then CLUSTER_VERSION_SPEC="--cluster-version $GKE_CLUSTER_VERSION" else CLUSTER_VERSION_SPEC="--release-channel $GKE_RELEASE_CHANNEL" fi # TODO: Move some of these config settings to properties file. # TODO: Should this be regional instead? eval gcloud beta container clusters create $GKE_CLUSTER --project $PROJECT_ID \ --zone $ZONE --network $NETWORK_REFERENCE --subnetwork $SUBNET_REFERENCE \ $CLUSTER_VERSION_SPEC --machine-type $GKE_MACHINE_TYPE \ --disk-type $GKE_DISK_TYPE --disk-size $GKE_DISK_SIZE --service-account $SA_EMAIL \ --num-nodes $GKE_NUM_NODES --enable-stackdriver-kubernetes --enable-autoupgrade \ --enable-autorepair --enable-ip-alias --addons HorizontalPodAutoscaling,HttpLoadBalancing \ "${CLUSTER_SECONDARY_RANGE_NAME:+'--cluster-secondary-range-name' $CLUSTER_SECONDARY_RANGE_NAME}" \ "${SERVICES_SECONDARY_RANGE_NAME:+'--services-secondary-range-name' $SERVICES_SECONDARY_RANGE_NAME}" # If the cluster already exists, we already retrieved credentials way up at the top of the script. bold "Retrieving credentials for GKE cluster $GKE_CLUSTER..." gcloud container clusters get-credentials $GKE_CLUSTER --zone $ZONE --project $PROJECT_ID else bold "Using existing GKE cluster $GKE_CLUSTER..." check_existing_cluster_prereqs fi GCR_PUBSUB_TOPIC_NAME=projects/$PROJECT_ID/topics/gcr EXISTING_GCR_PUBSUB_TOPIC_NAME=$(gcloud pubsub topics list --project $PROJECT_ID \ --filter="name=$GCR_PUBSUB_TOPIC_NAME" --format="value(name)") if [ -z "$EXISTING_GCR_PUBSUB_TOPIC_NAME" ]; then bold "Creating pubsub topic $GCR_PUBSUB_TOPIC_NAME for GCR..." gcloud pubsub topics create --project $PROJECT_ID $GCR_PUBSUB_TOPIC_NAME else bold "Using existing pubsub topic $EXISTING_GCR_PUBSUB_TOPIC_NAME for GCR..." fi EXISTING_GCR_PUBSUB_SUBSCRIPTION_NAME=$(gcloud pubsub subscriptions list \ --project $PROJECT_ID \ --filter="name=projects/$PROJECT_ID/subscriptions/$GCR_PUBSUB_SUBSCRIPTION" \ --format="value(name)") if [ -z "$EXISTING_GCR_PUBSUB_SUBSCRIPTION_NAME" ]; then bold "Creating pubsub subscription $GCR_PUBSUB_SUBSCRIPTION for GCR..." gcloud pubsub subscriptions create --project $PROJECT_ID $GCR_PUBSUB_SUBSCRIPTION \ --topic=gcr else bold "Using existing pubsub subscription $GCR_PUBSUB_SUBSCRIPTION for GCR..." fi GCB_PUBSUB_TOPIC_NAME=projects/$PROJECT_ID/topics/cloud-builds EXISTING_GCB_PUBSUB_TOPIC_NAME=$(gcloud pubsub topics list --project $PROJECT_ID \ --filter="name=$GCB_PUBSUB_TOPIC_NAME" --format="value(name)") if [ -z "$EXISTING_GCB_PUBSUB_TOPIC_NAME" ]; then bold "Creating pubsub topic $GCB_PUBSUB_TOPIC_NAME for GCB..." gcloud pubsub topics create --project $PROJECT_ID $GCB_PUBSUB_TOPIC_NAME else bold "Using existing pubsub topic $EXISTING_GCB_PUBSUB_TOPIC_NAME for GCB..." fi EXISTING_GCB_PUBSUB_SUBSCRIPTION_NAME=$(gcloud pubsub subscriptions list \ --project $PROJECT_ID \ --filter="name=projects/$PROJECT_ID/subscriptions/$GCB_PUBSUB_SUBSCRIPTION" \ --format="value(name)") if [ -z "$EXISTING_GCB_PUBSUB_SUBSCRIPTION_NAME" ]; then bold "Creating pubsub subscription $GCB_PUBSUB_SUBSCRIPTION for GCB..." gcloud pubsub subscriptions create --project $PROJECT_ID $GCB_PUBSUB_SUBSCRIPTION \ --topic=projects/$PROJECT_ID/topics/cloud-builds else bold "Using existing pubsub subscription $GCB_PUBSUB_SUBSCRIPTION for GCB..." fi NOTIFICATION_PUBSUB_TOPIC_NAME=projects/$PROJECT_ID/topics/$PUBSUB_NOTIFICATION_TOPIC EXISTING_NOTIFICATION_PUBSUB_TOPIC_NAME=$(gcloud pubsub topics list --project $PROJECT_ID \ --filter="name=$NOTIFICATION_PUBSUB_TOPIC_NAME" --format="value(name)") if [ -z "$EXISTING_NOTIFICATION_PUBSUB_TOPIC_NAME" ]; then bold "Creating pubsub topic $NOTIFICATION_PUBSUB_TOPIC_NAME for notifications..." gcloud pubsub topics create --project $PROJECT_ID $NOTIFICATION_PUBSUB_TOPIC_NAME else bold "Using existing pubsub topic $EXISTING_NOTIFICATION_PUBSUB_TOPIC_NAME for notifications..." fi EXISTING_HAL_DEPLOY_APPLY_JOB_NAME=$(kubectl get job -n halyard \ --field-selector metadata.name=="hal-deploy-apply" \ -o json | jq -r .items[0].metadata.name) if [ $EXISTING_HAL_DEPLOY_APPLY_JOB_NAME != 'null' ]; then bold "Deleting earlier job $EXISTING_HAL_DEPLOY_APPLY_JOB_NAME..." kubectl delete job hal-deploy-apply -n halyard fi bold "Provisioning Spinnaker resources..." envsubst < $PARENT_DIR/spinnaker-for-gcp/scripts/install/quick-install.yml | kubectl apply -f - job_ready() { printf "Waiting on job $1 to complete" while [[ "$(kubectl get job $1 -n halyard -o \ jsonpath="{.status.succeeded}")" != "1" ]]; do printf "." sleep 5 done echo "" } job_ready hal-deploy-apply # Sourced to import $IP_ADDR. # Used at the end of setup to check if installation is exposed via a secured endpoint. source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/update_landing_page.sh PARENT_DIR=$PARENT_DIR PROPERTIES_FILE=$PROPERTIES_FILE $PARENT_DIR/spinnaker-for-gcp/scripts/manage/deploy_application_manifest.sh # Delete any existing deployment config secret. # It will be recreated with up-to-date contents during push_config.sh. EXISTING_DEPLOYMENT_SECRET_NAME=$(kubectl get secret -n halyard \ --field-selector metadata.name=="spinnaker-deployment" \ -o json | jq .items[0].metadata.name) if [ $EXISTING_DEPLOYMENT_SECRET_NAME != 'null' ]; then bold "Deleting Kubernetes secret spinnaker-deployment..." kubectl delete secret spinnaker-deployment -n halyard fi EXISTING_CLOUD_FUNCTION=$(gcloud functions list --project $PROJECT_ID \ --format="value(name)" --filter="entryPoint=$CLOUD_FUNCTION_NAME") if [ -z "$EXISTING_CLOUD_FUNCTION" ]; then bold "Deploying audit log cloud function $CLOUD_FUNCTION_NAME..." cat $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/config_json.template | envsubst > $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/config.json cat $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/index_js.template | envsubst > $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/index.js gcloud functions deploy $CLOUD_FUNCTION_NAME --source $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog \ --trigger-http --memory 2048MB --runtime nodejs8 --allow-unauthenticated --project $PROJECT_ID --region $REGION gcloud alpha functions add-iam-policy-binding $CLOUD_FUNCTION_NAME --project $PROJECT_ID --region $REGION --member allUsers --role roles/cloudfunctions.invoker else bold "Using existing audit log cloud function $CLOUD_FUNCTION_NAME..." fi if [ "$USE_CLOUD_SHELL_HAL_CONFIG" = true ]; then # Not passing $CI since the guard makes it clear we are running from cloud shell. $PARENT_DIR/spinnaker-for-gcp/scripts/manage/push_and_apply.sh else # We want the local hal config to match what was deployed. CI=$CI PARENT_DIR=$PARENT_DIR PROPERTIES_FILE=$PROPERTIES_FILE $PARENT_DIR/spinnaker-for-gcp/scripts/manage/pull_config.sh # We want a full backup stored in the bucket and the full deployment config stored in a secret. CI=$CI PARENT_DIR=$PARENT_DIR PROPERTIES_FILE=$PROPERTIES_FILE $PARENT_DIR/spinnaker-for-gcp/scripts/manage/push_config.sh fi deploy_ready() { printf "Waiting on $2 to come online" while [[ "$(kubectl get deploy $1 -n spinnaker -o \ jsonpath="{.status.readyReplicas}")" != \ "$(kubectl get deploy $1 -n spinnaker -o \ jsonpath="{.status.replicas}")" ]]; do printf "." sleep 5 done echo "" } deploy_ready spin-gate "API server" deploy_ready spin-front50 "storage server" deploy_ready spin-orca "orchestration engine" deploy_ready spin-kayenta "canary analysis engine" deploy_ready spin-deck "UI server" if [ "$CI" != true ]; then $PARENT_DIR/spinnaker-for-gcp/scripts/cli/install_hal.sh --version $HALYARD_VERSION $PARENT_DIR/spinnaker-for-gcp/scripts/cli/install_spin.sh # We want a backup containing the newly-created ~/.spin/* files as well. # Not passing $CI since the guard already ensures it is not true. $PARENT_DIR/spinnaker-for-gcp/scripts/manage/push_config.sh fi # If restoring a secured endpoint, leave the user on the documentation for iap configuration. if [ "$USE_CLOUD_SHELL_HAL_CONFIG" = true -a -n "$IP_ADDR" -a "$CI" != true ]; then $PARENT_DIR/spinnaker-for-gcp/scripts/expose/launch_configure_iap.sh fi echo bold "Installation complete." echo bold "Sign up for Spinnaker for GCP updates and announcements:" bold " https://groups.google.com/forum/#!forum/spinnaker-for-gcp-announce" echo ================================================ FILE: scripts/install/setup_properties.sh ================================================ #!/usr/bin/env bash # PROJECT_ID should be set, but we will try to determine via gcloud config if not set. # DEPLOYMENT_NAME, GKE_CLUSTER and ZONE are optional. # If GKE_CLUSTER is set, ZONE is required. (This indicates that we should install in an existing cluster.) # If using a pre-existing cluster, that cluster must have: # - IP aliases enabled (since we are using a hosted Redis instance) # - Full Cloud Platform scope for its nodes (if using the default service account) # ZONE can be set and GKE_CLUSTER left unset. (This indicates we should create a new cluster in $ZONE.) bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_duplicate_dirs.sh || exit 1 if [ -z "$PROJECT_ID" ]; then PROJECT_ID=$(gcloud info --format='value(config.project)') fi if [ -z "$PROJECT_ID" ]; then echo "PROJECT_ID must be specified." exit 1 fi PROPERTIES_FILE="$HOME/cloudshell_open/spinnaker-for-gcp/scripts/install/properties" if [ -f "$PROPERTIES_FILE" ]; then bold "The properties file already exists at $PROPERTIES_FILE. Please move it out of the way if you want to generate a new properties file." exit 1 fi if [ "$GKE_CLUSTER" ]; then if [ -z "$ZONE" ]; then echo "If GKE_CLUSTER is specified, ZONE must also be specified." exit 1 fi # Since cluster already exists, must resolve service account from the cluster. EXISTING_SA_EMAIL=$(gcloud beta container clusters describe --project $PROJECT_ID \ --zone $ZONE $GKE_CLUSTER --format="value(nodeConfig.serviceAccount)") if [ -z $EXISTING_SA_EMAIL ]; then echo "Unable to resolve service account from existing cluster $GKE_CLUSTER in zone $ZONE." exit 1 fi if [ "$EXISTING_SA_EMAIL" == "default" ]; then SERVICE_ACCOUNT_NAME="Compute Engine default service account" else SERVICE_ACCOUNT_NAME=$(echo $EXISTING_SA_EMAIL | cut -d @ -f 1) fi fi NETWORK="default" SUBNET="default" ZONE=${ZONE:-us-east1-c} REGION=$(echo $ZONE | cut -d - -f 1,2) source ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/service_utils.sh query_redis_instance_names() { if [ $(has_service_enabled $1 redis.googleapis.com) ]; then # TODO: Should really query redis instances across _all_ regions to ensure no deployment naming collision. # TODO: Alternatively, could incorporate region in generated deployment name. EXISTING_REDIS_NAMES=$(gcloud redis instances list --region $REGION --project $1 \ --filter="name:spinnaker-" \ --format="value(name)") echo "$EXISTING_REDIS_NAMES" fi } EXISTING_REDIS_NAMES=$(query_redis_instance_names $PROJECT_ID) # Also avoid name collisions with potential Shared VPC host project. if [ $(has_service_enabled $PROJECT_ID compute.googleapis.com) ]; then SHARED_VPC_HOST_PROJECT=$(gcloud compute shared-vpc get-host-project $PROJECT_ID --format="value(name)") fi if [ "$SHARED_VPC_HOST_PROJECT" ]; then SHARED_VPC_HOST_PROJECT_REDIS_NAMES=$(query_redis_instance_names $SHARED_VPC_HOST_PROJECT) EXISTING_REDIS_NAMES="$EXISTING_REDIS_NAMES"$'\n'"$SHARED_VPC_HOST_PROJECT_REDIS_NAMES" fi EXISTING_DEPLOYMENT_COUNT=$(echo "$EXISTING_REDIS_NAMES" | sed '/^$/d' | wc -l) NEW_DEPLOYMENT_SUFFIX=$(($EXISTING_DEPLOYMENT_COUNT + 1)) NEW_DEPLOYMENT_NAME="spinnaker-$NEW_DEPLOYMENT_SUFFIX" while [[ "$(echo "$EXISTING_REDIS_NAMES" | grep ^$NEW_DEPLOYMENT_NAME$ | wc -l)" != "0" ]]; do NEW_DEPLOYMENT_NAME="spinnaker-$((++NEW_DEPLOYMENT_SUFFIX))" done cat > ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties <> ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties <> ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties <> ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties <> ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties < 0) ? execution.stages.find(stage => stage.status === 'RUNNING') : {}; var user = execution && execution.authentication && execution.authentication.user ? execution.authentication.user : 'n/a'; if (execution && execution.trigger) { if (execution.trigger.runAsUser) { user = execution.trigger.runAsUser; } else if (execution.trigger.user) { user = execution.trigger.user; } } var creationTimestamp = moment.tz(Number(req.body.payload.details.created), config.TIMEZONE).format('ddd, DD MMM YYYY HH:mm:ss z'); var reasonSegment; if (eventSource === 'igor') { if (eventType === 'build') { var lastBuild = content.project.lastBuild; var jenkinsTimestamp = moment.tz(Number(lastBuild.timestamp), config.TIMEZONE).format('ddd, DD MMM YYYY HH:mm:ss z'); if (lastBuild.result === 'SUCCESS') { log('Jenkins project ' + content.project.name + ' successfully completed build #' + lastBuild.number + ' at ' + jenkinsTimestamp + '.', null, null); } else { log('Jenkins project ' + content.project.name + ' completed build #' + lastBuild.number + ' with status ' + lastBuild.result + ' at ' + jenkinsTimestamp + '.', null, null, 'ERROR'); } } else if (eventType === 'docker') { log('Docker tag ' + content.tag + ' was pushed to repository ' + content.repository + ' in registry ' + content.registry + ' at ' + creationTimestamp + '.', null, null); } } else if (eventType === 'git') { log('Received webhook for project ' + content.slug + ' in org ' + content.repoProject + ' from ' + eventSource + ' at commit ' + content.hash + ' on branch ' + content.branch + ' at ' + creationTimestamp + '.', null, null); } else if (eventType === 'orca:stage:starting' && !stageDetails.syntheticStageOwner) { if (!content.standalone) { log('User ' + user + ' executed operation ' + stageDetails.name + ' (of type ' + stageDetails.type + ') via pipeline ' + execution.name + ' of application ' + execution.application + ' at ' + creationTimestamp + '.', execution.application, execution.name); } else if (stageDetails.type === 'savePipeline') { log('User ' + user + ' executed operation (' + execution.description + ') at ' + creationTimestamp + '.', null, null); } else { reasonSegment = context.reason ? ' for reason "' + context.reason + '"' : ''; log('User ' + user + ' executed ad-hoc operation ' + execution.stages[0].type + ' (' + execution.description + ')' + reasonSegment + ' at ' + creationTimestamp + '.', null, null); } } else if (eventType === 'orca:pipeline:starting') { var parametersSegment = execution.trigger.parameters ? ' (with parameters ' + JSON.stringify(execution.trigger.parameters) + ')' : ''; log('User ' + user + ' executed pipeline ' + execution.name + ' of application ' + execution.application + ' via ' + execution.trigger.type + ' trigger' + parametersSegment + ' at ' + creationTimestamp + '.', execution.application, execution.name); } else if (eventType === 'orca:pipeline:failed' && execution.canceled) { var cancellationUser = execution.canceledBy ? execution.canceledBy : null; if (cancellationUser) { reasonSegment = execution.cancellationReason ? ' for reason "' + execution.cancellationReason + '"' : ''; log('User ' + cancellationUser + ' canceled pipeline ' + execution.name + ' of application ' + execution.application + reasonSegment + ' at ' + creationTimestamp + '.', execution.application, execution.name, 'WARNING'); } else { log('Pipeline ' + execution.name + ' of application ' + execution.application + ' failed at ' + creationTimestamp + '.', execution.application, execution.name, 'ERROR'); } } else if (eventType === 'orca:pipeline:complete') { log('Pipeline ' + execution.name + ' of application ' + execution.application + ' completed at ' + creationTimestamp + '.', execution.application, execution.name); } else if (!content.standalone && context && stageDetails && stageDetails.type === 'manualJudgment' && eventType === 'orca:task:failed') { var judgmentInputSegment = context.judgmentInput ? ' (judgment "' + context.judgmentInput + '" was selected)' : ''; log('User ' + context.lastModifiedBy + ' judged stage ' + stageDetails.name + ' of pipeline ' + execution.name + ' of application ' + execution.application + ' to stop' + judgmentInputSegment + ' at ' + creationTimestamp + '.', execution.application, execution.name, 'WARNING'); } else if (!content.standalone && context && stageDetails && stageDetails.type === 'manualJudgment' && eventType === 'orca:task:complete') { var judgmentInputSegment = context.judgmentInput ? ' (judgment "' + context.judgmentInput + '" was selected)' : ''; log('User ' + context.lastModifiedBy + ' judged stage ' + stageDetails.name + ' of pipeline ' + execution.name + ' of application ' + execution.application + ' to continue' + judgmentInputSegment + ' at ' + creationTimestamp + '.'); } else if (eventType === 'orca:task:failed') { var failureReasonSegment = context.exception && context.exception.details && context.exception.details.errors && context.exception.details.errors[0] ? ' due to ' + JSON.stringify(context.exception.details.errors) : ''; if (!content.standalone) { log('Operation ' + stageDetails.name + ' (of type ' + stageDetails.type + ') of pipeline ' + execution.name + ' of application ' + execution.application + ' failed' + failureReasonSegment + ' at ' + creationTimestamp + '.', execution.application, execution.name, 'ERROR'); } else { log('Ad-hoc operation ' + stageDetails.type + ' failed' + failureReasonSegment + ' at ' + creationTimestamp + '.', null, null, 'ERROR'); } } res.status(200).send('Success: ' + req.body.eventName); } } catch (err) { log(err, 'ERROR'); res.status(err.code || 500).send(err); } }; /** * Verify that the webhook request came from spinnaker/echo. * * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg==" */ function verifyWebhook (authorization) { const basicAuth = new Buffer(authorization.replace('Basic ', ''), 'base64').toString(); const parts = basicAuth.split(':'); if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) { const error = new Error('Invalid credentials'); error.code = 401; throw error; } } /** * Writes message to StackDriver with specified severity. * * @param {string} message - The message to log to StackDriver logging. * @param {('ALERT', 'CRITICAL', 'DEBUG', 'EMERGENCY', 'ERROR', 'INFO', 'NOTICE', 'WARNING', 'WRITE')} severity - The * severity of the logged message. Defaults to 'INFO'. */ function log(message, application, pipeline, severity = 'INFO') { var log = logging.log(config.AUDIT_LOG_NAME); var metadata = {resource: {type: 'cloud_function'}, severity: severity}; var jsonPayload = {message: message}; if (application) { jsonPayload.application = application; } if (pipeline) { jsonPayload.pipeline = pipeline; } var entry = log.entry(metadata, jsonPayload); log.write(entry); } ================================================ FILE: scripts/install/spinnakerAuditLog/package.json ================================================ { "dependencies": { "@google-cloud/logging": "4.1.1", "moment-timezone": "^0.5.11" } } ================================================ FILE: scripts/manage/add_gae_account.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties read -e -p "Please enter the id of the project within which you wish to manage GAE resources: " -i $PROJECT_ID MANAGED_PROJECT_ID read -e -p "Please enter a name for the new Spinnaker account: " -i "$MANAGED_PROJECT_ID-acct" GAE_ACCOUNT_NAME bold "Assigning required roles to $SERVICE_ACCOUNT_NAME..." SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') GAE_REQUIRED_ROLES=(storage.admin appengine.appAdmin cloudscheduler.admin cloudbuild.serviceAgent cloudtasks.queueAdmin) EXISTING_ROLES=$(gcloud projects get-iam-policy --filter bindings.members:$SA_EMAIL $MANAGED_PROJECT_ID \ --flatten bindings[].members --format="value(bindings.role)") if [ "$?" != "0" ]; then bold "$USER does not have permission to query IAM policy on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi for r in "${GAE_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $EXISTING_ROLES | grep $r)" ]; then bold "Assigning role $r in project $MANAGED_PROJECT_ID to service account $SA_EMAIL..." gcloud projects add-iam-policy-binding $MANAGED_PROJECT_ID \ --member serviceAccount:$SA_EMAIL \ --role roles/$r \ --format=none if [ "$?" != "0" ]; then bold "$USER does not have permission to assign role $r on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi fi done ~/hal/hal config provider appengine enable ~/hal/hal config provider appengine account add $GAE_ACCOUNT_NAME --project $MANAGED_PROJECT_ID bold "Remember that your configuration changes have only been made locally." bold "They must be pushed and applied to your deployment to take effect:" bold " ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh" ================================================ FILE: scripts/manage/add_gce_account.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties read -e -p "Please enter the id of the project within which you wish to manage GCE resources: " -i $PROJECT_ID MANAGED_PROJECT_ID read -e -p "Please enter a name for the new Spinnaker account: " -i "$MANAGED_PROJECT_ID-acct" GCE_ACCOUNT_NAME bold "Assigning required roles to $SERVICE_ACCOUNT_NAME..." SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') GCE_REQUIRED_ROLES=(compute.instanceAdmin compute.networkAdmin compute.securityAdmin compute.storageAdmin iam.serviceAccountUser) EXISTING_ROLES=$(gcloud projects get-iam-policy --filter bindings.members:$SA_EMAIL $MANAGED_PROJECT_ID \ --flatten bindings[].members --format="value(bindings.role)") if [ "$?" != "0" ]; then bold "$USER does not have permission to query IAM policy on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi for r in "${GCE_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $EXISTING_ROLES | grep $r)" ]; then bold "Assigning role $r in project $MANAGED_PROJECT_ID to service account $SA_EMAIL..." gcloud projects add-iam-policy-binding $MANAGED_PROJECT_ID \ --member serviceAccount:$SA_EMAIL \ --role roles/$r \ --format=none if [ "$?" != "0" ]; then bold "$USER does not have permission to assign role $r on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi fi done ~/hal/hal config provider google account add $GCE_ACCOUNT_NAME --project $MANAGED_PROJECT_ID ~/hal/hal config provider google enable bold "Remember that your configuration changes have only been made locally." bold "They must be pushed and applied to your deployment to take effect:" bold " ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh" ================================================ FILE: scripts/manage/add_gke_account.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties CURRENT_K8S_CONTEXT=$(kubectl config current-context) AVAILABLE_K8S_CONTEXTS=$(kubectl config get-contexts -o name) echo "Available contexts:" echo "$AVAILABLE_K8S_CONTEXTS" echo if [ -z $CURRENT_K8S_CONTEXT ]; then read -e -p "Please enter the context you wish to use to manage your GKE resources: " TARGET_K8S_CONTEXT else read -e -p "Please enter the context you wish to use to manage your GKE resources: " -i $CURRENT_K8S_CONTEXT TARGET_K8S_CONTEXT fi FOUND_CONTEXT=$(echo "$AVAILABLE_K8S_CONTEXTS" | grep "^$TARGET_K8S_CONTEXT$") if [ -z $FOUND_CONTEXT ]; then bold "$TARGET_K8S_CONTEXT not found in available contexts..." exit 1 fi MANAGED_PROJECT_ID=$(echo $TARGET_K8S_CONTEXT | cut -d _ -f 2) read -e -p "Please enter the id of the project within which the referenced cluster lives: " -i $MANAGED_PROJECT_ID MANAGED_PROJECT_ID read -e -p "Please enter a name for the new Spinnaker account: " -i "$(echo $TARGET_K8S_CONTEXT | cut -d _ -f 4)-acct" GKE_ACCOUNT_NAME bold "Assigning required roles to $SERVICE_ACCOUNT_NAME..." SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') GKE_REQUIRED_ROLES=(container.admin) EXISTING_ROLES=$(gcloud projects get-iam-policy --filter bindings.members:$SA_EMAIL $MANAGED_PROJECT_ID \ --flatten bindings[].members --format="value(bindings.role)") if [ "$?" != "0" ]; then bold "$USER does not have permission to query IAM policy on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi for r in "${GKE_REQUIRED_ROLES[@]}"; do if [ -z "$(echo $EXISTING_ROLES | grep $r)" ]; then bold "Assigning role $r in project $MANAGED_PROJECT_ID to service account $SA_EMAIL..." gcloud projects add-iam-policy-binding $MANAGED_PROJECT_ID \ --member serviceAccount:$SA_EMAIL \ --role roles/$r \ --format=none if [ "$?" != "0" ]; then bold "$USER does not have permission to assign role $r on project $MANAGED_PROJECT_ID." \ "Please grant the necessary permissions and re-run this command." exit 1 fi fi done mkdir -p ~/.hal/default/credentials KUBECONFIG_FILENAME="kubeconfig-$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 9 | head -n 1)" bold "Copying ~/.kube/config into ~/.hal/default/credentials/$KUBECONFIG_FILENAME so it can be pushed to your halyard daemon's pod..." cp ~/.kube/config ~/.hal/default/credentials/$KUBECONFIG_FILENAME ~/hal/hal config provider kubernetes account add $GKE_ACCOUNT_NAME \ --provider-version v2 \ --context $TARGET_K8S_CONTEXT \ --kubeconfig-file ~/.hal/default/credentials/$KUBECONFIG_FILENAME bold "Remember that your configuration changes have only been made locally." bold "They must be pushed and applied to your deployment to take effect:" bold " ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh" ================================================ FILE: scripts/manage/add_missing_properties.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) PROPERTIES_FILE=$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties add_property_if_missing() { if [ -z "$(grep "export $1=" $PROPERTIES_FILE)" ]; then bold "Adding declaration of $1 to $PROPERTIES_FILE..." echo >> $PROPERTIES_FILE echo "$2" >> $PROPERTIES_FILE fi } read -r -d '' CSR_PROPERTY_DECLARATION <&2 CLUSTER_EXISTS=$(gcloud container clusters list --project $PROJECT_ID \ --filter="name=$GKE_CLUSTER" \ --format="value(name)") echo $CLUSTER_EXISTS } check_existing_cluster_location() { bold "Verifying location of existing cluster $GKE_CLUSTER..." # Query for cluster in specified zone, just in case there are multiple clusters with the same name. CLUSTER_EXISTS_IN_SPECIFIED_ZONE=$(gcloud container clusters list --project $PROJECT_ID \ --zone=$ZONE \ --filter="name=$GKE_CLUSTER" \ --format="value(location)") # If it's not in the specified zone, figure out where exactly it is. if [ -z "$CLUSTER_EXISTS_IN_SPECIFIED_ZONE" ]; then EXISTING_CLUSTER_LOCATION=$(gcloud container clusters list --project $PROJECT_ID \ --filter="name=$GKE_CLUSTER" \ --format="value(location)") LOCATION_IS_REGION=$(gcloud compute regions list --project $PROJECT_ID \ --filter="name=$EXISTING_CLUSTER_LOCATION" \ --format="value(name)") if [ -n "$LOCATION_IS_REGION" ]; then bold "Your pre-existing cluster $GKE_CLUSTER is regional; we do not support regional clusters." exit 1 fi fi } check_existing_cluster_prereqs() { EXISTING_CLUSTER_DESCRIPTION=$(gcloud container clusters describe $GKE_CLUSTER --zone $ZONE --format json) IP_ALIASES_ENABLED=$(echo $EXISTING_CLUSTER_DESCRIPTION | jq .ipAllocationPolicy.useIpAliases) if [ "$IP_ALIASES_ENABLED" != "true" ]; then bold "Your pre-existing cluster must have IP Aliases enabled." exit 1 fi NODE_CONFIG_SERVICE_ACCOUNT=$(echo $EXISTING_CLUSTER_DESCRIPTION | jq -r .nodeConfig.serviceAccount) # If using the "Compute Engine default service account", Full Cloud Platform scope is required for its nodes. if [ "$NODE_CONFIG_SERVICE_ACCOUNT" == "default" ]; then NODES_HAVE_CLOUD_PLATFORM_SCOPE=$(echo $EXISTING_CLUSTER_DESCRIPTION | \ jq '[.nodeConfig.oauthScopes[] == "https://www.googleapis.com/auth/cloud-platform"] | any') if [ "$NODES_HAVE_CLOUD_PLATFORM_SCOPE" != "true" ]; then bold "Your pre-existing cluster is using the \"Compute Engine default service account\". As such," \ "your nodes must have Full Cloud Platform scope." bold "In general, we recommend using an IAM-backed service account instead. An IAM-backed service" \ "account will be assigned the required roles during the Spinnaker for GCP setup process." exit 1 fi fi } ================================================ FILE: scripts/manage/connect_to_redis.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh bold "Resolving redis host..." export REDIS_INSTANCE_HOST=$(gcloud redis instances list \ --project $PROJECT_ID --region $REGION \ --filter="name=projects/$PROJECT_ID/locations/$REGION/instances/$REDIS_INSTANCE" \ --format="value(host)") bold "Locating redis-cli deployment..." REDIS_CLI_DEPLOYMENT=$(kubectl get deployments -n spinnaker --field-selector metadata.name=redisbox \ --output name) if [ -z $REDIS_CLI_DEPLOYMENT ]; then bold "Deploying redis-cli..." kubectl run redisbox --image=gcr.io/google_containers/redis:v1 -n spinnaker fi bold "Waiting for redis-cli deployment to become available..." kubectl wait --for condition=available deployment redisbox -n spinnaker bold "Locating redis-cli pod..." REDIS_CLI_POD=$(kubectl get pods -n spinnaker -l run=redisbox \ -o=jsonpath='{.items[0].metadata.name}') bold "Connecting to redis-cli pod and specifying redis host $REDIS_INSTANCE_HOST..." kubectl exec -it $REDIS_CLI_POD -n spinnaker -- redis-cli -h $REDIS_INSTANCE_HOST bold "Deleting redis-cli deployment..." kubectl delete deployment redisbox -n spinnaker ================================================ FILE: scripts/manage/connect_unsecured.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh bold "Locating Deck pod..." DECK_POD=$(kubectl -n spinnaker get pods -l cluster=spin-deck,app=spin \ -o=jsonpath='{.items[0].metadata.name}') bold "Forwarding localhost port 8080 to 9000 on $DECK_POD..." pkill -f 'kubectl -n spinnaker port-forward' kubectl -n spinnaker port-forward $DECK_POD 8080:9000 > /dev/null 2>&1 & # Query for static ip address as a signal that the Spinnaker installation is exposed via a secured endpoint. export IP_ADDR=$(gcloud compute addresses list --filter="name=$STATIC_IP_NAME" \ --format="value(address)" --global --project $PROJECT_ID) if [ "$IP_ADDR" ]; then bold "Are you sure you aren't intending to connect via the domain name instead? Asking since you have a static ip configured..." fi ================================================ FILE: scripts/manage/deploy_application_manifest.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" if [ ! -f "$PROPERTIES_FILE" ]; then bold "No properties file was found. Not updating GKE Application details view." git checkout -- $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_expanded.md exit 0 fi source "$PROPERTIES_FILE" # Query for static ip address as a signal that the Spinnaker installation is exposed via a secured endpoint. export IP_ADDR=$(gcloud compute addresses list --filter="name=$STATIC_IP_NAME" \ --format="value(address)" --global --project $PROJECT_ID) if [ -z "$IP_ADDR" ]; then APP_MANIFEST_MIDDLE=spinnaker_application_manifest_middle_unsecured.yaml else APP_MANIFEST_MIDDLE=spinnaker_application_manifest_middle_secured.yaml fi kubectl apply -f "https://raw.githubusercontent.com/GoogleCloudPlatform/marketplace-k8s-app-tools/master/crd/app-crd.yaml" cat $PARENT_DIR/spinnaker-for-gcp/templates/spinnaker_application_manifest_top.yaml \ $PARENT_DIR/spinnaker-for-gcp/templates/$APP_MANIFEST_MIDDLE \ $PARENT_DIR/spinnaker-for-gcp/templates/spinnaker_application_manifest_bottom.yaml \ | envsubst | kubectl apply -f - bold "Labeling resources as components of application $DEPLOYMENT_NAME..." kubectl label service --overwrite -n spinnaker spin-clouddriver app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-deck app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-echo app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-front50 app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-gate app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-igor app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-kayenta app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-orca app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label service --overwrite -n spinnaker spin-rosco app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-clouddriver app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-deck app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-echo app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-front50 app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-gate app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-igor app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-kayenta app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-orca app.kubernetes.io/name=$DEPLOYMENT_NAME -o name kubectl label deployment --overwrite -n spinnaker spin-rosco app.kubernetes.io/name=$DEPLOYMENT_NAME -o name ================================================ FILE: scripts/manage/generate_deletion_script.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties bold "Generating deletion script for $DEPLOYMENT_NAME in cluster $GKE_CLUSTER of project $PROJECT_ID..." ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_project_mismatch.sh DELETION_SCRIPT_FILENAME="$HOME/cloudshell_open/spinnaker-for-gcp/scripts/manage/delete-all_${PROJECT_ID}_${GKE_CLUSTER}_${DEPLOYMENT_NAME}.sh" SA_EMAIL=$(gcloud iam service-accounts --project $PROJECT_ID list \ --filter="displayName:$SERVICE_ACCOUNT_NAME" \ --format='value(email)') cat > $DELETION_SCRIPT_FILENAME <> $DELETION_SCRIPT_FILENAME <> $DELETION_SCRIPT_FILENAME <> $DELETION_SCRIPT_FILENAME <
## Manage Spinnaker via Halyard from Cloud Shell This management environment lets you run [Halyard commands](https://www.spinnaker.io/reference/halyard/) to configure and manage your Spinnaker installation. ### Ensure you are connected to the correct Kubernetes context ```bash PROJECT_ID={{project-id}} ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_cluster_config.sh ``` ### Pull Spinnaker config Paste and run this command to pull the configuration from your Spinnaker deployment into your Cloud Shell. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/pull_config.sh ``` ### Update this console **This is required if you've just pulled config from a different Spinnaker deployment.** This command refreshes the contents of the right-hand pane, including details on how to connect to Spinnaker. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_console.sh ``` ### Configure Spinnaker via Halyard All [halyard](https://www.spinnaker.io/reference/halyard/commands/) commands are available. ```bash hal config ``` As with provisioning Spinnaker, don't use `hal deploy connect` when managing Spinnaker. Also, don't use `hal deploy apply`. Instead, use the `push_and_apply.sh` command shown below. ### Notes on Halyard commands that reference local files If you add a Kubernetes account that references a kubeconfig file, that file must live within the '`~/.hal/default/credentials`' directory on your Cloud Shell VM. The kubeconfig is specified using the `--kubeconfig-file` argument to the `hal config provider kubernetes account add` and ...`edit` commands. A similar requirement applies for any other local file referenced from your halyard config, including Google JSON key files specified via the `--json-path` argument to various commands. These files must live within '`~/.hal/default/credentials`' or '`~/.hal/default/profiles`'. ### Push and apply updated config to Spinnaker deployment If you change any of the configuration, paste and run this command to push and apply those changes to your Spinnaker deployment. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh ``` ## Included command-line tools ### Halyard CLI The [Halyard CLI](https://www.spinnaker.io/reference/halyard/) (`hal`) and daemon are installed in your Cloud Shell. If you want to use a specific version of Halyard, use: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/cli/install_hal.sh --version $HALYARD_VERSION ``` If you want to upgrade to the latest version of Halyard, use: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/cli/update_hal.sh ``` ### Spinnaker CLI The [Spinnaker CLI](https://www.spinnaker.io/guides/spin/app/) (`spin`) is installed in your Cloud Shell. If you want to upgrade to the latest version, use: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/cli/install_spin.sh ``` ## Scripts for Common Commands Remember that any configuration changes you make locally (e.g. adding accounts) must be pushed and applied to your deployment to take effect: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh ``` ### Add Spinnaker account for GKE This script grants the required [IAM roles](https://cloud.google.com/kubernetes-engine/docs/how-to/iam) to the Spinnaker instance's service account, in the GCP project containing the referenced cluster. Before you run this command, make sure you've configured the context you intend to use to manage your GKE resources. The public Spinnaker documentation contains details on [configuring GKE clusters](https://www.spinnaker.io/setup/install/providers/kubernetes-v2/gke/). ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/add_gke_account.sh ``` ### Add Spinnaker account for GCE This script grants the required [IAM roles](https://cloud.google.com/compute/docs/access/) to the Spinnaker instance's service account, in the GCP project within which you wish to manage GCE resources. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/add_gce_account.sh ``` ### Add Spinnaker account for GAE This script grants the required [IAM roles](https://cloud.google.com/appengine/docs/admin-api/access-control) to the Spinnaker instance's service account, in the GCP project within which you wish to manage GAE resources. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/add_gae_account.sh ``` ### Upgrade Spinnaker First, modify `SPINNAKER_VERSION` in your `properties` file to reflect the desired version of Spinnaker: ```bash cloudshell edit ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ``` Next, use Halyard to apply the changes: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_spinnaker_version.sh ``` ### Upgrade Halyard daemon running in cluster First, modify `HALYARD_VERSION` in your `properties` file to reflect the desired version of Halyard: ```bash cloudshell edit ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties ``` Next, apply this change to the Statefulset managing the Halyard daemon: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_halyard_daemon.sh ``` ### Upgrade Management Environment Update the commands and documentation in your management environment to the latest available version. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_management_environment.sh ``` ### Sign up for Spinnaker for GCP updates and announcements Join the [mailing list](https://groups.google.com/forum/#!forum/spinnaker-for-gcp-announce) to keep informed about updates and other announcements. ### Connect to Redis ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/connect_to_redis.sh ``` ### Restore a backup to Cloud Shell Restore a backup of the halyard configuration and deployment configuration from Cloud Source Repositories to your Cloud Shell. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/restore_backup_to_cloud_shell.sh -p $PROJECT_ID -r $CONFIG_CSR_REPO -h GIT_HASH ``` All backups can be viewed in this [Cloud Source Repository](https://source.cloud.google.com/$PROJECT_ID/$CONFIG_CSR_REPO). ## Configure Operator Access To add additional operators, grant them the `Owner` role on GCP Project {{project-id}}: [IAM Permissions](https://console.developers.google.com/iam-admin/iam?project={{project-id}}) Once they have been added to the project, they can locate Spinnaker by navigating to the newly-registered [Kubernetes Application](https://console.developers.google.com/kubernetes/application/$ZONE/$DEPLOYMENT_NAME/spinnaker/$DEPLOYMENT_NAME?project={{project-id}}). The application's *Next Steps* section contains the relevant links and operator instructions. ### If you have secured Spinnaker via IAP Granting someone the `Owner` role does not implicitly grant them access as a user. For configuring user access, please continue on to the *Configure User Access (IAP)* section. ================================================ FILE: scripts/manage/landing_page_secured.md ================================================ ## Configure User Access (IAP) ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/grant_iap_access.sh ``` Alternatively, you can manually grant the `IAP-secured Web App User` role on the `spinnaker/spin-deck` resource to the user you'd like to grant access to [here](https://console.developers.google.com/security/iap?project={{project-id}}). ## Use Spinnaker ### Connect to Spinnaker Connect to your Spinnaker installation [here](https://$DOMAIN_NAME). ### View Spinnaker Audit Log View the who, what, when and where of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project={{project-id}}&resource=cloud_function&logName=projects%2F{{project-id}}%2Flogs%2F$CLOUD_FUNCTION_NAME&minLogLevel=200). ### View Spinnaker Container Logs View the logging output of the individual components of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project={{project-id}}&resource=k8s_container%2Fcluster_name%2F$GKE_CLUSTER%2Fnamespace_name%2Fspinnaker). ### Install sample applications and pipelines There are sample applications with example pipelines available to install and try out. View and install the samples by running this command: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/list_samples.sh ``` ## Delete Spinnaker ### Generate a cleanup script This command generates a script that deletes all the resources that were provisioned as part of your Spinnaker installation. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/generate_deletion_script.sh ``` ================================================ FILE: scripts/manage/landing_page_unsecured.md ================================================ ## Use Spinnaker ### Forward the port to Deck, and connect Don't use the `hal deploy connect` command. Instead, use the following command only. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/connect_unsecured.sh ``` To connect to the Deck UI, click on the Preview button above and select "Preview on port 8080": ![Image](https://github.com/GoogleCloudPlatform/spinnaker-for-gcp/raw/master/scripts/manage/preview_button.png) ### View Spinnaker Audit Log View the who, what, when and where of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project={{project-id}}&resource=cloud_function&logName=projects%2F{{project-id}}%2Flogs%2F$CLOUD_FUNCTION_NAME&minLogLevel=200). ### View Spinnaker Container Logs View the logging output of the individual components of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project={{project-id}}&resource=k8s_container%2Fcluster_name%2F$GKE_CLUSTER%2Fnamespace_name%2Fspinnaker). ### Expose Spinnaker If you would like to connect to Spinnaker without relying on port forwarding, we can expose it via a secure domain behind the [Identity-Aware Proxy](https://cloud.google.com/iap/). Note that this phase could take 30-60 minutes. **Spinnaker will be inaccessible during this time.** ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_endpoint.sh ``` ### Install sample applications and pipelines There are sample applications with example pipelines available to install and try out. View and install the samples by running this command: ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/list_samples.sh ``` ## Delete Spinnaker ### Generate a cleanup script This command generates a script that deletes all the resources that were provisioned as part of your Spinnaker installation. ```bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/generate_deletion_script.sh ``` ================================================ FILE: scripts/manage/list_samples.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } bold "Here is a list of sample applications available to install. Selecting one will launch" \ "a tutorial to install it." PS3='Please enter your choice: ' tutorials=($(ls -d ~/cloudshell_open/spinnaker-for-gcp/samples/*/ | xargs -n 1 basename) "Quit") select tutorial in "${tutorials[@]}" do case $tutorial in "Quit") break ;; "") bold "Please choose a valid entry (1-${#tutorials[@]})";; *) bold "Launching $tutorial tutorial..." cloudshell launch-tutorial ~/cloudshell_open/spinnaker-for-gcp/samples/$tutorial/install.md break ;; esac done ================================================ FILE: scripts/manage/pull_config.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) if [ "$CI" == true ]; then HAL_PARENT_DIR=$PARENT_DIR else HAL_PARENT_DIR=$HOME fi source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_duplicate_dirs.sh || exit 1 CURRENT_CONTEXT=$(kubectl config current-context) if [ "$?" != "0" ]; then bold "No current Kubernetes context is configured." exit 1 fi HALYARD_POD=spin-halyard-0 TEMP_DIR=$(mktemp -d -t halyard.XXXXX) pushd $TEMP_DIR mkdir .hal # Remove local config so persistent config from Halyard Daemon pod can be copied into place. bold "Removing $HAL_PARENT_DIR/.hal..." rm -rf $HAL_PARENT_DIR/.hal # Copy persistent config into place. bold "Copying halyard/$HALYARD_POD:/home/spinnaker/.hal into $HAL_PARENT_DIR/.hal..." kubectl cp halyard/$HALYARD_POD:/home/spinnaker/.hal .hal source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/restore_config_utils.sh rewrite_hal_key_paths # We want just these subdirs from the Halyard Daemon pod to be copied into place in $HAL_PARENT_DIR/.hal. copy_hal_subdirs cp .hal/config $HAL_PARENT_DIR/.hal EXISTING_DEPLOYMENT_SECRET_NAME=$(kubectl get secret -n halyard \ --field-selector metadata.name=="spinnaker-deployment" \ -o json | jq .items[0].metadata.name) if [ $EXISTING_DEPLOYMENT_SECRET_NAME != 'null' ]; then bold "Restoring Spinnaker deployment config files from Kubernetes secret spinnaker-deployment..." DEPLOYMENT_SECRET_DATA=$(kubectl get secret spinnaker-deployment -n halyard -o json) extract_to_file_if_defined() { DATA_ITEM_VALUE=$(echo $DEPLOYMENT_SECRET_DATA | jq -r ".data.\"$1\"") if [ $DATA_ITEM_VALUE != 'null' ]; then echo $DATA_ITEM_VALUE | base64 -d > $2 fi } extract_to_file_if_defined properties "$PROPERTIES_FILE" extract_to_file_if_defined config.json $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/config.json extract_to_file_if_defined index.js $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/index.js extract_to_file_if_defined configure_iap_expanded.md $PARENT_DIR/spinnaker-for-gcp/scripts/expose/configure_iap_expanded.md extract_to_file_if_defined openapi_expanded.yml $PARENT_DIR/spinnaker-for-gcp/scripts/expose/openapi_expanded.yml mkdir -p ~/.spin extract_to_file_if_defined config ~/.spin/config extract_to_file_if_defined key.json ~/.spin/key.json rewrite_spin_key_path fi popd rm -rf $TEMP_DIR if [ "$CI" != true ]; then # Update the generated markdown pages. ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_landing_page.sh fi ================================================ FILE: scripts/manage/push_and_apply.sh ================================================ #!/usr/bin/env bash ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_config.sh || exit 1 ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/apply_config.sh ================================================ FILE: scripts/manage/push_config.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) if [ "$CI" == true ]; then HAL_PARENT_DIR=$PARENT_DIR else HAL_PARENT_DIR=$HOME fi source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_duplicate_dirs.sh || exit 1 $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_git_config.sh || exit 1 source "$PROPERTIES_FILE" # TODO(duftler): Add check to ensure that we are not overriding with older or empty config. CURRENT_CONTEXT=$(kubectl config current-context) if [ "$?" != "0" ]; then bold "No current Kubernetes context is configured." exit 1 fi CURRENT_CONTEXT_PROJECT=$(echo $CURRENT_CONTEXT | cut -d '_' -f 2) CURRENT_CONTEXT_ZONE=$(echo $CURRENT_CONTEXT | cut -d '_' -f 3) CURRENT_CONTEXT_CLUSTER=$(echo $CURRENT_CONTEXT | cut -d '_' -f 4) if [ $CURRENT_CONTEXT_PROJECT != $PROJECT_ID ]; then bold "Your Spinnaker config references project $PROJECT_ID, but you are connected to a cluster in project $CURRENT_CONTEXT_PROJECT." bold "Use 'kubectl config use-context' to connect to the correct cluster before pushing the config." exit 1 fi if [ $CURRENT_CONTEXT_ZONE != $ZONE ]; then bold "Your Spinnaker config references zone $ZONE, but you are connected to a cluster in zone $CURRENT_CONTEXT_ZONE." bold "Use 'kubectl config use-context' to connect to the correct cluster before pushing the config." exit 1 fi if [ $CURRENT_CONTEXT_CLUSTER != $GKE_CLUSTER ]; then bold "Your Spinnaker config references cluster $GKE_CLUSTER, but you are connected to cluster $CURRENT_CONTEXT_CLUSTER." bold "Use 'kubectl config use-context' to connect to the correct cluster before pushing the config." exit 1 fi source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/cluster_utils.sh CLUSTER_EXISTS=$(check_for_existing_cluster) if [ -z "$CLUSTER_EXISTS" ]; then bold "Cluster $GKE_CLUSTER cannot be found. It may not exist." bold "To recreate your installation with this config, run:" bold "USE_CLOUD_SHELL_HAL_CONFIG=true $PARENT_DIR/spinnaker-for-gcp/scripts/install/setup.sh" exit 1 fi if [ -z "$CONFIG_CSR_REPO" ]; then bold "CONFIG_CSR_REPO was not set. Please run the $PARENT_DIR/spinnaker-for-gcp/scripts/manage/update_management_environment.sh" \ "command to ensure you have all the necessary properties declared." exit 1 fi HALYARD_POD=spin-halyard-0 TEMP_DIR=$(mktemp -d -t halyard.XXXXX) pushd $TEMP_DIR EXISTING_CSR_REPO=$(gcloud source repos list --format="value(name)" --filter="name=projects/$PROJECT_ID/repos/$CONFIG_CSR_REPO" --project=$PROJECT_ID) if [ -z "$EXISTING_CSR_REPO" ]; then bold "Creating Cloud Source Repository $CONFIG_CSR_REPO..." gcloud source repos create $CONFIG_CSR_REPO --project=$PROJECT_ID fi gcloud source repos clone $CONFIG_CSR_REPO --project=$PROJECT_ID cd $CONFIG_CSR_REPO bold "Backing up $HAL_PARENT_DIR/.hal..." rm -rf .hal mkdir .hal # We want just these subdirs within $HAL_PARENT_DIR/.hal to be copied into place on the Halyard Daemon pod. DIRS=(credentials profiles service-settings) for p in "${DIRS[@]}"; do for f in $(find $HAL_PARENT_DIR/.hal/*/$p -prune 2> /dev/null); do SUB_PATH=$(echo $f | rev | cut -d '/' -f 1,2 | rev) mkdir -p .hal/$SUB_PATH cp -RT $HAL_PARENT_DIR/.hal/$SUB_PATH .hal/$SUB_PATH done done cp $HAL_PARENT_DIR/.hal/config .hal # Please note, rewritable key paths are in both push_config.sh and restore_config_utils.sh REWRITABLE_KEYS=(kubeconfigFile jsonPath jsonKey passwordFile path templatePath tokenFile \ usernamePasswordFile sshPrivateKeyFilePath sshKnownHostsFilePath trustStore credentialPath) for k in "${REWRITABLE_KEYS[@]}"; do grep $k .hal/config &> /dev/null FOUND_TOKEN=$? if [ "$FOUND_TOKEN" == "0" ]; then bold "Rewriting $k path to reflect user 'spinnaker' on Halyard Daemon pod..." sed -i "s/$k: \/home\/$USER/$k: \/home\/spinnaker/" .hal/config fi done bold "Backing up Spinnaker deployment config files..." rm -rf deployment_config_files mkdir deployment_config_files copy_if_exists() { if [ -e $1 ]; then # If a filter token was passed, only copy the file if the token is present in the source file. if [ $3 ]; then if [ "$(grep $3 $1)" ]; then cp $1 $2 fi else cp $1 $2 fi fi } copy_if_exists "$PROPERTIES_FILE" deployment_config_files copy_if_exists $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/config.json deployment_config_files copy_if_exists $PARENT_DIR/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/index.js deployment_config_files # These files are generated when Spinnaker is exposed via IAP. # If the operator is managing more than one installation we don't want to inadvertently backup files from the wrong installation. copy_if_exists $PARENT_DIR/spinnaker-for-gcp/scripts/expose/configure_iap_expanded.md deployment_config_files "$PROJECT_ID\." copy_if_exists $PARENT_DIR/spinnaker-for-gcp/scripts/expose/openapi_expanded.yml deployment_config_files "$PROJECT_ID\." copy_if_exists ~/.spin/config deployment_config_files "$PROJECT_ID\." copy_if_exists ~/.spin/config deployment_config_files "localhost\:" copy_if_exists ~/.spin/key.json deployment_config_files "$PROJECT_ID\." # Remove old persistent config so new config can be copied into place. bold "Removing halyard/$HALYARD_POD:/home/spinnaker/.hal..." kubectl -n halyard exec $HALYARD_POD -- bash -c "rm -rf ~/.hal/*" # Copy new config into place. bold "Copying $HAL_PARENT_DIR/.hal into halyard/$HALYARD_POD:/home/spinnaker/.hal..." kubectl -n halyard cp $TEMP_DIR/$CONFIG_CSR_REPO/.hal spin-halyard-0:/home/spinnaker EXISTING_DEPLOYMENT_SECRET_NAME=$(kubectl get secret -n halyard \ --field-selector metadata.name=="spinnaker-deployment" \ -o json | jq .items[0].metadata.name) if [ $EXISTING_DEPLOYMENT_SECRET_NAME != 'null' ]; then bold "Deleting Kubernetes secret spinnaker-deployment..." kubectl delete secret spinnaker-deployment -n halyard fi bold "Creating Kubernetes secret spinnaker-deployment containing Spinnaker deployment config files..." kubectl create secret generic spinnaker-deployment -n halyard \ --from-file deployment_config_files git add . git commit -m 'Automated backup.' git push popd rm -rf $TEMP_DIR ================================================ FILE: scripts/manage/restore_backup_to_cloud_shell.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/check_git_config.sh || exit 1 while getopts ":p:r:h:" options; do case $options in p ) PROJECT_ID=$OPTARG ;; r ) CONFIG_CSR_REPO=$OPTARG ;; h ) GIT_HASH=$OPTARG ;; \? ) bold "Invalid option supplied: -$OPTARG" esac done EXAMPLE_COMMAND="'restore_backup_to_cloud_shell.sh -p PROJECT -r REPOSITORY_NAME -h GIT_HASH'" if [ -z "$PROJECT_ID" ]; then bold "Project id is required. $EXAMPLE_COMMAND" exit 1 fi if [ -z "$CONFIG_CSR_REPO" ]; then bold "Cloud Source Repository name is required. $EXAMPLE_COMMAND" exit 1 fi if [ -z "$GIT_HASH" ]; then bold "Git commit hash is required. $EXAMPLE_COMMAND" exit 1 fi TEMP_DIR=$(mktemp -d -t halyard.XXXXX) pushd $TEMP_DIR EXISTING_CSR_REPO=$(gcloud source repos list --format="value(name)" --filter="name=projects/$PROJECT_ID/repos/$CONFIG_CSR_REPO" --project=$PROJECT_ID) if [ -n "$EXISTING_CSR_REPO" ]; then gcloud source repos clone $CONFIG_CSR_REPO --project=$PROJECT_ID else bold "Cloud Source Repository $CONFIG_CSR_REPO not found" popd rm -rf $TEMP_DIR exit 1 fi cd $CONFIG_CSR_REPO HASH_CHECKOUT_ERROR=$(git branch --contains $GIT_HASH 2>&1 > /dev/null) if [ -n "$HASH_CHECKOUT_ERROR" ]; then bold "Git commit hash: $GIT_HASH not found. Please enter a valid commit hash." popd rm -rf $TEMP_DIR exit 1 fi HASH_PREVIEW_LINK="https://source.cloud.google.com/$PROJECT_ID/$EXISTING_CSR_REPO/+/$GIT_HASH" read -p ". $(tput bold)You are about to replace the configuration files in your Cloud Shell with the configuration at: . $HASH_PREVIEW_LINK . This step is not reversible. Do you wish to continue (Y/n)? $(tput sgr0)" yn case $yn in [Yy]* ) ;; "" ) ;; * ) popd rm -rf $TEMP_DIR exit ;; esac git checkout $GIT_HASH &> /dev/null # Remove local hal config so persistent config from backup can be copied into place. bold "Removing $HOME/.hal..." rm -rf ~/.hal # Copy persistent config into place. bold "Copying $CONFIG_CSR_REPO/.hal into $HOME/.hal..." source ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/restore_config_utils.sh rewrite_hal_key_paths # We want just these subdirs from the backup to be copied into place in ~/.hal. copy_hal_subdirs cp .hal/config ~/.hal remove_and_copy() { if [ -e $1 ]; then cp $1 $2 elif [ -e $2 ]; then rm $2 fi } cd deployment_config_files bold "Restoring deployment config... from $CONFIG_CSR_REPO" remove_and_copy properties ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties remove_and_copy config.json ~/cloudshell_open/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/config.json remove_and_copy index.js ~/cloudshell_open/spinnaker-for-gcp/scripts/install/spinnakerAuditLog/index.js remove_and_copy configure_iap_expanded.md ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_iap_expanded.md remove_and_copy openapi_expanded.yml ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/openapi_expanded.yml mkdir -p ~/.spin remove_and_copy config ~/.spin/config remove_and_copy key.json ~/.spin/key.json if [ -e ~/.spin/config ]; then rewrite_spin_key_path fi popd rm -rf $TEMP_DIR bold "Configuration applied. To diff this config with what was last deployed, go to:" bold "https://source.cloud.google.com/$PROJECT_ID/$EXISTING_CSR_REPO/+/$GIT_HASH...master" bold "Note: If secure access via IAP is already configured, that access is left unchanged and remains secure." bold "To apply the halyard config changes to the cluster, run:" bold "~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh" ================================================ FILE: scripts/manage/restore_config_utils.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) if [ "$CI" == true ]; then HAL_PARENT_DIR=$PARENT_DIR else HAL_PARENT_DIR=$HOME fi source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh # Please note, rewritable key paths are in both push_config.sh and restore_config_utils.sh rewrite_hal_key_paths() { REWRITABLE_KEYS=(kubeconfigFile jsonPath jsonKey passwordFile path templatePath tokenFile \ usernamePasswordFile sshPrivateKeyFilePath sshKnownHostsFilePath trustStore credentialPath) for k in "${REWRITABLE_KEYS[@]}"; do grep $k .hal/config &> /dev/null FOUND_TOKEN=$? if [ "$FOUND_TOKEN" == "0" ]; then bold "Rewriting $k path to reflect local user '$USER' on Cloud Shell VM..." sed -i "s/$k: \/home\/spinnaker/$k: \/home\/$USER/" .hal/config fi done } copy_hal_subdirs() { DIRS=(credentials profiles service-settings) for p in "${DIRS[@]}"; do for f in $(find .hal/*/$p -prune 2> /dev/null); do SUB_PATH=$(echo $f | rev | cut -d '/' -f 1,2 | rev) mkdir -p $HAL_PARENT_DIR/.hal/$SUB_PATH cp -RT .hal/$SUB_PATH $HAL_PARENT_DIR/.hal/$SUB_PATH done done } rewrite_spin_key_path() { bold "Rewriting key path in $HOME/.spin/config to reflect local user '$USER' on Cloud Shell VM..." sed -i "s/^ serviceAccountKeyPath: .*/ serviceAccountKeyPath: \"\/home\/$USER\/.spin\/key.json\"/" $HOME/.spin/config } ================================================ FILE: scripts/manage/service_utils.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" if [ -f "$PROPERTIES_FILE" ]; then source "$PROPERTIES_FILE" fi bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } has_service_enabled() { gcloud services list --project $1 \ --filter="config.name:$2" \ --format="value(config.name)" } check_for_command() { COMMAND_PRESENT=$(command -v $1) echo $COMMAND_PRESENT } check_for_required_binaries() { REQUIRED_BINARIES=(git gcloud jq kubectl) MISSING_BINARIES="" for b in "${REQUIRED_BINARIES[@]}"; do BINARY_PATH=$(check_for_command $b) if [ -z "$BINARY_PATH" ]; then if [ -z $MISSING_BINARIES ]; then MISSING_BINARIES="$b" else MISSING_BINARIES="$MISSING_BINARIES, $b" fi fi done if [ -n "$MISSING_BINARIES" ]; then bold "The following command(s) are required for setup but were not found: $MISSING_BINARIES" exit 1 fi } check_for_shared_vpc() { if [ "$PROJECT_ID" != "$NETWORK_PROJECT" -a "$1" = true ]; then bold "Automated setup of Spinnaker for GCP with a Shared VPC host project is currently unsupported. To proceed, continue the setup in Cloud Shell." exit 1 fi } ================================================ FILE: scripts/manage/update_console.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) $PARENT_DIR/spinnaker-for-gcp/scripts/manage/check_duplicate_dirs.sh || exit 1 cloudshell launch-tutorial ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/landing_page_expanded.md ================================================ FILE: scripts/manage/update_halyard_daemon.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties bold "Updating halyard daemon..." if [ -z "$HALYARD_VERSION" ]; then bold "HALYARD_VERSION not set..." exit 1 fi kubectl set image statefulset spin-halyard -n halyard halyard-daemon=us-docker.pkg.dev/spinnaker-community/docker/halyard:$HALYARD_VERSION ================================================ FILE: scripts/manage/update_landing_page.sh ================================================ #!/usr/bin/env bash [ -z "$PARENT_DIR" ] && PARENT_DIR=$(dirname $(realpath $0) | rev | cut -d '/' -f 4- | rev) source $PARENT_DIR/spinnaker-for-gcp/scripts/manage/service_utils.sh [ -z "$PROPERTIES_FILE" ] && PROPERTIES_FILE="$PARENT_DIR/spinnaker-for-gcp/scripts/install/properties" if [ ! -f "$PROPERTIES_FILE" ]; then bold "No properties file was found. Resetting the management environment." git checkout -- $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_expanded.md exit 0 fi source "$PROPERTIES_FILE" # Query for static ip address as a signal that the Spinnaker installation is exposed via a secured endpoint. export IP_ADDR=$(gcloud compute addresses list --filter="name=$STATIC_IP_NAME" \ --format="value(address)" --global --project $PROJECT_ID) if [ -z "$IP_ADDR" ]; then bold "Updating Cloud Shell landing page for unsecured Spinnaker..." cat $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_base.md $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_unsecured.md \ | envsubst > $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_expanded.md else bold "Updating Cloud Shell landing page for secured Spinnaker..." cat $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_base.md $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_secured.md \ | envsubst > $PARENT_DIR/spinnaker-for-gcp/scripts/manage/landing_page_expanded.md fi ================================================ FILE: scripts/manage/update_management_environment.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } pushd ~/cloudshell_open/spinnaker-for-gcp/scripts/manage # We re-generate landing_page_expanded.md all the time; we should not stash those changes. git checkout -- landing_page_expanded.md GIT_STASH_COUNT_BEFORE=$(git stash list | wc -l) bold "Stashing local changes..." git stash save "Stashed by update_management_environment.sh" GIT_STASH_COUNT_AFTER=$(git stash list | wc -l) if [ "$GIT_STASH_COUNT_AFTER" != $GIT_STASH_COUNT_BEFORE ]; then bold "Changes were stashed. You will need to manually reapply any stashed changes after the update." fi git checkout master git pull origin master # New properties have been added over time and we want to ensure these are declared in the properties file. ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/add_missing_properties.sh # Update the GKE Application details view. ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/deploy_application_manifest.sh # Update the generated markdown pages. ./update_landing_page.sh # Refresh the tutorial view. ./update_console.sh popd ================================================ FILE: scripts/manage/update_spinnaker_version.sh ================================================ #!/usr/bin/env bash bold() { echo ". $(tput bold)" "$*" "$(tput sgr0)"; } source ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties bold "Updating Spinnaker to version $SPINNAKER_VERSION..." ~/hal/hal config version edit --version $SPINNAKER_VERSION ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/update_landing_page.sh ================================================ FILE: templates/spinnaker_application_manifest_bottom.yaml ================================================ info: - name: Application Namespace value: spinnaker selector: matchLabels: app.kubernetes.io/name: $DEPLOYMENT_NAME componentKinds: - group: v1 kind: ConfigMap - group: extensions/v1beta1 kind: Deployment - group: v1 kind: PersistentVolumeClaim - group: v1 kind: Secret - group: v1 kind: Service - group: apps/v1beta2 kind: StatefulSet ================================================ FILE: templates/spinnaker_application_manifest_middle_secured.yaml ================================================ notes: |- # Manage your Spinnaker [Open management console in Cloud Shell](https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_git_repo=https://github.com/GoogleCloudPlatform/spinnaker-for-gcp.git&cloudshell_working_dir=scripts/manage&cloudshell_tutorial=landing_page_expanded.md&cloudshell_print=instructions.txt) Ensure you're connected to the correct GKE Cluster from within Cloud Shell. Follow the instructions in the management console to check the Kubernetes context you are using and update it if necessary. # Connect to your Spinnaker [https://$DOMAIN_NAME](https://$DOMAIN_NAME) # View the who, what, when and where of your Spinnaker installation [Spinnaker audit log](https://console.developers.google.com/logs/viewer?project=$PROJECT_ID&resource=cloud_function&logName=projects%2F$PROJECT_ID%2Flogs%2F$CLOUD_FUNCTION_NAME&minLogLevel=200) # View Spinnaker container logs [Spinnaker container logs](https://console.developers.google.com/logs/viewer?project=$PROJECT_ID&resource=k8s_container%2Fcluster_name%2F$GKE_CLUSTER%2Fnamespace_name%2Fspinnaker) # Configuration backups Full backups are stored in [this repo](https://source.cloud.google.com/$PROJECT_ID/$CONFIG_CSR_REPO). # Stay up to date Join the [Spinnaker for GCP Announce](https://groups.google.com/forum/#!forum/spinnaker-for-gcp-announce) Google Group. ================================================ FILE: templates/spinnaker_application_manifest_middle_unsecured.yaml ================================================ notes: |- # Manage your Spinnaker [Open management console in Cloud Shell](https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_git_repo=https://github.com/GoogleCloudPlatform/spinnaker-for-gcp.git&cloudshell_working_dir=scripts/manage&cloudshell_tutorial=landing_page_expanded.md&cloudshell_print=instructions.txt) Ensure you're connected to the correct GKE Cluster from within Cloud Shell. Follow the instructions in the management console to check the Kubernetes context you are using and update it if necessary. # Connect to your Spinnaker Follow the link above to establish port forwarding via Cloud Shell. Note that you can securely expose your Spinnaker via that link as well so it can be directly accessed in the future. # View the who, what, when and where of your Spinnaker installation [Spinnaker audit log](https://console.developers.google.com/logs/viewer?project=$PROJECT_ID&resource=cloud_function&logName=projects%2F$PROJECT_ID%2Flogs%2F$CLOUD_FUNCTION_NAME&minLogLevel=200) # View Spinnaker container logs [Spinnaker container logs](https://console.developers.google.com/logs/viewer?project=$PROJECT_ID&resource=k8s_container%2Fcluster_name%2F$GKE_CLUSTER%2Fnamespace_name%2Fspinnaker) # Configuration backups Full backups are stored in [this repo](https://source.cloud.google.com/$PROJECT_ID/$CONFIG_CSR_REPO). # Stay up to date Join the [Spinnaker for GCP Announce](https://groups.google.com/forum/#!forum/spinnaker-for-gcp-announce) Google Group. ================================================ FILE: templates/spinnaker_application_manifest_top.yaml ================================================ --- apiVersion: app.k8s.io/v1beta1 kind: Application metadata: name: $DEPLOYMENT_NAME namespace: spinnaker annotations: kubernetes-engine.cloud.google.com/icon: >- data:image/png;base64, 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 marketplace.cloud.google.com/deploy-info: '{"partner_id": "google-cloud-platform", "product_id": "spinnaker", "partner_name": "Google"}' labels: app.kubernetes.io/name: spinnaker app.kubernetes.io/instance: $DEPLOYMENT_NAME app.kubernetes.io/version: $SPINNAKER_VERSION app.kubernetes.io/managed-by: Marketplace spec: descriptor: type: Spinnaker version: $SPINNAKER_VERSION description: |- Spinnaker is an open source, multi-cloud continuous delivery platform for releasing software changes with high velocity and confidence. If you would like to learn more about Spinnaker, please visit the [Spinnaker website](https://spinnaker.io/). # Support This image is built by Google. It is your responsibility to keep container images you run or store in your own repositories up to date with security patches. Community support for Spinnaker is available on: [GitHub](https://github.com/spinnaker/spinnaker/issues) [Slack](http://join.spinnaker.io/) [Stack Overflow](http://stackoverflow.com/questions/tagged/spinnaker/) maintainers: - name: Google Click to Deploy url: https://cloud.google.com/solutions/#click-to-deploy links: - description: 'User Guide: Install and Manage Spinnaker on Google Cloud Platform' url: https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp - description: 'Spinnaker Documentation' url: https://spinnaker.io