diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..5a8755e73 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +# Copyright 2022 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. + +name: | + Create a new release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version" + required: true + changelog: + description: "I have updated the CHANGELOG" + required: true + type: boolean + +permissions: + contents: write + +jobs: + release: + name: "Release new version" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: "Validate input" + run: | + [[ "${{ github.event.inputs.changelog }}" != "true" ]] && { echo 'You didn''t update the changelog.' ; exit 1; } + [[ -n "${{ github.event.inputs.version }}" ]] || { echo 'Version not specified!'; exit 1; } + [[ "${{ github.event.inputs.version }}" != v* ]] && { echo 'Version does not start with v!' ; exit 1; } + + - uses: actions/setup-go@v3 + with: + go-version: 1.16 + + - name: "Update all module names" + run: | + cd tools/tfeditor + go build . + ./tfeditor -path ../.. -module-name "google-pso-tool/cloud-foundation-fabric/{{ .Module }}/${{ github.event.inputs.version }}" + cd ../.. + + git config --global user.name "Release Automation" + git config --global user.email "cloud-foundation-fabric@google.com" + + git commit -a -m "Release version ${{ github.event.inputs.version }}" + git push origin master + + - name: "Tag and release" + run: | + git tag ${{ github.event.inputs.version }} + git push origin ${{ github.event.inputs.version }} diff --git a/.gitignore b/.gitignore index 4fb446019..f2ac5f40d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ fast/stages/**/terraform-*.auto.tfvars.json fast/stages/**/0*.auto.tfvars* **/node_modules fast/stages/**/globals.auto.tfvars.json -cloud_sql_proxy \ No newline at end of file +cloud_sql_proxy +examples/cloud-operations/binauthz/tenant-setup.yaml +examples/cloud-operations/binauthz/app/app.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 47a77febe..4ae3a0091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ All notable changes to this project will be documented in this file. - add support for IAM and Cloud Build triggers to source repository module - add `id` output to service account module +- add support for secrets to cloud function module +- new binary authorization module **FAST** - add support for Cloud Source Repositories in stage 0 and 1 CI/CD - fix Gitlab workflow indentation +- remove unsupported attributes and add supported ones to the Gitlab mapping used for Workload Identity Federation pools +- add roles for CI/CD source repositories to stage 1 service account on automation project +- fixes to CI/CD source repositories in stage 1 ## [16.0.0] - 2022-06-06 diff --git a/examples/cloud-operations/binauthz/README.md b/examples/cloud-operations/binauthz/README.md new file mode 100644 index 000000000..18d7ab51b --- /dev/null +++ b/examples/cloud-operations/binauthz/README.md @@ -0,0 +1,127 @@ +# Binary Authorization + +The following example shows to how to create a CI and a CD pipeline in Cloud Build for the deployment of an application to a private GKE cluster with unrestricted access to a public endpoint. The example enables a Binary Authorization policy in the project so only images that have been attested can be deployed to the cluster. The attestations are created using a cryptographic key pair that has been provisioned in KMS. + +The diagram below depicts the architecture used in the example. + +![Architecture](diagram.png) + +The CI and CD pipelines are implemented as Cloud Build triggers that run with a user-specified service account. + +The CI pipeline does the following: + +* Builds and pushes the image to Artifact registry +* Creates an attestation for the image. + +The CD pipeline deploys the application to the cluster. + +## Running the example + +Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=examples%2Fcloud-operations%2Fbinauthz), then go through the following steps to create resources: + +* `terraform init` +* `terraform apply -var project_id=my-project-id` + +WARNING: The example requires the activation of the Binary Authorization API. That API does not support authentication with user credentials. A service account will need to be used to run the example + +## Testing the example + +Once the resources have been created, do the following to verify that everything works as expected. + +1. Fetch the cluster credentials + + gcloud container clusters get-credentials cluster --project + +2. Apply the manifest tenant-setup.yaml available in your work directory. + + kubectl apply -f tenant-setup.yaml + + By applying that manifest thw following is created: + + * A namespace called "apis". This is the namespace where the application will be deployed. + * A Role and a RoleBinding in previously created namespace so the service account that has been configured for the CD pipeline trigger in Cloud Build is able to deploy the kubernetes application to that namespace. + +3. Change to the image subdirectory in your work directory + + cd /image + +4. Run the following commands: + + git init + git remote add origin ssh://:2022/p//r/image + git push -u origin main + +4. In the Cloud Build > History section in the Google Cloud console you should see a job running. That job is build the image, pushing to Artifact Registry and creating an attestation. + + Once the job finishes copy the digest of the image that is displayed in the Cloud Build job output. + +5. Change to the app subdirectory in your working directory. + + cd /app + +6. Edit the app.yaml file and replace the string DIGEST with the value you copied before. + +7. Run the following commands: + + git init + git remote add origin ssh://:2022/p//r/app + git push -u origin main + +8. In the Cloud Build > History section in the Google Cloud console you should see a job running. The job will deploy the application to the cluster. + +9. Go to the Kubernetes Engine > Workloads section to check that the deployment was successful and that the Binary Authorization admissions controller webhook did not block the deployment. + +10. Change to the working directory and try to deploy an image that has not been attested. + + cat < Workloads section to check that that the Binary Authorization admissions controller webhook did not block the deployment. + +The application deployed to the cluster is an RESTful API that enables managing Google Cloud storage buckets in the project. Workload identity is used so the app can interact with the Google Cloud Storage API. + +Once done testing, you can clean up resources by running `terraform destroy`. + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [project_id](variables.tf#L26) | Project ID. | string | ✓ | | +| [master_cidr_block](variables.tf#L49) | Master CIDR block. | string | | "10.0.0.0/28" | +| [pods_cidr_block](variables.tf#L37) | Pods CIDR block. | string | | "172.16.0.0/20" | +| [prefix](variables.tf#L31) | Prefix for resources created. | string | | null | +| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | object({…}) | | null | +| [region](variables.tf#L61) | Region. | string | | "europe-west1" | +| [services_cidr_block](variables.tf#L43) | Services CIDR block. | string | | "192.168.0.0/24" | +| [subnet_cidr_block](variables.tf#L55) | Subnet CIDR block. | string | | "10.0.1.0/24" | +| [zone](variables.tf#L67) | Zone. | string | | "europe-west1-c" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [app_repo_url](outputs.tf#L22) | App source repository url. | | +| [image_repo_url](outputs.tf#L17) | Image source repository url. | | + + diff --git a/examples/cloud-operations/binauthz/app/clobuild.yaml b/examples/cloud-operations/binauthz/app/clobuild.yaml new file mode 100644 index 000000000..6477ecd7d --- /dev/null +++ b/examples/cloud-operations/binauthz/app/clobuild.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 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 +# +# https://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. + +steps: + - id: 'Deploy app' + name: 'gcr.io/cloud-builders/kubectl' + args: + - 'apply' + - '-f' + - 'app.yaml' + env: + - 'CLOUDSDK_COMPUTE_ZONE=${_ZONE}' + - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER}' +options: + logging: CLOUD_LOGGING_ONLY diff --git a/examples/cloud-operations/binauthz/diagram.png b/examples/cloud-operations/binauthz/diagram.png new file mode 100644 index 000000000..0ee786f03 Binary files /dev/null and b/examples/cloud-operations/binauthz/diagram.png differ diff --git a/examples/cloud-operations/binauthz/image/.dockerignore b/examples/cloud-operations/binauthz/image/.dockerignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/examples/cloud-operations/binauthz/image/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/examples/cloud-operations/binauthz/image/.gitignore b/examples/cloud-operations/binauthz/image/.gitignore new file mode 100644 index 000000000..a8603104a --- /dev/null +++ b/examples/cloud-operations/binauthz/image/.gitignore @@ -0,0 +1 @@ +node_modules/** diff --git a/examples/cloud-operations/binauthz/image/Dockerfile b/examples/cloud-operations/binauthz/image/Dockerfile new file mode 100644 index 000000000..03c3e436c --- /dev/null +++ b/examples/cloud-operations/binauthz/image/Dockerfile @@ -0,0 +1,25 @@ +# 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. + +FROM node:18-alpine + +WORKDIR /app + +COPY ["package.json", "package-lock.json*", "./"] + +RUN npm install + +COPY . . + +CMD [ "node", "index.js" ] \ No newline at end of file diff --git a/examples/cloud-operations/binauthz/image/README.md b/examples/cloud-operations/binauthz/image/README.md new file mode 100644 index 000000000..88481cc82 --- /dev/null +++ b/examples/cloud-operations/binauthz/image/README.md @@ -0,0 +1,27 @@ +# Storage API + +This application it is a RESTful API that let's you manage the Google Cloud Storage buckets available is a project. In order to do so the application needs to authenticate with a service account that has been granted the Storage Admin (`roles/storage.admin`) role. + +Find below the operations that can be performed using it: + +* Get buckets in project + + curl -v http://localhost:3000/buckets + +* Get files in bucket + + curl -v http://localhost:3000/buckets/BUCKET_NAME + +* Create a bucket + + curl -v http://localhost:3000/buckets \ + -H'Content-Type: application/json' \ + -d @- < { + try { + const [buckets] = await storage.getBuckets(); + res.json(buckets.map(bucket => bucket.name)); + } catch (error) { + res.status(500).json({ + message: `An error occurred trying to fetch the buckets in project: ${error}` + }); + } +}); + +app.get('/buckets/:name', async (req, res) => { + const name = req.params.name; + try { + const [files] = await storage.bucket(name).getFiles(); + res.json(files.map(file => file.name)); + } catch (error) { + res.status(500).json({ + message: `An error occurred fetch the files in ${name} bucket: ${error}` + }); + } +}); + +app.post('/buckets', async (req, res) => { + const name = req.body.name; + try { + const [bucket] = await storage.createBucket(name); + res.status(201).json({ + "name": bucket.name + }); + } catch (error) { + res.status(500).json({ + message: `An error occurred trying to create ${name} bucket: ${error}` + }); + } +}); + +app.delete('/buckets/:name', async (req, res) => { + const name = req.params.name; + try { + await storage.bucket(name).delete(); + res.send() + } catch (error) { + res.status(500).json({ + message: `An error occurred trying to delete ${name} bucket: ${error}` + }); + } +}); + +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}`) +}) \ No newline at end of file diff --git a/examples/cloud-operations/binauthz/image/package-lock.json b/examples/cloud-operations/binauthz/image/package-lock.json new file mode 100644 index 000000000..c7eed8518 --- /dev/null +++ b/examples/cloud-operations/binauthz/image/package-lock.json @@ -0,0 +1,2277 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@google-cloud/storage": "^5.18.3", + "express": "^4.17.3" + } + }, + "node_modules/@google-cloud/common": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.10.0.tgz", + "integrity": "sha512-XMbJYMh/ZSaZnbnrrOFfR/oQrb0SxG4qh6hDisWCoEbFcBHV0qHQo4uXfeMCzolx2Mfkh6VDaOGg+hyJsmxrlw==", + "dependencies": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^7.14.0", + "retry-request": "^4.2.2", + "teeny-request": "^7.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.18.3.tgz", + "integrity": "sha512-573qJ0ECoy3nkY5YaMWcVf4/46n/zdvfNgAyjaLQywl/eL38uxDhs7YVJd3pcgslaMUwKKsd/eD3St+Pq2iPew==", + "dependencies": { + "@google-cloud/common": "^3.8.1", + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.0.0", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "snakeize": "^0.1.0", + "stream-events": "^1.0.4", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/date-and-time": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.0.tgz", + "integrity": "sha512-DY53oj742mykXjZzDxT7NxH5cxwBRb7FsVG5+8pcV96qU9JQd0UhA21pQB18fwwsXOXeSM0RJV4OzgVxu8eatg==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/gaxios": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", + "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==" + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + }, + "node_modules/teeny-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@google-cloud/common": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.10.0.tgz", + "integrity": "sha512-XMbJYMh/ZSaZnbnrrOFfR/oQrb0SxG4qh6hDisWCoEbFcBHV0qHQo4uXfeMCzolx2Mfkh6VDaOGg+hyJsmxrlw==", + "requires": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^7.14.0", + "retry-request": "^4.2.2", + "teeny-request": "^7.0.0" + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==" + }, + "@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==" + }, + "@google-cloud/storage": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.18.3.tgz", + "integrity": "sha512-573qJ0ECoy3nkY5YaMWcVf4/46n/zdvfNgAyjaLQywl/eL38uxDhs7YVJd3pcgslaMUwKKsd/eD3St+Pq2iPew==", + "requires": { + "@google-cloud/common": "^3.8.1", + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.0.0", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "snakeize": "^0.1.0", + "stream-events": "^1.0.4", + "xdg-basedir": "^4.0.0" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "requires": { + "retry": "0.13.1" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, + "body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "date-and-time": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.0.tgz", + "integrity": "sha512-DY53oj742mykXjZzDxT7NxH5cxwBRb7FsVG5+8pcV96qU9JQd0UhA21pQB18fwwsXOXeSM0RJV4OzgVxu8eatg==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "gaxios": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", + "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.1" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "requires": { + "node-forge": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + } + }, + "hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==" + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + }, + "teeny-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/examples/cloud-operations/binauthz/image/package.json b/examples/cloud-operations/binauthz/image/package.json new file mode 100644 index 000000000..26cd3ebbc --- /dev/null +++ b/examples/cloud-operations/binauthz/image/package.json @@ -0,0 +1,15 @@ +{ + "name": "app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@google-cloud/storage": "^5.18.3", + "express": "^4.17.3" + } +} diff --git a/examples/cloud-operations/binauthz/main.tf b/examples/cloud-operations/binauthz/main.tf new file mode 100644 index 000000000..af34cb926 --- /dev/null +++ b/examples/cloud-operations/binauthz/main.tf @@ -0,0 +1,274 @@ +/** + * Copyright 2022 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. + */ + +locals { + prefix = (var.prefix == null || var.prefix == "") ? "" : "${var.prefix}-" + k8s_ns = "apis" + k8s_sa = "storage-api-sa" + image = ( + "${var.region}-docker.pkg.dev/${module.project.project_id}/${module.docker_artifact_registry.name}/storage-api" + ) +} + +module "project" { + source = "../../../modules/project" + billing_account = (var.project_create != null + ? var.project_create.billing_account_id + : null + ) + parent = (var.project_create != null + ? var.project_create.parent + : null + ) + prefix = var.project_create == null ? null : var.prefix + name = var.project_id + services = [ + "artifactregistry.googleapis.com", + "binaryauthorization.googleapis.com", + "cloudbuild.googleapis.com", + "cloudkms.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "containeranalysis.googleapis.com", + "sourcerepo.googleapis.com" + ] + iam = { + "roles/storage.admin" = [module.sa.iam_email] + "roles/logging.logWriter" = [ + module.image_cb_sa.iam_email, + module.app_cb_sa.iam_email + ] + "roles/container.viewer" = [module.app_cb_sa.iam_email] + "roles/containeranalysis.occurrences.editor" = [module.image_cb_sa.iam_email] + "roles/containeranalysis.notes.attacher" = [module.image_cb_sa.iam_email] + } +} + +module "vpc" { + source = "../../../modules/net-vpc" + project_id = module.project.project_id + name = "${local.prefix}vpc" + subnets = [ + { + ip_cidr_range = var.subnet_cidr_block + name = "subnet" + region = var.region + secondary_ip_range = { + pods = var.pods_cidr_block + services = var.services_cidr_block + } + } + ] +} + +module "nat" { + source = "../../../modules/net-cloudnat" + project_id = module.project.project_id + region = var.region + name = "${local.prefix}nat" + router_network = module.vpc.name +} + +module "cluster" { + source = "../../../modules/gke-cluster" + project_id = module.project.project_id + name = "${local.prefix}cluster" + location = var.zone + network = module.vpc.self_link + subnetwork = module.vpc.subnet_self_links["${var.region}/subnet"] + secondary_range_pods = "pods" + secondary_range_services = "services" + private_cluster_config = { + enable_private_nodes = true + enable_private_endpoint = false + master_ipv4_cidr_block = var.master_cidr_block + master_global_access = false + } + enable_binary_authorization = true + workload_identity = true +} + +module "cluster_nodepool" { + source = "../../../modules/gke-nodepool" + project_id = module.project.project_id + cluster_name = module.cluster.name + location = var.zone + name = "nodepool" + node_service_account_create = true + initial_node_count = 3 +} + +module "kms" { + source = "../../../modules/kms" + project_id = module.project.project_id + keyring = { location = var.region, name = "test-keyring" } + keyring_create = true + keys = { test-key = null } + key_purpose = { + test-key = { + purpose = "ASYMMETRIC_SIGN" + version_template = { + algorithm = "RSA_SIGN_PKCS1_4096_SHA512" + protection_level = null + } + } + } + key_iam = { + test-key = { + "roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email] + "roles/cloudkms.signer" = [module.image_cb_sa.iam_email] + } + } +} + +data "google_kms_crypto_key_version" "version" { + crypto_key = module.kms.key_ids["test-key"] +} + +module "binauthz" { + source = "../../../modules/binauthz" + project_id = module.project.project_id + default_admission_rule = { + evaluation_mode = "ALWAYS_DENY" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = null + } + cluster_admission_rules = { + "${var.zone}.${module.cluster.name}" = { + evaluation_mode = "REQUIRE_ATTESTATION" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = ["test-attestor"] + } + } + attestors_config = { + "test-attestor" : { + note_reference = null + pgp_public_keys = null + pkix_public_keys = [{ + id = data.google_kms_crypto_key_version.version.id + public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem + signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm + }] + iam = { + "roles/binaryauthorization.attestorsViewer" = [module.image_cb_sa.iam_email] + } + } + } +} + +module "docker_artifact_registry" { + source = "../../../modules/artifact-registry" + project_id = module.project.project_id + location = var.region + format = "DOCKER" + id = "${local.prefix}registry" + iam = { + "roles/artifactregistry.writer" = [module.image_cb_sa.iam_email] + "roles/artifactregistry.reader" = [module.cluster_nodepool.service_account_iam_email] + } +} + +module "image_cb_sa" { + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = "sa-cb-image" +} + +module "image_repo" { + source = "../../../modules/source-repository" + project_id = module.project.project_id + name = "${local.prefix}image" + triggers = { + image-trigger = { + filename = "cloudbuild.yaml" + included_files = null + service_account = module.image_cb_sa.id + template = { + branch_name = "main" + project_id = module.project.project_id + tag_name = null + } + substitutions = { + _IMAGE = local.image + _ATTESTOR = module.binauthz.attestors["test-attestor"].id + _KEY_VERSION = data.google_kms_crypto_key_version.version.name + } + } + } + iam = { + "roles/source.reader" = [module.image_cb_sa.iam_email] + } +} + +module "app_cb_sa" { + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = "sa-cb-app" +} + +module "app_repo" { + source = "../../../modules/source-repository" + project_id = module.project.project_id + name = "${local.prefix}app" + triggers = { + app-trigger = { + filename = "cloudbuild.yaml" + included_files = null + service_account = module.app_cb_sa.id + template = { + branch_name = "main" + project_id = module.project.project_id + tag_name = null + } + substitutions = { + _ZONE = var.zone + _CLUSTER = module.cluster.name + } + } + } + iam = { + "roles/source.reader" = [module.app_cb_sa.iam_email] + } +} + +module "sa" { + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = "sa-storage-api" + iam = { + "roles/iam.workloadIdentityUser" : ["serviceAccount:${module.cluster.cluster.project}.svc.id.goog[${local.k8s_ns}/${local.k8s_sa}]"] + } +} + +resource "local_file" "app_file" { + content = templatefile("${path.module}/templates/app.yaml.tpl", { + k8s_ns = local.k8s_ns + k8s_sa = local.k8s_sa + google_sa = module.sa.email + image = local.image + }) + filename = "${path.module}/app/app.yaml" + file_permission = "0666" +} + +resource "local_file" "rbac_file" { + content = templatefile("${path.module}/templates/tenant-setup.yaml.tpl", { + k8s_ns = local.k8s_ns + google_sa = module.app_cb_sa.email + }) + filename = "${path.module}/tenant-setup.yaml" + file_permission = "0666" +} diff --git a/examples/cloud-operations/binauthz/outputs.tf b/examples/cloud-operations/binauthz/outputs.tf new file mode 100644 index 000000000..dc0829d58 --- /dev/null +++ b/examples/cloud-operations/binauthz/outputs.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2022 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. + */ + +output "image_repo_url" { + description = "Image source repository url." + value = "ssh://@source.developers.google.com:2022/p/${module.project.project_id}/r/${module.image_repo.name}" +} + +output "app_repo_url" { + description = "App source repository url." + value = "ssh://@source.developers.google.com:2022/p/${module.project.project_id}/r/${module.app_repo.name}" +} diff --git a/examples/cloud-operations/binauthz/templates/app.yaml.tpl b/examples/cloud-operations/binauthz/templates/app.yaml.tpl new file mode 100644 index 000000000..43991c8d3 --- /dev/null +++ b/examples/cloud-operations/binauthz/templates/app.yaml.tpl @@ -0,0 +1,45 @@ +# Copyright 2022 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 +# +# https://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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: storage-api-sa + namespace: ${k8s_ns} + annotations: + iam.gke.io/gcp-service-account: ${google_sa} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: storage-api-deployment + namespace: ${k8s_ns} +spec: + selector: + matchLabels: + app: storage-api + replicas: 2 + template: + metadata: + labels: + app: storage-api + spec: + serviceAccountName: ${k8s_sa} + containers: + - name: storage-api + image: ${image}:DIGEST + ports: + - containerPort: 3000 + nodeSelector: + iam.gke.io/gke-metadata-server-enabled: "true" \ No newline at end of file diff --git a/examples/cloud-operations/binauthz/templates/tenant-setup.yaml.tpl b/examples/cloud-operations/binauthz/templates/tenant-setup.yaml.tpl new file mode 100644 index 000000000..f5609dc23 --- /dev/null +++ b/examples/cloud-operations/binauthz/templates/tenant-setup.yaml.tpl @@ -0,0 +1,54 @@ +# Copyright 2022 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 +# +# https://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. + +apiVersion: v1 +kind: Namespace +metadata: + name: ${k8s_ns} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: app-deployment-manager + namespace: ${k8s_ns} +rules: +- apiGroups: + - '' + - 'extensions' + - 'apps' + resources: + - 'namespaces' + - 'serviceaccounts' + - 'deployments' + verbs: + - 'get' + - 'list' + - 'watch' + - 'create' + - 'update' + - 'patch' + - 'delete' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: app-deployment-manager + namespace: ${k8s_ns} +subjects: +- kind: User + name: ${google_sa} +roleRef: + kind: Role + name: app-deployment-manager + apiGroup: rbac.authorization.k8s.io diff --git a/examples/cloud-operations/binauthz/variables.tf b/examples/cloud-operations/binauthz/variables.tf new file mode 100644 index 000000000..c010a12ac --- /dev/null +++ b/examples/cloud-operations/binauthz/variables.tf @@ -0,0 +1,71 @@ +/** + * Copyright 2022 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. + */ + +variable "project_create" { + description = "Parameters for the creation of the new project." + type = object({ + billing_account_id = string + parent = string + }) + default = null +} + +variable "project_id" { + description = "Project ID." + type = string +} + +variable "prefix" { + description = "Prefix for resources created." + type = string + default = null +} + +variable "pods_cidr_block" { + description = "Pods CIDR block." + type = string + default = "172.16.0.0/20" +} + +variable "services_cidr_block" { + description = "Services CIDR block." + type = string + default = "192.168.0.0/24" +} + +variable "master_cidr_block" { + description = "Master CIDR block." + type = string + default = "10.0.0.0/28" +} + +variable "subnet_cidr_block" { + description = "Subnet CIDR block." + type = string + default = "10.0.1.0/24" +} + +variable "region" { + description = "Region." + type = string + default = "europe-west1" +} + +variable "zone" { + description = "Zone." + type = string + default = "europe-west1-c" +} diff --git a/examples/cloud-operations/glb_and_armor/architecture.png b/examples/cloud-operations/glb_and_armor/architecture.png new file mode 100644 index 000000000..4a2b5b376 Binary files /dev/null and b/examples/cloud-operations/glb_and_armor/architecture.png differ diff --git a/examples/factories/project-factory/main.tf b/examples/factories/project-factory/main.tf index 774fc1d64..b9f64361c 100644 --- a/examples/factories/project-factory/main.tf +++ b/examples/factories/project-factory/main.tf @@ -28,7 +28,7 @@ locals { _service_accounts_iam = { for r in local._service_accounts_iam_bindings : r => [ for k, v in var.service_accounts : - "serviceAccount:${k}@${local._project_id}.iam.gserviceaccount.com" + module.service-accounts[k].iam_email if try(index(v, r), null) != null ] } diff --git a/fast/assets/templates/workflow-gitlab.yaml b/fast/assets/templates/workflow-gitlab.yaml index 986d57602..f138ee304 100644 --- a/fast/assets/templates/workflow-gitlab.yaml +++ b/fast/assets/templates/workflow-gitlab.yaml @@ -15,6 +15,15 @@ default: image: name: registry.gitlab.com/gitlab-org/terraform-images/releases/1.1 + before_script: + - | + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + echo "$CICD_MODULES_KEY" | base64 -d | tr -d '\r' | ssh-add - > /dev/null + mkdir -p ~/.ssh + ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts + ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts + cd "$${TF_ROOT}" + cp -R .tf-setup/. . variables: FAST_OUTPUTS_BUCKET: ${outputs_bucket} @@ -43,6 +52,7 @@ cache: # Configure GCP Auth with Access Token gcp-auth: stage: gcp-auth + before_script: [] script: - | PAYLOAD="$(cat < /dev/null - echo "$CICD_MODULES_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p ~/.ssh - ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts - ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts - cd "$${TF_ROOT}" - cp -R .tf-setup/. . gitlab-terraform init dependencies: - gcp-auth @@ -120,13 +124,6 @@ tf-validate: stage: tf-validate script: - | - ssh-agent -a $SSH_AUTH_SOCK > /dev/null - echo "$CICD_MODULES_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p ~/.ssh - ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts - ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts - cd "$${TF_ROOT}" - cp -R .tf-setup/. . gitlab-terraform validate dependencies: - gcp-auth @@ -136,13 +133,6 @@ tf-plan: stage: tf-plan script: - | - ssh-agent -a $SSH_AUTH_SOCK > /dev/null - echo "$CICD_MODULES_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir -p ~/.ssh - ssh-keyscan -H 'gitlab.com' >> ~/.ssh/known_hosts - ssh-keyscan gitlab.com | sort -u - ~/.ssh/known_hosts -o ~/.ssh/known_hosts - cd "$${TF_ROOT}" - cp -R .tf-setup/. . gitlab-terraform plan gitlab-terraform plan-json dependencies: diff --git a/fast/stages/00-bootstrap/README.md b/fast/stages/00-bootstrap/README.md index 8698e284d..7280a34bf 100644 --- a/fast/stages/00-bootstrap/README.md +++ b/fast/stages/00-bootstrap/README.md @@ -177,6 +177,8 @@ Before the first run, the following IAM groups must exist to allow IAM bindings - gcp-security-admins - gcp-support +You can refer to [this animated image](./groups.gif) for a step by step on group creation. + #### Configure variables Then make sure you have configured the correct values for the following variables by providing a `terraform.tfvars` file: @@ -349,13 +351,27 @@ The variable maps each provider's `issuer` attribute with the definitions in the Provider key names are used by the `cicd_repositories` variable to configure authentication for CI/CD repositories, and generally from your Terraform code whenever you need to configure IAM access or impersonation for federated identities. -This is a sample configuration of a GitHub provider, the `attribute_condition` attribute can be set to null if needed: +This is a sample configuration of a GitHub and a Gitlab provider, `attribute_condition` attribute can use any of the mapped attribute for the provider (refer to the `identity-providers.tf` file for the full list) or set to `null` if needed: ```hcl federated_identity_providers = { github-sample = { attribute_condition = "attribute.repository_owner==\"my-github-org\"" issuer = "github" + custom_settings = null + } + gitlab-sample = { + attribute_condition = "attribute.namespace_path==\"my-gitlab-org\"" + issuer = "gitlab" + custom_settings = null + } + gitlab-ce-sample = { + attribute_condition = "attribute.namespace_path==\"my-gitlab-org\"" + issuer = "gitlab" + custom_settings = { + issuer_uri = "https://gitlab.fast.example.com" + allowed_audiences = ["https://gitlab.fast.example.com"] + } } } ``` @@ -376,6 +392,12 @@ cicd_repositories = { name = "my-gh-org/fast-bootstrap" type = "github" } + cicd = { + branch = null + identity_provider = "github-sample" + name = "my-gh-org/fast-cicd" + type = "github" + } resman = { branch = "main" identity_provider = "github-sample" @@ -389,6 +411,8 @@ The `type` attribute can be set to one of the supported repository types: `githu Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage. +You can use Terraform to automate creation of the repositories using the `00-cicd` stage. + The remaining configuration is manual, as it regards the repositories themselves: - create a repository for modules @@ -397,7 +421,7 @@ The remaining configuration is manual, as it regards the repositories themselves - for GitHub - create a key pair - create a [deploy key](https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys) in the modules repository with the public key - - create a `CICD_MODULES_KEY` secret with the private key in each of the repositories that need to access modules + - create a `CICD_MODULES_KEY` secret with the private key in each of the repositories that need to access modules (for Gitlab, please Base64 encode the private key for masking) - for Gitlab - TODO - for Source Repositories @@ -437,31 +461,31 @@ The remaining configuration is manual, as it regards the repositories themselves | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| | [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | | -| [organization](variables.tf#L152) | Organization details. | object({…}) | ✓ | | | -| [prefix](variables.tf#L167) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | +| [organization](variables.tf#L162) | Organization details. | object({…}) | ✓ | | | +| [prefix](variables.tf#L177) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | | | [bootstrap_user](variables.tf#L25) | Email of the nominal user running this stage for the first time. | string | | null | | -| [cicd_repositories](variables.tf#L31) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_role_names](variables.tf#L77) | Names of custom roles defined at the org level. | object({…}) | | {…} | | -| [federated_identity_providers](variables.tf#L89) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | -| [groups](variables.tf#L99) | Group names to grant organization-level permissions. | map(string) | | {…} | | -| [iam](variables.tf#L113) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | -| [iam_additive](variables.tf#L119) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | -| [log_sinks](variables.tf#L127) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | -| [outputs_location](variables.tf#L161) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | +| [cicd_repositories](variables.tf#L31) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_role_names](variables.tf#L83) | Names of custom roles defined at the org level. | object({…}) | | {…} | | +| [federated_identity_providers](variables.tf#L95) | Workload Identity Federation pools. The `cicd_repositories` variable references keys here. | map(object({…})) | | {} | | +| [groups](variables.tf#L109) | Group names to grant organization-level permissions. | map(string) | | {…} | | +| [iam](variables.tf#L123) | Organization-level custom IAM settings in role => [principal] format. | map(list(string)) | | {} | | +| [iam_additive](variables.tf#L129) | Organization-level custom IAM settings in role => [principal] format for non-authoritative bindings. | map(list(string)) | | {} | | +| [log_sinks](variables.tf#L137) | Org-level log sinks, in name => {type, filter} format. | map(object({…})) | | {…} | | +| [outputs_location](variables.tf#L171) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [automation](outputs.tf#L81) | Automation resources. | | | -| [billing_dataset](outputs.tf#L86) | BigQuery dataset prepared for billing export. | | | -| [cicd_repositories](outputs.tf#L91) | CI/CD repository configurations. | | | -| [custom_roles](outputs.tf#L103) | Organization-level custom roles. | | | -| [federated_identity](outputs.tf#L108) | Workload Identity Federation pool and providers. | | | -| [outputs_bucket](outputs.tf#L118) | GCS bucket where generated output files are stored. | | | -| [project_ids](outputs.tf#L123) | Projects created by this stage. | | | -| [providers](outputs.tf#L142) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | -| [service_accounts](outputs.tf#L132) | Automation service accounts created by this stage. | | | -| [tfvars](outputs.tf#L151) | Terraform variable files for the following stages. | ✓ | | +| [automation](outputs.tf#L87) | Automation resources. | | | +| [billing_dataset](outputs.tf#L92) | BigQuery dataset prepared for billing export. | | | +| [cicd_repositories](outputs.tf#L97) | CI/CD repository configurations. | | | +| [custom_roles](outputs.tf#L109) | Organization-level custom roles. | | | +| [federated_identity](outputs.tf#L114) | Workload Identity Federation pool and providers. | | | +| [outputs_bucket](outputs.tf#L124) | GCS bucket where generated output files are stored. | | | +| [project_ids](outputs.tf#L129) | Projects created by this stage. | | | +| [providers](outputs.tf#L149) | Terraform provider files for this stage and dependent stages. | ✓ | stage-01 | +| [service_accounts](outputs.tf#L138) | Automation service accounts created by this stage. | | | +| [tfvars](outputs.tf#L158) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/00-bootstrap/automation.tf b/fast/stages/00-bootstrap/automation.tf index 1cd5f15f2..7e0582635 100644 --- a/fast/stages/00-bootstrap/automation.tf +++ b/fast/stages/00-bootstrap/automation.tf @@ -38,12 +38,18 @@ module "automation-project" { "roles/owner" = [ module.automation-tf-bootstrap-sa.iam_email ] + "roles/cloudbuild.builds.editor" = [ + module.automation-tf-resman-sa.iam_email + ] "roles/iam.serviceAccountAdmin" = [ module.automation-tf-resman-sa.iam_email ] "roles/iam.workloadIdentityPoolAdmin" = [ module.automation-tf-resman-sa.iam_email ] + "roles/source.admin" = [ + module.automation-tf-resman-sa.iam_email + ] "roles/storage.admin" = [ module.automation-tf-resman-sa.iam_email ] @@ -114,6 +120,37 @@ module "automation-tf-bootstrap-sa" { } } +# cicd stage's bucket and service account + +module "automation-tf-cicd-gcs" { + source = "../../../modules/gcs" + project_id = module.automation-project.project_id + name = "iac-core-cicd-0" + prefix = local.prefix + versioning = true + iam = { + "roles/storage.objectAdmin" = [module.automation-tf-cicd-provisioning-sa.iam_email] + } + depends_on = [module.organization] +} + +module "automation-tf-cicd-provisioning-sa" { + source = "../../../modules/iam-service-account" + project_id = module.automation-project.project_id + name = "cicd-0" + description = "Terraform stage 1 CICD service account." + prefix = local.prefix + # allow SA used by CI/CD workflow to impersonate this SA + iam = { + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.automation-tf-cicd-sa["cicd"].iam_email, null) + ]) + } + iam_storage_roles = { + (module.automation-tf-output-gcs.name) = ["roles/storage.admin"] + } +} + # resource hierarchy stage's bucket and service account module "automation-tf-resman-gcs" { diff --git a/fast/stages/00-bootstrap/cicd.tf b/fast/stages/00-bootstrap/cicd.tf index b4032c766..fba8f2481 100644 --- a/fast/stages/00-bootstrap/cicd.tf +++ b/fast/stages/00-bootstrap/cicd.tf @@ -23,20 +23,25 @@ locals { v != null && ( - v.type == "sourcerepo" + try(v.type, null) == "sourcerepo" || - contains(keys(local.identity_providers), coalesce(v.identity_provider, ":")) + contains(keys(local.identity_providers), coalesce(try(v.identity_provider, null), ":")) ) && - fileexists("${path.module}/templates/workflow-${v.type}.yaml") + fileexists(format("${path.module}/templates/workflow-%s.yaml", try(v.type, ""))) ) } cicd_workflow_providers = { bootstrap = "00-bootstrap-providers.tf" + cicd = "00-cicd-providers.tf" resman = "01-resman-providers.tf" } cicd_workflow_var_files = { bootstrap = [] + cicd = [ + "00-bootstrap.auto.tfvars.json", + "globals.auto.tfvars.json" + ] resman = [ "00-bootstrap.auto.tfvars.json", "globals.auto.tfvars.json" diff --git a/fast/stages/00-bootstrap/groups.gif b/fast/stages/00-bootstrap/groups.gif new file mode 100644 index 000000000..0744cb099 Binary files /dev/null and b/fast/stages/00-bootstrap/groups.gif differ diff --git a/fast/stages/00-bootstrap/identity-providers.tf b/fast/stages/00-bootstrap/identity-providers.tf index 31bf5d2cf..d315bdb38 100644 --- a/fast/stages/00-bootstrap/identity-providers.tf +++ b/fast/stages/00-bootstrap/identity-providers.tf @@ -19,17 +19,20 @@ locals { identity_providers = { for k, v in var.federated_identity_providers : k => merge( - v, lookup(local.identity_providers_defs, v.issuer, {}) + v, + lookup(local.identity_providers_defs, v.issuer, {}) ) } identity_providers_defs = { + # https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect github = { attribute_mapping = { - "google.subject" = "assertion.sub" - "attribute.sub" = "assertion.sub" - "attribute.actor" = "assertion.actor" - "attribute.repository" = "assertion.repository" - "attribute.ref" = "assertion.ref" + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.actor" = "assertion.actor" + "attribute.repository" = "assertion.repository" + "attribute.repository_owner" = "assertion.repository_owner" + "attribute.ref" = "assertion.ref" } issuer_uri = "https://token.actions.githubusercontent.com" principal_tpl = "principal://iam.googleapis.com/%s/subject/repo:%s:ref:refs/heads/%s" @@ -38,11 +41,20 @@ locals { # https://docs.gitlab.com/ee/ci/cloud_services/index.html#how-it-works gitlab = { attribute_mapping = { - "google.subject" = "assertion.sub" - "attribute.sub" = "assertion.sub" - "attribute.actor" = "assertion.actor" - "attribute.repository" = "assertion.project_path" - "attribute.ref" = "assertion.ref" + "google.subject" = "assertion.sub" + "attribute.sub" = "assertion.sub" + "attribute.environment" = "assertion.environment" + "attribute.environment_protected" = "assertion.environment_protected" + "attribute.namespace_id" = "assertion.namespace_id" + "attribute.namespace_path" = "assertion.namespace_path" + "attribute.pipeline_id" = "assertion.pipeline_id" + "attribute.pipeline_source" = "assertion.pipeline_source" + "attribute.project_id" = "assertion.project_id" + "attribute.project_path" = "assertion.project_path" + "attribute.repository" = "assertion.project_path" + "attribute.ref" = "assertion.ref" + "attribute.ref_protected" = "assertion.ref_protected" + "attribute.ref_type" = "assertion.ref_type" } allowed_audiences = ["https://gitlab.com"] issuer_uri = "https://gitlab.com" @@ -70,7 +82,15 @@ resource "google_iam_workload_identity_pool_provider" "default" { attribute_condition = each.value.attribute_condition attribute_mapping = each.value.attribute_mapping oidc { - allowed_audiences = try(each.value.allowed_audiences, null) - issuer_uri = each.value.issuer_uri + allowed_audiences = ( + try(each.value.custom_settings.allowed_audiences, null) != null + ? each.value.custom_settings.allowed_audiences + : try(each.value.allowed_audiences, null) + ) + issuer_uri = ( + try(each.value.custom_settings.issuer_uri, null) != null + ? each.value.custom_settings.issuer_uri + : try(each.value.issuer_uri, null) + ) } } diff --git a/fast/stages/00-bootstrap/log-export.tf b/fast/stages/00-bootstrap/log-export.tf index 682d473d9..a0019914e 100644 --- a/fast/stages/00-bootstrap/log-export.tf +++ b/fast/stages/00-bootstrap/log-export.tf @@ -59,15 +59,15 @@ module "log-export-gcs" { module "log-export-logbucket" { source = "../../../modules/logging-bucket" - count = contains(local.log_types, "logging") ? 1 : 0 + for_each = toset([for k, v in var.log_sinks : k if v.type == "logging"]) parent_type = "project" parent = module.log-export-project.project_id - id = "audit-logs-0" + id = "audit-logs-${each.key}" } module "log-export-pubsub" { source = "../../../modules/pubsub" - for_each = toset([for k, v in var.log_sinks : k if v == "pubsub"]) + for_each = toset([for k, v in var.log_sinks : k if v.type == "pubsub"]) project_id = module.log-export-project.project_id name = "audit-logs-${each.key}" } diff --git a/fast/stages/00-bootstrap/organization.tf b/fast/stages/00-bootstrap/organization.tf index 51299e45d..947269c1f 100644 --- a/fast/stages/00-bootstrap/organization.tf +++ b/fast/stages/00-bootstrap/organization.tf @@ -85,11 +85,6 @@ locals { _iam_bootstrap_user = ( var.bootstrap_user == null ? [] : ["user:${var.bootstrap_user}"] ) - _log_sink_destinations = { - bigquery = try(module.log-export-dataset.0.id, null), - logging = try(module.log-export-logbucket.0.id, null), - storage = try(module.log-export-gcs.0.name, null) - } iam = { for role in local.iam_roles : role => distinct(concat( try(sort(local._iam[role]), []), @@ -108,13 +103,16 @@ locals { iam_roles_additive = distinct(concat( keys(local._iam_additive), keys(var.iam_additive) )) - log_sink_destinations = { - for k, v in var.log_sinks : k => ( - v.type == "pubsub" - ? module.log-export-pubsub[k] - : local._log_sink_destinations[v.type] - ) - } + log_sink_destinations = merge( + # use the same dataset for all sinks with `bigquery` as destination + { for k, v in var.log_sinks : k => module.log-export-dataset.0 if v.type == "bigquery" }, + # use the same gcs bucket for all sinks with `storage` as destination + { for k, v in var.log_sinks : k => module.log-export-gcs.0 if v.type == "storage" }, + # use separate pubsub topics and logging buckets for sinks with + # destination `pubsub` and `logging` + module.log-export-pubsub, + module.log-export-logbucket + ) } module "organization" { @@ -185,7 +183,7 @@ module "organization" { logging_sinks = { for name, attrs in var.log_sinks : name => { bq_partitioned_table = attrs.type == "bigquery" - destination = local.log_sink_destinations[name] + destination = local.log_sink_destinations[name].id exclusions = {} filter = attrs.filter iam = true diff --git a/fast/stages/00-bootstrap/outputs.tf b/fast/stages/00-bootstrap/outputs.tf index cfb2460b8..eceb5f820 100644 --- a/fast/stages/00-bootstrap/outputs.tf +++ b/fast/stages/00-bootstrap/outputs.tf @@ -43,6 +43,11 @@ locals { name = "bootstrap" sa = module.automation-tf-bootstrap-sa.email }) + "00-cicd" = templatefile(local._tpl_providers, { + bucket = module.automation-tf-cicd-gcs.name + name = "cicd" + sa = module.automation-tf-cicd-provisioning-sa.email + }) "01-resman" = templatefile(local._tpl_providers, { bucket = module.automation-tf-resman-gcs.name name = "resman" @@ -57,6 +62,7 @@ locals { federated_identity_providers = local.wif_providers outputs_bucket = module.automation-tf-output-gcs.name project_id = module.automation-project.project_id + project_number = module.automation-project.number } custom_roles = local.custom_roles } @@ -133,6 +139,7 @@ output "service_accounts" { description = "Automation service accounts created by this stage." value = { bootstrap = module.automation-tf-bootstrap-sa.email + cicd = module.automation-tf-cicd-provisioning-sa.email resman = module.automation-tf-resman-sa.email } } diff --git a/fast/stages/00-bootstrap/variables.tf b/fast/stages/00-bootstrap/variables.tf index a08fedb1c..f2f1edcd5 100644 --- a/fast/stages/00-bootstrap/variables.tf +++ b/fast/stages/00-bootstrap/variables.tf @@ -37,6 +37,12 @@ variable "cicd_repositories" { name = string type = string }) + cicd = object({ + branch = string + identity_provider = string + name = string + type = string + }) resman = object({ branch = string identity_provider = string @@ -91,6 +97,10 @@ variable "federated_identity_providers" { type = map(object({ attribute_condition = string issuer = string + custom_settings = object({ + issuer_uri = string + allowed_audiences = list(string) + }) })) default = {} nullable = false diff --git a/fast/stages/00-cicd/README.md b/fast/stages/00-cicd/README.md new file mode 100644 index 000000000..54c2b7241 --- /dev/null +++ b/fast/stages/00-cicd/README.md @@ -0,0 +1,208 @@ +# CI/CD bootstrap + +The primary purpose of this stage is to set up your CI/CD project structure automatically, with most of the +necessary configuration to run the pipelines out of the box. + +## How to run this stage + +This stage is meant to be executed after the [bootstrap](../00-bootstrap) stage has run, as it leverages the automation service account and bucket created there. +The entire stage is optional, you may also choose to create your repositories manually. + +### Providers configuration + +The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). + +To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. + +If you have set a valid value for `outputs_location` in the bootstrap stage (see the [bootstrap stage README](../00-bootstrap/#output-files-and-cross-stage-variables) for more details), simply link the relevant `providers.tf` file from this stage's folder in the path you specified: + +```bash +# `outputs_location` is set to `~/fast-config` +ln -s ~/fast-config/providers/00-cicd-providers.tf . +``` + +If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: + +```bash +cd ../00-bootstrap +terraform output -json providers | jq -r '.["00-cicd"]' \ + > ../00-cicd/providers.tf +``` + +If you want to continue to rely on `outputs_location` logic, create a `terraform.tfvars` file and configure it as described [here](../00-bootstrap/#output-files-and-cross-stage-variables). + +### Variable configuration + +There are two broad sets of variables you will need to fill in: + +- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) +- variables specific to resources managed by this stage + +To avoid the tedious job of filling in the first group of variable with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. + +If you configured a valid path for `outputs_location` in the bootstrap stage, simply link the relevant `terraform-*.auto.tfvars.json` files from the outputs folder. For this stage, you need the `.tfvars` file compiled manually for the bootstrap stage, and the one generated by it: + +```bash +# `outputs_location` is set to `~/fast-config` +ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . +# also copy the tfvars file used for the bootstrap stage +cp ../00-bootstrap/terraform.tfvars . +``` + +A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file or add them to the file copied from bootstrap. + +Refer to the [Variables](#variables) table at the bottom of this document, for a full list of variables, their origin (e.g. a stage or specific to this one), and descriptions explaining their meaning. The sections below also describe some of the possible customizations. + +### CI/CD systems + +#### Gitlab + +To configure Gitlab, add the following variable: + +```hcl +gitlab = { + url = "https://gitlab.com" # Or self-hosted URL + project_visibility = "private" + shared_runners_enabled = true +} +``` + +Also set `GITLAB_TOKEN` to a token that has appropriate permissions. + +#### GitHub + +To configure GitHub, add the following variable: + +```hcl +github = { + url = null # Or GitHub Enterprise base URL + visibility = "private" +} +``` + +Also set `GITHUB_TOKEN` to a token that has appropriate permissions. + +### CI/CD repositories + +While the other stages create the necessary supporting structure for their CI/CD pipelines, like service accounts +and such, the `00-cicd` stage creates all the repositories in your CI/CD system through automation. Its +configuration is essentially a combination of all the `cicd_repositories` variables of the other stages +plus additional CI/CD system specific configuration information. + +This is an example of configuring the repositories in this stage. + +```hcl +cicd_repositories = { + bootstrap = { + branch = null + identity_provider = "github-sample" + name = "my-gh-org/fast-bootstrap" + description = "Google Cloud bootstrapping" + type = "github" + create = true + create_group = true + } + cicd = { + branch = null + identity_provider = "github-sample" + name = "my-gh-org/fast-cicd" + description = "Fabric FAST CI/CD setup" + type = "github" + create = true + create_group = true + } + resman = { + branch = "main" + identity_provider = "github-sample" + name = "my-gh-org/fast-resman" + description = "Google Cloud resource management" + type = "github" + create = true + create_group = true + } + networking = { + branch = "main" + identity_provider = "github-sample" + name = "my-gh-org/fast-networking" + description = "Google Cloud networking setup" + type = "github" + create = true + create_group = true + } + security = { + branch = "main" + identity_provider = "github-sample" + description = "Google Cloud security settings" + name = "my-gh-org/fast-security" + type = "github" + create = true + create_group = true + } + data-platform = { + branch = "main" + identity_provider = "github-sample" + name = "my-gh-org/fast-data-platform" + description = "Google Cloud data platform" + type = "github" + create = true + create_group = true + } + project-factory = { + branch = "main" + identity_provider = "github-sample" + name = "my-gh-org/fast-project-factory" + description = "Google Cloud project factory" + type = "github" + create = true + create_group = true + } +} +``` + +The `type` attribute can be set to one of the supported repository types: `github` or `gitlab`. + +Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage. + + +Once done, you can run this stage: + +```bash +terraform init +terraform apply +``` + + + + +## Files + +| name | description | resources | +|---|---|---| +| [cicd.tf](./cicd.tf) | None | tls_private_key | +| [github.tf](./github.tf) | None | github_actions_secret · github_repository | +| [gitlab.tf](./gitlab.tf) | None | gitlab_group · gitlab_project · gitlab_project_variable | +| [main.tf](./main.tf) | Module-level locals and resources. | | +| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | local_file | +| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | google_storage_bucket_object | +| [outputs.tf](./outputs.tf) | Module outputs. | | +| [variables.tf](./variables.tf) | Module variables. | | +| [versions.tf](./versions.tf) | Version pins. | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | +| [cicd_repositories](variables.tf#L35) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L132) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | +| [github](variables.tf#L120) | GitHub settings | object({…}) | | {…} | | +| [gitlab](variables.tf#L106) | Gitlab settings | object({…}) | | {…} | | +| [outputs_location](variables.tf#L141) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [tfvars](outputs.tf#L30) | Terraform variable files for the following stages. | ✓ | | + + diff --git a/fast/stages/00-cicd/cicd.tf b/fast/stages/00-cicd/cicd.tf new file mode 100644 index 000000000..948ab60ed --- /dev/null +++ b/fast/stages/00-cicd/cicd.tf @@ -0,0 +1,38 @@ +/** + * Copyright 2022 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. + */ + +locals { + supported_cicd_systems = ["gitlab", "github", "sourcerepo"] + cicd_repositories = { + for k, v in coalesce(var.cicd_repositories, {}) : k => merge(v, + { group = join("/", slice(split("/", v.name), 0, length(split("/", v.name)) - 1)) }, + { name = element(split("/", v.name), length(split("/", v.name)) - 1) }, + { create_group = try(v.create_group, true) }) + if( + v != null + && + contains(local.supported_cicd_systems, try(v.type, "")) + ) + } + cicd_repositories_by_system = { for system in local.supported_cicd_systems : system => { + for k, v in local.cicd_repositories : k => v if v.type == system + } + } +} + +resource "tls_private_key" "cicd-modules-key" { + algorithm = "ED25519" +} diff --git a/fast/stages/00-cicd/github.tf b/fast/stages/00-cicd/github.tf new file mode 100644 index 000000000..88e73618e --- /dev/null +++ b/fast/stages/00-cicd/github.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2022 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. + */ + +locals { + github_groups = distinct([for k, v in local.cicd_repositories_by_system["github"] : v.group]) +} + +provider "github" { + base_url = var.github.url + owner = local.github_groups[0] +} + +data "github_organization" "organization" { + for_each = toset(local.github_groups) + name = each.value +} + +data "github_repository" "repositories" { + for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo if !try(repo.create, true) } + full_name = format("%s/%s", each.value.group, each.value.name) +} + +resource "github_repository" "repositories" { + for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo if try(repo.create, true) } + + name = each.value.name + description = each.value.description + + visibility = var.github.visibility +} + +resource "github_actions_secret" "actions-modules-key" { + for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo } + + repository = try(each.value.create, true) ? github_repository.repositories[each.key].name : data.github_repository.repositories[each.key].name + secret_name = "CICD_MODULES_KEY" + plaintext_value = tls_private_key.cicd-modules-key.private_key_openssh +} diff --git a/fast/stages/00-cicd/gitlab.tf b/fast/stages/00-cicd/gitlab.tf new file mode 100644 index 000000000..47f1515e6 --- /dev/null +++ b/fast/stages/00-cicd/gitlab.tf @@ -0,0 +1,70 @@ +/** + * Copyright 2022 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. + */ + +locals { + gitlab_create_groups = distinct([for k, v in local.cicd_repositories_by_system["gitlab"] : v.group if try(v.create_group, false)]) + gitlab_existing_groups = distinct([for k, v in local.cicd_repositories_by_system["gitlab"] : v.group if !try(v.create_group, false)]) +} + +provider "gitlab" { + base_url = var.gitlab.url +} + +data "gitlab_group" "group" { + for_each = toset(local.gitlab_existing_groups) + full_path = each.value +} + +data "gitlab_project" "projects" { + for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo if !try(repo.create, true) } + id = format("%s/%s", each.value.group, each.value.name) +} + +resource "gitlab_group" "group" { + for_each = toset(local.gitlab_create_groups) + + name = each.value + path = each.value + description = "Cloud Foundation Fabric FAST: github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/" +} + +resource "gitlab_project" "projects" { + for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo if try(repo.create, true) } + + name = each.value.name + namespace_id = each.value.create_group ? gitlab_group.group[each.value.group].id : data.gitlab_group.group[each.value.group].id + description = each.value.description + + visibility_level = var.gitlab.project_visibility + shared_runners_enabled = var.gitlab.shared_runners_enabled + auto_devops_enabled = false +} + +resource "gitlab_project_variable" "project-modules-key" { + for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo } + + project = try(each.value.create, true) ? gitlab_project.projects[each.key].id : data.gitlab_project.projects[each.key].id + + key = "CICD_MODULES_KEY" + value = base64encode(tls_private_key.cicd-modules-key.private_key_openssh) + + protected = false + masked = true + variable_type = "env_var" +} + + + diff --git a/fast/stages/00-cicd/main.tf b/fast/stages/00-cicd/main.tf new file mode 100644 index 000000000..3ea74b550 --- /dev/null +++ b/fast/stages/00-cicd/main.tf @@ -0,0 +1,16 @@ +/** + * Copyright 2022 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. + */ + diff --git a/fast/stages/00-cicd/outputs-files.tf b/fast/stages/00-cicd/outputs-files.tf new file mode 100644 index 000000000..6c201fc4c --- /dev/null +++ b/fast/stages/00-cicd/outputs-files.tf @@ -0,0 +1,24 @@ +/** + * Copyright 2022 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. + */ + +# tfdoc:file:description Output files persistence to local filesystem. + +resource "local_file" "tfvars" { + for_each = var.outputs_location == null ? {} : { 1 = 1 } + file_permission = "0644" + filename = "${pathexpand(var.outputs_location)}/tfvars/00-cicd.auto.tfvars.json" + content = jsonencode(local.tfvars) +} diff --git a/fast/stages/00-cicd/outputs-gcs.tf b/fast/stages/00-cicd/outputs-gcs.tf new file mode 100644 index 000000000..3a03ae4d7 --- /dev/null +++ b/fast/stages/00-cicd/outputs-gcs.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2022 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. + */ + +# tfdoc:file:description Output files persistence to automation GCS bucket. + +resource "google_storage_bucket_object" "tfvars" { + bucket = var.automation.outputs_bucket + name = "tfvars/00-bootstrap.auto.tfvars.json" + content = jsonencode(local.tfvars) +} diff --git a/fast/stages/00-cicd/outputs.tf b/fast/stages/00-cicd/outputs.tf new file mode 100644 index 000000000..67341ba16 --- /dev/null +++ b/fast/stages/00-cicd/outputs.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2022 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. + */ + +locals { + gitlab_cicd_https = { for k, v in local.cicd_repositories_by_system["gitlab"] : k => v.create ? gitlab_project.projects[k].http_url_to_repo : data.gitlab_project.projects[k].http_url_to_repo } + gitlab_cicd_ssh = { for k, v in local.cicd_repositories_by_system["gitlab"] : k => v.create ? gitlab_project.projects[k].ssh_url_to_repo : data.gitlab_project.projects[k].ssh_url_to_repo } + github_cicd_https = { for k, v in local.cicd_repositories_by_system["github"] : k => v.create ? github_repository.repositories[k].http_clone_url : data.github_repository.repositories[k].http_clone_url } + github_cicd_ssh = { for k, v in local.cicd_repositories_by_system["github"] : k => v.create ? github_repository.repositories[k].git_clone_url : data.github_repository.repositories[k].git_clone_url } + + tfvars = { + cicd_repositories = merge(local.cicd_repositories_by_system["gitlab"], local.cicd_repositories_by_system["github"]) + cicd_ssh_urls = merge(local.gitlab_cicd_ssh, local.github_cicd_ssh) + cicd_https_urls = merge(local.gitlab_cicd_https, local.gitlab_cicd_https) + } +} + +output "tfvars" { + description = "Terraform variable files for the following stages." + sensitive = true + value = local.tfvars +} diff --git a/fast/stages/00-cicd/variables.tf b/fast/stages/00-cicd/variables.tf new file mode 100644 index 000000000..c5cc5f695 --- /dev/null +++ b/fast/stages/00-cicd/variables.tf @@ -0,0 +1,145 @@ +/** + * Copyright 2022 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. + */ + +variable "automation" { + # tfdoc:variable:source 00-bootstrap + description = "Automation resources created by the bootstrap stage." + type = object({ + outputs_bucket = string + project_id = string + project_number = string + federated_identity_pool = string + federated_identity_providers = map(object({ + issuer = string + issuer_uri = string + name = string + principal_tpl = string + principalset_tpl = string + })) + }) +} + +variable "cicd_repositories" { + description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." + type = object({ + bootstrap = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + resman = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + networking = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + security = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + data-platform = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + project-factory = object({ + branch = string + name = string + description = string + type = string + create = bool + create_group = bool + }) + }) + default = null + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || try(v.name, null) != null + ]) + error_message = "Non-null repositories need a non-null name." + } + validation { + condition = alltrue([ + for k, v in coalesce(var.cicd_repositories, {}) : + v == null || ( + contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) + ) + ]) + error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." + } +} + +variable "gitlab" { + description = "Gitlab settings" + type = object({ + url = string + project_visibility = string + shared_runners_enabled = bool + }) + default = { + url = "https://gitlab.com" + project_visibility = "private" + shared_runners_enabled = true + } +} + +variable "github" { + description = "GitHub settings" + type = object({ + url = string + visibility = string + }) + default = { + url = null + visibility = "private" + } +} + +variable "custom_roles" { + # tfdoc:variable:source 00-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = object({ + service_project_network_admin = string + }) + default = null +} + +variable "outputs_location" { + description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable" + type = string + default = null +} diff --git a/fast/stages/00-cicd/versions.tf b/fast/stages/00-cicd/versions.tf new file mode 100644 index 000000000..42d07f3c0 --- /dev/null +++ b/fast/stages/00-cicd/versions.tf @@ -0,0 +1,31 @@ +# Copyright 2022 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 +# +# https://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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + gitlab = { + source = "gitlabhq/gitlab" + version = ">= 3.15.0" + } + github = { + source = "integrations/github" + version = ">= 4.26.0" + } + tls = { + source = "hashicorp/tls" + version = "3.4.0" + } + } +} diff --git a/fast/stages/01-resman/README.md b/fast/stages/01-resman/README.md index cba806096..90ffa7c75 100644 --- a/fast/stages/01-resman/README.md +++ b/fast/stages/01-resman/README.md @@ -108,7 +108,7 @@ terraform apply This stage provides a single built-in customization that offers a minimal (but usable) implementation of the "application" or "business" grouping for resources discussed above. The `team_folders` variable allows you to specify a map of team name and groups, that will result in folders, automation service accounts, and IAM policies applied. -Consider the following example +Consider the following example in a `tfvars` file: ```hcl team_folders = { @@ -179,31 +179,30 @@ Due to its simplicity, this stage lends itself easily to customizations: adding | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [billing_account](variables.tf#L37) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | -| [organization](variables.tf#L140) | Organization details. | object({…}) | ✓ | | 00-bootstrap | -| [prefix](variables.tf#L164) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | -| [cicd_repositories](variables.tf#L46) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_roles](variables.tf#L116) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [groups](variables.tf#L125) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | -| [organization_policy_configs](variables.tf#L150) | Organization policies customization. | object({…}) | | null | | -| [outputs_location](variables.tf#L158) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | -| [tag_names](variables.tf#L175) | Customized names for resource management tags. | object({…}) | | {…} | | -| [team_folders](variables.tf#L192) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | +| [automation](variables.tf#L20) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | +| [billing_account](variables.tf#L38) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-bootstrap | +| [organization](variables.tf#L141) | Organization details. | object({…}) | ✓ | | 00-bootstrap | +| [prefix](variables.tf#L165) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 00-bootstrap | +| [cicd_repositories](variables.tf#L47) | CI/CD repository configuration. Identity providers reference keys in the `automation.federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | +| [custom_roles](variables.tf#L117) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | +| [groups](variables.tf#L126) | Group names to grant organization-level permissions. | map(string) | | {…} | 00-bootstrap | +| [organization_policy_configs](variables.tf#L151) | Organization policies customization. | object({…}) | | null | | +| [outputs_location](variables.tf#L159) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | +| [tag_names](variables.tf#L176) | Customized names for resource management tags. | object({…}) | | {…} | | +| [team_folders](variables.tf#L193) | Team folders to be created. Format is described in a code comment. | map(object({…})) | | null | | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [cicd_repositories](outputs.tf#L157) | WIF configuration for CI/CD repositories. | | | -| [dataplatform](outputs.tf#L169) | Data for the Data Platform stage. | | | -| [gke_multitenant](outputs.tf#L237) | Data for the GKE multitenant stage. | | 03-gke-multitenant | -| [networking](outputs.tf#L185) | Data for the networking stage. | | | -| [project_factories](outputs.tf#L194) | Data for the project factories stage. | | | -| [providers](outputs.tf#L210) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | -| [sandbox](outputs.tf#L217) | Data for the sandbox stage. | | xx-sandbox | -| [security](outputs.tf#L227) | Data for the networking stage. | | 02-security | -| [teams](outputs.tf#L254) | Data for the teams stage. | | | -| [tfvars](outputs.tf#L267) | Terraform variable files for the following stages. | ✓ | | +| [cicd_repositories](outputs.tf#L145) | WIF configuration for CI/CD repositories. | | | +| [dataplatform](outputs.tf#L159) | Data for the Data Platform stage. | | | +| [networking](outputs.tf#L175) | Data for the networking stage. | | | +| [project_factories](outputs.tf#L184) | Data for the project factories stage. | | | +| [providers](outputs.tf#L200) | Terraform provider files for this stage and dependent stages. | ✓ | 02-networking · 02-security · 03-dataplatform · xx-sandbox · xx-teams | +| [sandbox](outputs.tf#L207) | Data for the sandbox stage. | | xx-sandbox | +| [security](outputs.tf#L217) | Data for the networking stage. | | 02-security | +| [teams](outputs.tf#L227) | Data for the teams stage. | | | +| [tfvars](outputs.tf#L240) | Terraform variable files for the following stages. | ✓ | | diff --git a/fast/stages/01-resman/cicd-data-platform.tf b/fast/stages/01-resman/cicd-data-platform.tf index e62a02208..4a5c6d3f5 100644 --- a/fast/stages/01-resman/cicd-data-platform.tf +++ b/fast/stages/01-resman/cicd-data-platform.tf @@ -37,7 +37,7 @@ module "branch-dp-dev-cicd-repo" { included_files = [ "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" ] - service_account = module.branch-dp-dev-sa.iam_email + service_account = module.branch-dp-dev-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -47,6 +47,7 @@ module "branch-dp-dev-cicd-repo" { } } } + depends_on = [module.branch-dp-dev-sa-cicd] } module "branch-dp-prod-cicd-repo" { @@ -68,7 +69,7 @@ module "branch-dp-prod-cicd-repo" { included_files = [ "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" ] - service_account = module.branch-dp-prod-sa.iam_email + service_account = module.branch-dp-prod-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -78,6 +79,7 @@ module "branch-dp-prod-cicd-repo" { } } } + depends_on = [module.branch-dp-prod-sa-cicd] } # SAs used by CI/CD workflows to impersonate automation SAs @@ -96,7 +98,9 @@ module "branch-dp-dev-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ @@ -135,7 +139,9 @@ module "branch-dp-prod-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ diff --git a/fast/stages/01-resman/cicd-networking.tf b/fast/stages/01-resman/cicd-networking.tf index 541d8bda0..951770475 100644 --- a/fast/stages/01-resman/cicd-networking.tf +++ b/fast/stages/01-resman/cicd-networking.tf @@ -35,7 +35,7 @@ module "branch-network-cicd-repo" { fast-02-networking = { filename = ".cloudbuild/workflow.yaml" included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] - service_account = module.branch-network-sa.id + service_account = module.branch-network-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -45,6 +45,7 @@ module "branch-network-cicd-repo" { } } } + depends_on = [module.branch-network-sa-cicd] } # SA used by CI/CD workflows to impersonate automation SAs @@ -63,7 +64,9 @@ module "branch-network-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ diff --git a/fast/stages/01-resman/cicd-security.tf b/fast/stages/01-resman/cicd-security.tf index d6b0b8691..86fd84fdb 100644 --- a/fast/stages/01-resman/cicd-security.tf +++ b/fast/stages/01-resman/cicd-security.tf @@ -35,7 +35,7 @@ module "branch-security-cicd-repo" { fast-02-security = { filename = ".cloudbuild/workflow.yaml" included_files = ["**/*tf", ".cloudbuild/workflow.yaml"] - service_account = module.branch-security-sa.id + service_account = module.branch-security-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -45,6 +45,7 @@ module "branch-security-cicd-repo" { } } } + depends_on = [module.branch-security-sa-cicd] } # SA used by CI/CD workflows to impersonate automation SAs @@ -63,7 +64,9 @@ module "branch-security-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ diff --git a/fast/stages/01-resman/cicd-teams.tf b/fast/stages/01-resman/cicd-teams.tf index 2766e301e..f5e81fd3f 100644 --- a/fast/stages/01-resman/cicd-teams.tf +++ b/fast/stages/01-resman/cicd-teams.tf @@ -37,7 +37,7 @@ module "branch-teams-dev-pf-cicd-repo" { included_files = [ "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" ] - service_account = module.branch-teams-dev-pf-sa.iam_email + service_account = module.branch-teams-dev-pf-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -47,6 +47,7 @@ module "branch-teams-dev-pf-cicd-repo" { } } } + depends_on = [module.branch-teams-dev-pf-sa-cicd] } module "branch-teams-prod-pf-cicd-repo" { @@ -68,7 +69,7 @@ module "branch-teams-prod-pf-cicd-repo" { included_files = [ "**/*json", "**/*tf", "**/*yaml", ".cloudbuild/workflow.yaml" ] - service_account = module.branch-teams-prod-pf-sa.iam_email + service_account = module.branch-teams-prod-pf-sa-cicd.0.id substitutions = {} template = { project_id = null @@ -78,6 +79,7 @@ module "branch-teams-prod-pf-cicd-repo" { } } } + depends_on = [module.branch-teams-prod-pf-sa-cicd] } # SAs used by CI/CD workflows to impersonate automation SAs @@ -96,7 +98,9 @@ module "branch-teams-dev-pf-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ @@ -135,7 +139,9 @@ module "branch-teams-prod-pf-sa-cicd" { iam = ( each.value.type == "sourcerepo" # used directly from the cloud build trigger for source repos - ? {} + ? { + "roles/iam.serviceAccountUser" = local.automation_resman_sa + } # impersonated via workload identity federation for external repos : { "roles/iam.workloadIdentityUser" = [ diff --git a/fast/stages/01-resman/main.tf b/fast/stages/01-resman/main.tf index 6cefbd25d..c40957964 100644 --- a/fast/stages/01-resman/main.tf +++ b/fast/stages/01-resman/main.tf @@ -16,6 +16,13 @@ locals { # convenience flags that express where billing account resides + automation_resman_sa = try( + [format( + "serviceAccount:%s", + data.google_client_openid_userinfo.provider_identity.0.email + )], + [] + ) billing_ext = var.billing_account.organization_id == null billing_org = var.billing_account.organization_id == var.organization.id billing_org_ext = !local.billing_ext && !local.billing_org @@ -64,3 +71,7 @@ locals { try(var.automation.federated_identity_providers, null), {} ) } + +data "google_client_openid_userinfo" "provider_identity" { + count = length(local.cicd_repositories) > 0 ? 1 : 0 +} diff --git a/fast/stages/01-resman/outputs.tf b/fast/stages/01-resman/outputs.tf index 30a74a752..b2d89e81f 100644 --- a/fast/stages/01-resman/outputs.tf +++ b/fast/stages/01-resman/outputs.tf @@ -52,9 +52,11 @@ locals { for k, v in local.cicd_repositories : k => templatefile( "${path.module}/templates/workflow-${v.type}.yaml", merge(local.cicd_workflow_attrs[k], { - identity_provider = local.identity_providers[v.identity_provider].name - outputs_bucket = var.automation.outputs_bucket - stage_name = k + identity_provider = try( + local.identity_providers[v.identity_provider].name, null + ) + outputs_bucket = var.automation.outputs_bucket + stage_name = k }) ) } @@ -158,9 +160,11 @@ output "cicd_repositories" { description = "WIF configuration for CI/CD repositories." value = { for k, v in local.cicd_repositories : k => { - branch = v.branch - name = v.name - provider = local.identity_providers[v.identity_provider].name + branch = v.branch + name = v.name + provider = try( + local.identity_providers[v.identity_provider].name, null + ) service_account = local.cicd_workflow_attrs[k].service_account } if v != null } diff --git a/fast/stages/01-resman/variables.tf b/fast/stages/01-resman/variables.tf index 0f2bc5b9a..c1d534bca 100644 --- a/fast/stages/01-resman/variables.tf +++ b/fast/stages/01-resman/variables.tf @@ -23,6 +23,7 @@ variable "automation" { type = object({ outputs_bucket = string project_id = string + project_number = string federated_identity_pool = string federated_identity_providers = map(object({ issuer = string @@ -197,13 +198,4 @@ variable "team_folders" { impersonation_groups = list(string) })) default = null - # default = { - # team-a = { - # descriptive_name = "Team A" - # group_iam = { - # team-a-group@example.com = ["roles/owner", "roles/resourcemanager.projectCreator"] - # } - # impersonation_groups = ["team-a-admins@example.com"] - # } - # } } diff --git a/fast/stages/02-networking-nva/README.md b/fast/stages/02-networking-nva/README.md index fd9b2831f..ed3c8309a 100644 --- a/fast/stages/02-networking-nva/README.md +++ b/fast/stages/02-networking-nva/README.md @@ -225,8 +225,8 @@ If you have set a valid value for `outputs_location` in the bootstrap and in the ```bash # `outputs_location` is set to `~/fast-config` -ln -s ../../configs/example/02-networking/terraform-bootstrap.auto.tfvars.json -ln -s ../../configs/example/02-networking/terraform-resman.auto.tfvars.json +ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . # also copy the tfvars file used for the bootstrap stage cp ../00-bootstrap/terraform.tfvars . ``` diff --git a/fast/stages/02-networking-peering/README.md b/fast/stages/02-networking-peering/README.md index 316caf7e9..a4beab1ad 100644 --- a/fast/stages/02-networking-peering/README.md +++ b/fast/stages/02-networking-peering/README.md @@ -155,8 +155,8 @@ If you have set a valid value for `outputs_location` in the bootstrap and in the ```bash # `outputs_location` is set to `~/fast-config` -ln -s ../../configs/example/02-networking/terraform-bootstrap.auto.tfvars.json -ln -s ../../configs/example/02-networking/terraform-resman.auto.tfvars.json +ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . +ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . # also copy the tfvars file used for the bootstrap stage cp ../00-bootstrap/terraform.tfvars . ``` diff --git a/fast/stages/03-data-platform/dev/README.md b/fast/stages/03-data-platform/dev/README.md index 7bc41760f..9174ae6e9 100644 --- a/fast/stages/03-data-platform/dev/README.md +++ b/fast/stages/03-data-platform/dev/README.md @@ -108,6 +108,14 @@ If you're running this on top of Fast, you should run the following commands to ln -s ~/fast-config/providers/03-data-platform-dev-providers.tf . ``` +If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: + +```bash +cd ../../01-resman +terraform output -json providers | jq -r '.["03-data-platform-dev"]' \ + > ../03-data-platform/dev/providers.tf +``` + ### Variable configuration There are two broad sets of variables that can be configured: @@ -124,6 +132,8 @@ If you configured a valid path for `outputs_location` in the bootstrap security ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . ln -s ~/fast-config/tfvars/01-resman.auto.tfvars.json . ln -s ~/fast-config/tfvars/02-networking.auto.tfvars.json . +# also copy the tfvars file used for the bootstrap stage +cp ../../00-bootstrap/terraform.tfvars . ``` If you're not using FAST or its output files, refer to the [Variables](#variables) table at the bottom of this document for a full list of variables, their origin (e.g., a stage or specific to this one), and descriptions explaining their meaning. @@ -146,43 +156,44 @@ You can find examples in the `[demo](../../../../examples/data-solutions/data-pl ## Files -| name | description | modules | -|---|---|---| -| [main.tf](./main.tf) | Data Platformy. | data-platform-foundations | -| [outputs.tf](./outputs.tf) | Output variables. | | -| [variables.tf](./variables.tf) | Terraform Variables. | | +| name | description | modules | resources | +|---|---|---|---| +| [main.tf](./main.tf) | Data Platformy. | data-platform-foundations | | +| [outputs.tf](./outputs.tf) | Output variables. | | google_storage_bucket_object · local_file | +| [variables.tf](./variables.tf) | Terraform Variables. | | | ## Variables | name | description | type | required | default | producer | |---|---|:---:|:---:|:---:|:---:| -| [billing_account](variables.tf#L17) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-globals | -| [folder_ids](variables.tf#L56) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 01-resman | -| [host_project_ids](variables.tf#L74) | Shared VPC project ids. | object({…}) | ✓ | | 02-networking | -| [organization](variables.tf#L100) | Organization details. | object({…}) | ✓ | | 00-globals | -| [prefix](variables.tf#L116) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | -| [composer_config](variables.tf#L26) | | object({…}) | | {…} | | -| [data_catalog_tags](variables.tf#L39) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | -| [data_force_destroy](variables.tf#L50) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | -| [groups](variables.tf#L64) | Groups. | map(string) | | {…} | | -| [network_config_composer](variables.tf#L82) | Network configurations to use for Composer. | object({…}) | | {…} | | -| [outputs_location](variables.tf#L110) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | -| [project_services](variables.tf#L122) | List of core services enabled on all projects. | list(string) | | […] | | -| [region](variables.tf#L133) | Region used for regional resources. | string | | "europe-west1" | | -| [service_encryption_keys](variables.tf#L139) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | -| [subnet_self_links](variables.tf#L151) | Shared VPC subnet self links. | object({…}) | | null | 02-networking | -| [vpc_self_links](variables.tf#L160) | Shared VPC self links. | object({…}) | | null | 02-networking | +| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | +| [billing_account](variables.tf#L25) | Billing account id and organization id ('nnnnnnnn' or null). | object({…}) | ✓ | | 00-globals | +| [folder_ids](variables.tf#L64) | Folder to be used for the networking resources in folders/nnnn format. | object({…}) | ✓ | | 01-resman | +| [host_project_ids](variables.tf#L82) | Shared VPC project ids. | object({…}) | ✓ | | 02-networking | +| [organization](variables.tf#L108) | Organization details. | object({…}) | ✓ | | 00-globals | +| [prefix](variables.tf#L124) | Unique prefix used for resource names. Not used for projects if 'project_create' is null. | string | ✓ | | 00-globals | +| [composer_config](variables.tf#L34) | | object({…}) | | {…} | | +| [data_catalog_tags](variables.tf#L47) | List of Data Catalog Policy tags to be created with optional IAM binging configuration in {tag => {ROLE => [MEMBERS]}} format. | map(map(list(string))) | | {…} | | +| [data_force_destroy](variables.tf#L58) | Flag to set 'force_destroy' on data services like BigQery or Cloud Storage. | bool | | false | | +| [groups](variables.tf#L72) | Groups. | map(string) | | {…} | | +| [network_config_composer](variables.tf#L90) | Network configurations to use for Composer. | object({…}) | | {…} | | +| [outputs_location](variables.tf#L118) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | | +| [project_services](variables.tf#L130) | List of core services enabled on all projects. | list(string) | | […] | | +| [region](variables.tf#L141) | Region used for regional resources. | string | | "europe-west1" | | +| [service_encryption_keys](variables.tf#L147) | Cloud KMS to use to encrypt different services. Key location should match service region. | object({…}) | | null | | +| [subnet_self_links](variables.tf#L159) | Shared VPC subnet self links. | object({…}) | | null | 02-networking | +| [vpc_self_links](variables.tf#L168) | Shared VPC self links. | object({…}) | | null | 02-networking | ## Outputs | name | description | sensitive | consumers | |---|---|:---:|---| -| [bigquery_datasets](outputs.tf#L17) | BigQuery datasets. | | | -| [demo_commands](outputs.tf#L47) | Demo commands. | | | -| [gcs_buckets](outputs.tf#L22) | GCS buckets. | | | -| [kms_keys](outputs.tf#L27) | Cloud MKS keys. | | | -| [projects](outputs.tf#L32) | GCP Projects informations. | | | -| [vpc_network](outputs.tf#L37) | VPC network. | | | -| [vpc_subnet](outputs.tf#L42) | VPC subnetworks. | | | +| [bigquery_datasets](outputs.tf#L42) | BigQuery datasets. | | | +| [demo_commands](outputs.tf#L72) | Demo commands. | | | +| [gcs_buckets](outputs.tf#L47) | GCS buckets. | | | +| [kms_keys](outputs.tf#L52) | Cloud MKS keys. | | | +| [projects](outputs.tf#L57) | GCP Projects informations. | | | +| [vpc_network](outputs.tf#L62) | VPC network. | | | +| [vpc_subnet](outputs.tf#L67) | VPC subnetworks. | | | diff --git a/fast/stages/03-data-platform/dev/outputs.tf b/fast/stages/03-data-platform/dev/outputs.tf index 0820b64f6..346bf16b0 100644 --- a/fast/stages/03-data-platform/dev/outputs.tf +++ b/fast/stages/03-data-platform/dev/outputs.tf @@ -14,6 +14,31 @@ # tfdoc:file:description Output variables. +locals { + tfvars = { + bigquery_dataset = module.data-platform.bigquery-datasets + gcs_buckets = module.data-platform.gcs-buckets + projects = module.data-platform.projects + } +} + +# generate tfvars file for subsequent stages + +resource "local_file" "tfvars" { + for_each = var.outputs_location == null ? {} : { 1 = 1 } + file_permission = "0644" + filename = "${pathexpand(var.outputs_location)}/tfvars/03-data-platform-dev.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +resource "google_storage_bucket_object" "tfvars" { + bucket = var.automation.outputs_bucket + name = "tfvars/03-data-platform-dev.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +# outputs + output "bigquery_datasets" { description = "BigQuery datasets." value = module.data-platform.bigquery-datasets diff --git a/fast/stages/03-data-platform/dev/variables.tf b/fast/stages/03-data-platform/dev/variables.tf index 3b1645e41..9939d1940 100644 --- a/fast/stages/03-data-platform/dev/variables.tf +++ b/fast/stages/03-data-platform/dev/variables.tf @@ -14,6 +14,14 @@ # tfdoc:file:description Terraform Variables. +variable "automation" { + # tfdoc:variable:source 00-bootstrap + description = "Automation resources created by the bootstrap stage." + type = object({ + outputs_bucket = string + }) +} + variable "billing_account" { # tfdoc:variable:source 00-globals description = "Billing account id and organization id ('nnnnnnnn' or null)." diff --git a/fast/stages/03-project-factory/dev/main.tf b/fast/stages/03-project-factory/dev/main.tf index 802bff9db..120b83590 100644 --- a/fast/stages/03-project-factory/dev/main.tf +++ b/fast/stages/03-project-factory/dev/main.tf @@ -50,7 +50,7 @@ module "projects" { prefix = var.prefix service_accounts = try(each.value.service_accounts, {}) services = try(each.value.services, []) - service_identities_iam = try(each.value.services_iam, {}) + service_identities_iam = try(each.value.service_identities_iam, {}) vpc = try(each.value.vpc, null) } diff --git a/fast/stages/CLEANUP.md b/fast/stages/CLEANUP.md new file mode 100644 index 000000000..a0a1688fe --- /dev/null +++ b/fast/stages/CLEANUP.md @@ -0,0 +1,110 @@ +# FAST deployment clean up +In case you require destroying a previous FAST deployment in your organization, follow these steps. + +Destruction must be done in reverse order, from stage 3 to stage 0: + +## Stage 3 (Project Factory) + +```bash +cd $FAST_PWD/03-project-factory/prod/ +terraform destroy +``` + +## Stage 3 (GKE) +Terraform refuses to delete non-empty GCS buckets and/or BigQuery datasets, so they need to be removed manually from tf state + +```bash +cd $FAST_PWD/03-project-factory/prod/ + +# remove BQ dataset manually +for x in $(terraform state list | grep google_bigquery_dataset); do + terraform state rm "$x"; +done + +terraform destroy +``` + + +## Stage 2 (Security) +```bash +cd $FAST_PWD/02-security/ +terraform destroy +``` + +## Stage 2 (Networking) +```bash +cd $FAST_PWD/02-networking-XXX/ +terraform destroy +``` + +There's a minor glitch that can surface running terraform destroy, where the service project attachments to the Shared VPC will not get destroyed even with the relevant API call succeeding. We are investigating the issue, in the meantime just manually remove the attachment in the Cloud console or via the ```gcloud beta compute shared-vpc associated-projects remove``` [command](https://cloud.google.com/sdk/gcloud/reference/beta/compute/shared-vpc/associated-projects/remove) when terraform destroy fails, and then relaunch the command. + +## Stage 1 (Resource Management) +Stage 1 is a little more complicated because of the GCS Buckets. By default terraform refuses to delete non-empty buckets, which is a good thing for your terraform state. However, it makes destruction a bit harder + +```bash +cd $FAST_PWD/01-resman/ + +# remove buckets from state since terraform refuses to delete them +for x in $(terraform state list | grep google_storage_bucket.bucket); do + terraform state rm "$x" +done + +terraform destroy +``` + +## Stage 0 (Bootstrap) +**You should follow these steps carefully because we can end up destroying our own permissions. As we will be removing gcp-admins group roles, where your user belongs to, you will be required to grant organization admin role again** + +We also have to remove several resources (GCS buckets and BQ datasets) manually. + +```bash +cd $FAST_PWD/00-bootstrap/ + +# remove provider config to execute without SA impersonation +rm 00-bootstrap-providers.tf + +# migrate to local state +terraform init -migrate-state + +# remove GCS buckets and BQ dataset manually +for x in $(terraform state list | grep google_storage_bucket.bucket); do + terraform state rm "$x"; +done + +for x in $(terraform state list | grep google_bigquery_dataset); do + terraform state rm "$x"; +done + +terraform destroy + +# when this fails continue with the steps below +# make your user (the one you are using to execute this step) org admin again, as we will remove organization-admins group roles + +# Add the Organization Admin role to $BU_USER in the GCP Console + +# grant yourself this permission so you can finish the destruction +export FAST_DESTROY_ROLES="roles/billing.admin roles/logging.admin \ + roles/iam.organizationRoleAdmin roles/resourcemanager.projectDeleter \ + roles/resourcemanager.folderAdmin roles/owner" + +export FAST_BU=$(gcloud config list --format 'value(core.account)') + +# find your org id +gcloud organizations list --filter display_name:[part of your domain] + +# set your org id +export FAST_ORG_ID=XXXX + +for role in $FAST_DESTROY_ROLES; do + gcloud organizations add-iam-policy-binding $FAST_ORG_ID \ + --member user:$FAST_BU --role $role +done + +terraform destroy +rm -i terraform.tfstate* +``` + +In case you are willing to deploy FAST stages again, the following changes shall be done before: +* Modify the [prefix](00-bootstrap/variables.tf) variable to allow the deployment of resources that need unique names (eg, projects). +* Modify the [custom_roles](00-bootstrap/variables.tf) variable to allow recently deleted custom roles to be created again. \ No newline at end of file diff --git a/fast/stages/README.md b/fast/stages/README.md index 8b0814280..d7951ba79 100644 --- a/fast/stages/README.md +++ b/fast/stages/README.md @@ -17,6 +17,8 @@ To achieve this, we rely on specific GCP functionality like [delegated role gran Refer to each stage's documentation for a detailed description of its purpose, the architectural choices made in its design, and how it can be configured and wired together to terraform a whole GCP organization. The following is a brief overview of each stage. +To destroy a previous FAST deployment follow the instructions detailed in [cleanup](CLEANUP.md). + ## Organizational level (00-01) - [Bootstrap](00-bootstrap/README.md) diff --git a/modules/binauthz/README.md b/modules/binauthz/README.md new file mode 100644 index 000000000..b773ceb6d --- /dev/null +++ b/modules/binauthz/README.md @@ -0,0 +1,79 @@ +# Google Cloud Artifact Registry Module + +This module simplifies the creation of a Binary Authorization policy, attestors and attestor IAM bindings. + +## Example + +### Binary Athorization + +```hcl +module "binauthz" { + source = "./modules/binauthz" + project_id = "my_project" + global_policy_evaluation_mode = "DISABLE" + default_admission_rule = { + evaluation_mode = "ALWAYS_DENY" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = null + } + cluster_admission_rules = { + "europe-west1-c.cluster" = { + evaluation_mode = "REQUIRE_ATTESTATION" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = [ "test" ] + } + } + attestors_config = { + "test": { + note_reference = null + pgp_public_keys = [ + < + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [project_id](variables.tf#L17) | Project ID. | string | ✓ | | +| [admission_whitelist_patterns](variables.tf#L28) | An image name pattern to allowlist | list(string) | | null | +| [attestors_config](variables.tf#L58) | Attestors configuration | map(object({…})) | | null | +| [cluster_admission_rules](variables.tf#L48) | Admission rules | map(object({…})) | | null | +| [default_admission_rule](variables.tf#L34) | Default admission rule | object({…}) | | {…} | +| [global_policy_evaluation_mode](variables.tf#L22) | Global policy evaluation mode. | string | | null | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [attestors](outputs.tf#L22) | Attestors. | | +| [id](outputs.tf#L17) | Binary Authorization policy ID | | +| [notes](outputs.tf#L30) | Notes. | | + + diff --git a/modules/binauthz/main.tf b/modules/binauthz/main.tf new file mode 100644 index 000000000..2c1af463a --- /dev/null +++ b/modules/binauthz/main.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2022 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. + */ + +resource "google_binary_authorization_policy" "policy" { + project = var.project_id + dynamic "admission_whitelist_patterns" { + for_each = toset(coalesce(var.admission_whitelist_patterns, [])) + content { + name_pattern = admission_whitelist_patterns.value + } + } + default_admission_rule { + evaluation_mode = var.default_admission_rule.evaluation_mode + enforcement_mode = var.default_admission_rule.enforcement_mode + require_attestations_by = [for attestor in coalesce(var.default_admission_rule.attestors, []) : google_binary_authorization_attestor.attestors[attestor].name] + } + dynamic "cluster_admission_rules" { + for_each = coalesce(var.cluster_admission_rules, {}) + content { + cluster = cluster_admission_rules.key + evaluation_mode = cluster_admission_rules.value.evaluation_mode + enforcement_mode = cluster_admission_rules.value.enforcement_mode + require_attestations_by = [for attestor in cluster_admission_rules.value.attestors : google_binary_authorization_attestor.attestors[attestor].name] + } + } +} + +resource "google_binary_authorization_attestor" "attestors" { + for_each = coalesce(var.attestors_config, {}) + name = each.key + project = var.project_id + attestation_authority_note { + note_reference = each.value.note_reference == null ? google_container_analysis_note.notes[each.key].name : each.value.note_reference + dynamic "public_keys" { + for_each = coalesce(each.value.pgp_public_keys, []) + content { + ascii_armored_pgp_public_key = public_keys.value + } + } + dynamic "public_keys" { + for_each = { + for pkix_public_key in coalesce(each.value.pkix_public_keys, []) : + "${pkix_public_key.public_key_pem}-${pkix_public_key.signature_algorithm}" => pkix_public_key + } + content { + id = public_keys.value.id + pkix_public_key { + public_key_pem = public_keys.value.public_key_pem + signature_algorithm = public_keys.value.signature_algorithm + } + } + } + } +} + +resource "google_binary_authorization_attestor_iam_binding" "bindings" { + for_each = merge(flatten([ + for name, attestor_config in var.attestors_config : { for role, members in coalesce(attestor_config.iam, {}) : "${name}-${role}" => { + name = name + role = role + members = members + } }])...) + project = google_binary_authorization_attestor.attestors[each.value.name].project + attestor = google_binary_authorization_attestor.attestors[each.value.name].name + role = each.value.role + members = each.value.members +} + +resource "google_container_analysis_note" "notes" { + for_each = toset([for name, attestor_config in var.attestors_config : name if attestor_config.note_reference == null]) + name = "${each.value}-note" + project = var.project_id + attestation_authority { + hint { + human_readable_name = "Attestor ${each.value} note" + } + } +} diff --git a/modules/binauthz/outputs.tf b/modules/binauthz/outputs.tf new file mode 100644 index 000000000..19fac8368 --- /dev/null +++ b/modules/binauthz/outputs.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2022 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. + */ + +output "id" { + description = "Binary Authorization policy ID" + value = google_binary_authorization_policy.policy.id +} + +output "attestors" { + description = "Attestors." + value = google_binary_authorization_attestor.attestors + depends_on = [ + google_binary_authorization_attestor_iam_binding.bindings + ] +} + +output "notes" { + description = "Notes." + value = google_container_analysis_note.notes +} diff --git a/modules/binauthz/variables.tf b/modules/binauthz/variables.tf new file mode 100644 index 000000000..f9502a69b --- /dev/null +++ b/modules/binauthz/variables.tf @@ -0,0 +1,71 @@ +/** + * Copyright 2022 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. + */ + +variable "project_id" { + description = "Project ID." + type = string +} + +variable "global_policy_evaluation_mode" { + description = "Global policy evaluation mode." + type = string + default = null +} + +variable "admission_whitelist_patterns" { + description = "An image name pattern to allowlist" + type = list(string) + default = null +} + +variable "default_admission_rule" { + description = "Default admission rule" + type = object({ + evaluation_mode = string + enforcement_mode = string + attestors = list(string) + }) + default = { + evaluation_mode = "ALWAYS_ALLOW" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = null + } +} + +variable "cluster_admission_rules" { + description = "Admission rules" + type = map(object({ + evaluation_mode = string + enforcement_mode = string + attestors = list(string) + })) + default = null +} + +variable "attestors_config" { + description = "Attestors configuration" + type = map(object({ + note_reference = string + iam = map(list(string)) + pgp_public_keys = list(string) + pkix_public_keys = list(object({ + id = string + public_key_pem = string + signature_algorithm = string + })) + })) + default = null +} diff --git a/modules/binauthz/versions.tf b/modules/binauthz/versions.tf new file mode 100644 index 000000000..b2efbeadf --- /dev/null +++ b/modules/binauthz/versions.tf @@ -0,0 +1,29 @@ +# Copyright 2022 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 +# +# https://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. + +terraform { + required_version = ">= 1.1.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.17.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.17.0" + } + } +} + + diff --git a/modules/cloud-function/README.md b/modules/cloud-function/README.md index 6eac68bf1..73a1d3f2a 100644 --- a/modules/cloud-function/README.md +++ b/modules/cloud-function/README.md @@ -173,11 +173,12 @@ module "cf-http" { | [labels](variables.tf#L82) | Resource labels. | map(string) | | {} | | [prefix](variables.tf#L93) | Optional prefix used for resource names. | string | | null | | [region](variables.tf#L104) | Region used for all resources. | string | | "europe-west1" | -| [service_account](variables.tf#L110) | Service account email. Unused if service account is auto-created. | string | | null | -| [service_account_create](variables.tf#L116) | Auto-create service account. | bool | | false | -| [trigger_config](variables.tf#L122) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | -| [vpc_connector](variables.tf#L132) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | -| [vpc_connector_config](variables.tf#L142) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | +| [secrets](variables.tf#L110) | Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format. | map(object({…})) | | {} | +| [service_account](variables.tf#L122) | Service account email. Unused if service account is auto-created. | string | | null | +| [service_account_create](variables.tf#L128) | Auto-create service account. | bool | | false | +| [trigger_config](variables.tf#L134) | Function trigger configuration. Leave null for HTTP trigger. | object({…}) | | null | +| [vpc_connector](variables.tf#L144) | VPC connector configuration. Set create to 'true' if a new connector needs to be created. | object({…}) | | null | +| [vpc_connector_config](variables.tf#L154) | VPC connector network configuration. Must be provided if new VPC connector is being created. | object({…}) | | null | ## Outputs diff --git a/modules/cloud-function/main.tf b/modules/cloud-function/main.tf index 949cb69b1..0a26c1205 100644 --- a/modules/cloud-function/main.tf +++ b/modules/cloud-function/main.tf @@ -91,6 +91,35 @@ resource "google_cloudfunctions_function" "function" { } } + dynamic "secret_environment_variables" { + for_each = { for k, v in var.secrets : k => v if !v.is_volume } + iterator = secret + content { + key = secret.key + project_id = secret.value.project_id + secret = secret.value.secret + version = try(secret.value.versions.0, "latest") + } + } + + dynamic "secret_volumes" { + for_each = { for k, v in var.secrets : k => v if v.is_volume } + iterator = secret + content { + mount_path = secret.key + project_id = secret.value.project_id + secret = secret.value.secret + dynamic "versions" { + for_each = secret.value.versions + iterator = version + content { + path = split(":", version)[1] + version = split(":", version)[0] + } + } + } + } + } resource "google_cloudfunctions_function_iam_binding" "default" { diff --git a/modules/cloud-function/variables.tf b/modules/cloud-function/variables.tf index a613b2f68..ce8633c8f 100644 --- a/modules/cloud-function/variables.tf +++ b/modules/cloud-function/variables.tf @@ -107,6 +107,18 @@ variable "region" { default = "europe-west1" } +variable "secrets" { + description = "Secret Manager secrets. Key is the variable name or mountpoint, volume versions are in version:path format." + type = map(object({ + is_volume = bool + project_id = number + secret = string + versions = list(string) + })) + nullable = false + default = {} +} + variable "service_account" { description = "Service account email. Unused if service account is auto-created." type = string diff --git a/modules/folder/README.md b/modules/folder/README.md index 2e5b8b5aa..38670c758 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -141,7 +141,7 @@ module "folder-sink" { logging_sinks = { warnings = { type = "storage" - destination = module.gcs.name + destination = module.gcs.id filter = "severity=WARNING" include_children = true exclusions = {} diff --git a/modules/gcs/README.md b/modules/gcs/README.md index 3b8c52dbe..669009e04 100644 --- a/modules/gcs/README.md +++ b/modules/gcs/README.md @@ -135,9 +135,10 @@ module "bucket-gcs-notification" { | name | description | sensitive | |---|---|:---:| | [bucket](outputs.tf#L17) | Bucket resource. | | -| [name](outputs.tf#L22) | Bucket name. | | -| [notification](outputs.tf#L30) | GCS Notification self link. | | -| [topic](outputs.tf#L34) | Topic ID used by GCS. | | -| [url](outputs.tf#L38) | Bucket URL. | | +| [id](outputs.tf#L28) | Bucket ID (same as name). | | +| [name](outputs.tf#L37) | Bucket name. | | +| [notification](outputs.tf#L46) | GCS Notification self link. | | +| [topic](outputs.tf#L51) | Topic ID used by GCS. | | +| [url](outputs.tf#L56) | Bucket URL. | | diff --git a/modules/gcs/outputs.tf b/modules/gcs/outputs.tf index 3e1ca8746..a00c04cf7 100644 --- a/modules/gcs/outputs.tf +++ b/modules/gcs/outputs.tf @@ -19,6 +19,21 @@ output "bucket" { value = google_storage_bucket.bucket } +# We add `id` as an alias to `name` to simplify log sink handling. +# Since all other log destinations (pubsub, logging-bucket, bigquery) +# have an id output, it is convenient to have in this module too to +# handle all log destination as homogeneous objects (i.e. you can +# assume any valid log destination has an `id` output). + +output "id" { + description = "Bucket ID (same as name)." + value = "${local.prefix}${lower(var.name)}" + depends_on = [ + google_storage_bucket.bucket, + google_storage_bucket_iam_binding.bindings + ] +} + output "name" { description = "Bucket name." value = "${local.prefix}${lower(var.name)}" @@ -27,14 +42,17 @@ output "name" { google_storage_bucket_iam_binding.bindings ] } + output "notification" { description = "GCS Notification self link." value = local.notification ? google_storage_notification.notification[0].self_link : null } + output "topic" { description = "Topic ID used by GCS." value = local.notification ? google_pubsub_topic.topic[0].id : null } + output "url" { description = "Bucket URL." value = google_storage_bucket.bucket.url diff --git a/modules/iam-service-account/README.md b/modules/iam-service-account/README.md index ab6b1882c..ad39c389e 100644 --- a/modules/iam-service-account/README.md +++ b/modules/iam-service-account/README.md @@ -2,6 +2,8 @@ This module allows simplified creation and management of one a service account and its IAM bindings. A key can optionally be generated and will be stored in Terraform state. To use it create a sensitive output in your root modules referencing the `key` output, then extract the private key from the JSON formatted outputs. Alternatively, the `key` can be generated with `openssl` library and only public part uploaded to the Service Account, for more refer to the [Onprem SA Key Management](../../examples/cloud-operations/onprem-sa-key-management/) example. +Note that this module does not fully comply with our design principles, as outputs have no dependencies on IAM bindings to prevent resource cycles. + ## Example ```hcl @@ -64,9 +66,9 @@ module "myproject-default-service-accounts" { | [email](outputs.tf#L17) | Service account email. | | | [iam_email](outputs.tf#L25) | IAM-format service account email. | | | [id](outputs.tf#L33) | Service account id. | | -| [key](outputs.tf#L38) | Service account key. | ✓ | -| [name](outputs.tf#L44) | Service account name. | | -| [service_account](outputs.tf#L49) | Service account resource. | | -| [service_account_credentials](outputs.tf#L54) | Service account json credential templates for uploaded public keys data. | | +| [key](outputs.tf#L41) | Service account key. | ✓ | +| [name](outputs.tf#L47) | Service account name. | | +| [service_account](outputs.tf#L52) | Service account resource. | | +| [service_account_credentials](outputs.tf#L57) | Service account json credential templates for uploaded public keys data. | | diff --git a/modules/iam-service-account/outputs.tf b/modules/iam-service-account/outputs.tf index 4f0e0aa52..42196534c 100644 --- a/modules/iam-service-account/outputs.tf +++ b/modules/iam-service-account/outputs.tf @@ -33,6 +33,9 @@ output "iam_email" { output "id" { description = "Service account id." value = local.service_account.id + depends_on = [ + local.service_account + ] } output "key" { diff --git a/modules/organization/README.md b/modules/organization/README.md index 7aee01518..0fc50c89e 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -179,7 +179,7 @@ module "org" { logging_sinks = { warnings = { type = "storage" - destination = module.gcs.name + destination = module.gcs.id filter = "severity=WARNING" include_children = true bq_partitioned_table = null diff --git a/modules/project/README.md b/modules/project/README.md index 687a1f08f..a62b58f61 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -221,7 +221,7 @@ module "project-host" { logging_sinks = { warnings = { type = "storage" - destination = module.gcs.name + destination = module.gcs.id filter = "severity=WARNING" iam = false unique_writer = false diff --git a/tests/examples/cloud_operations/binauthz/__init__.py b/tests/examples/cloud_operations/binauthz/__init__.py new file mode 100644 index 000000000..6d6d1266c --- /dev/null +++ b/tests/examples/cloud_operations/binauthz/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 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. diff --git a/tests/examples/cloud_operations/binauthz/fixture/main.tf b/tests/examples/cloud_operations/binauthz/fixture/main.tf new file mode 100644 index 000000000..5871ca851 --- /dev/null +++ b/tests/examples/cloud_operations/binauthz/fixture/main.tf @@ -0,0 +1,21 @@ +/** + * Copyright 2022 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. + */ + +module "test" { + source = "../../../../../examples/cloud-operations/binauthz" + project_create = var.project_create + project_id = var.project_id +} diff --git a/tests/examples/cloud_operations/binauthz/fixture/variables.tf b/tests/examples/cloud_operations/binauthz/fixture/variables.tf new file mode 100644 index 000000000..439d6b0b4 --- /dev/null +++ b/tests/examples/cloud_operations/binauthz/fixture/variables.tf @@ -0,0 +1,26 @@ +# Copyright 2022 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 +# +# https://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. + +variable "project_create" { + type = object({ + billing_account_id = string + parent = string + }) + default = null +} + +variable "project_id" { + type = string + default = "my-project" +} diff --git a/tests/examples/cloud_operations/binauthz/test_plan.py b/tests/examples/cloud_operations/binauthz/test_plan.py new file mode 100644 index 000000000..6e176b1c0 --- /dev/null +++ b/tests/examples/cloud_operations/binauthz/test_plan.py @@ -0,0 +1,19 @@ +# Copyright 2022 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. + +def test_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner() + assert len(modules) == 13 + assert len(resources) == 42 diff --git a/tests/fast/stages/s01_resman/fixture/main.tf b/tests/fast/stages/s01_resman/fixture/main.tf index ddb9aafef..57d35b163 100644 --- a/tests/fast/stages/s01_resman/fixture/main.tf +++ b/tests/fast/stages/s01_resman/fixture/main.tf @@ -20,6 +20,7 @@ module "stage" { federated_identity_pool = null federated_identity_providers = null project_id = "fast-prod-automation" + project_number = 123456 outputs_bucket = "test" } billing_account = { diff --git a/tests/fast/stages/s03_data_platform/fixture/main.tf b/tests/fast/stages/s03_data_platform/fixture/main.tf index e7fd8d4d4..bf9be2fd2 100644 --- a/tests/fast/stages/s03_data_platform/fixture/main.tf +++ b/tests/fast/stages/s03_data_platform/fixture/main.tf @@ -18,6 +18,9 @@ module "stage" { source = "../../../../../fast/stages/03-data-platform/dev/" + automation = { + outputs_bucket = "test" + } billing_account = { id = "012345-67890A-BCDEF0", organization_id = 123456 diff --git a/tests/modules/binauthz/__init__.py b/tests/modules/binauthz/__init__.py new file mode 100644 index 000000000..6d6d1266c --- /dev/null +++ b/tests/modules/binauthz/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 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. diff --git a/tests/modules/binauthz/fixture/main.tf b/tests/modules/binauthz/fixture/main.tf new file mode 100644 index 000000000..95f76d634 --- /dev/null +++ b/tests/modules/binauthz/fixture/main.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2022 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. + */ + +module "test" { + source = "../../../../modules/binauthz" + project_id = var.project_id + global_policy_evaluation_mode = var.global_policy_evaluation_mode + default_admission_rule = var.default_admission_rule + attestors_config = var.attestors_config +} diff --git a/tests/modules/binauthz/fixture/variables.tf b/tests/modules/binauthz/fixture/variables.tf new file mode 100644 index 000000000..327ced252 --- /dev/null +++ b/tests/modules/binauthz/fixture/variables.tf @@ -0,0 +1,103 @@ +/** + * Copyright 2022 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. + */ + +variable "project_id" { + type = string + default = "my_project" +} + +variable "global_policy_evaluation_mode" { + type = string + default = null +} + +variable "admission_whitelist_patterns" { + type = list(string) + default = [ + "gcr.io/google_containers/*" + ] +} + +variable "default_admission_rule" { + type = object({ + evaluation_mode = string + enforcement_mode = string + attestors = list(string) + }) + default = { + evaluation_mode = "ALWAYS_ALLOW" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = null + } +} + +variable "cluster_admission_rules" { + type = map(object({ + evaluation_mode = string + enforcement_mode = string + attestors = list(string) + })) + default = { + "europe-west1-c.cluster" = { + evaluation_mode = "REQUIRE_ATTESTATION" + enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG" + attestors = ["test"] + } + } +} + +variable "attestors_config" { + description = "Attestors configuration" + type = map(object({ + note_reference = string + iam = map(list(string)) + pgp_public_keys = list(string) + pkix_public_keys = list(object({ + id = string + public_key_pem = string + signature_algorithm = string + })) + })) + default = { + "test" : { + note_reference = null + pgp_public_keys = [ + <= MOD_LIMITS[name.source] else '✓' - print(f'[{flag}] {name.source.ljust(source_just)} ' - f'{name.name.ljust(name_just)} ' - f'{name.value.ljust(value_just)} ' - f'({name_length})') + if name_length >= MOD_LIMITS[name.source]: + flag = "✗" + errors += [f"{name.source}:{name.name}:{name_length}"] + else: + flag = "✓" + + print(f"[{flag}] {name.source.ljust(source_just)} " + f"{name.name.ljust(name_just)} " + f"{name.value.ljust(value_just)} " + f"({name_length})") + if errors: + raise ValueError(errors) if __name__ == '__main__': diff --git a/tools/tfeditor/go.mod b/tools/tfeditor/go.mod new file mode 100644 index 000000000..d1a4a41e4 --- /dev/null +++ b/tools/tfeditor/go.mod @@ -0,0 +1,8 @@ +module github.com/GoogleCloudPlatform/cloud-foundation-fabric/tools/tfeditor + +go 1.16 + +require ( + github.com/hashicorp/hcl/v2 v2.12.0 // indirect + github.com/zclconf/go-cty v1.8.0 // indirect +) diff --git a/tools/tfeditor/go.sum b/tools/tfeditor/go.sum new file mode 100644 index 000000000..1ed29912f --- /dev/null +++ b/tools/tfeditor/go.sum @@ -0,0 +1,51 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4= +github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= +github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tools/tfeditor/main.go b/tools/tfeditor/main.go new file mode 100644 index 000000000..f8a4e0a97 --- /dev/null +++ b/tools/tfeditor/main.go @@ -0,0 +1,198 @@ +/* + Copyright 2022 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. + + This tool updates in-place versions.tf files to change module_name parameter + on an automated basis. In retains all existing comments and structures. +*/ +package main + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" +) + +type Provider struct { + Source string + Version string +} + +func main() { + var path string + var pattern string + var moduleName string + var terraformVersion string + var providerVersions string + var updateProviderVersions bool = false + var updateTerraformVersion bool = false + var updateModuleName bool = false + + flag.StringVar(&path, "path", "./", "Path to search for file pattern (default ./") + flag.StringVar(&pattern, "pattern", "versions.tf", "Pattern to search (default versions.tf)") + flag.StringVar(&moduleName, "module-name", "", "module_name attribute for provider_meta (can be templated with {{ .Module }})") + flag.StringVar(&providerVersions, "provider-versions", "", "set provider versions (usage: hashicorp/google>= 4.0.0,hashicorp/googlegoogle-beta== 4.0.0)") + flag.StringVar(&terraformVersion, "terraform-version", "", "Update terraform required_version") + flag.Parse() + + if !strings.HasSuffix(path, "/") { + path = fmt.Sprintf("%s/", path) + } + + if moduleName != "" { + updateModuleName = true + log.Printf("Updating module_name to: %s", moduleName) + } + + if terraformVersion != "" { + updateTerraformVersion = true + log.Printf("Updating Terraform version to: %s", terraformVersion) + } + + providerVersionsMap := map[string]Provider{} + if providerVersions != "" { + for _, v := range strings.Split(providerVersions, ",") { + re := regexp.MustCompile("([a-zA-Z_/-]+)(.+)") + split := re.FindAllStringSubmatch(v, -1) + for _, splitN := range split { + providerKey := filepath.Base(splitN[1]) + providerVersionsMap[providerKey] = Provider{Source: splitN[1], Version: splitN[2]} + log.Printf("Updating provider %s to: %s %s", providerKey, providerVersionsMap[providerKey].Source, providerVersionsMap[providerKey].Version) + } + } + updateProviderVersions = true + } + + log.Printf("Looking for files (%s) in: %s", pattern, path) + + var foundFiles []string + err := filepath.Walk(path, func(file string, f os.FileInfo, err error) error { + if isMatch, _ := filepath.Match(pattern, filepath.Base(file)); isMatch { + foundFiles = append(foundFiles, file) + } + return nil + }) + log.Printf("Found %d files.", len(foundFiles)) + + for _, foundFile := range foundFiles { + fileBytes, fErr := ioutil.ReadFile(foundFile) + if fErr != nil { + panic(fErr) + } + fileBasename := filepath.Base(foundFile) + + hclFile, diag := hclwrite.ParseConfig(fileBytes, fileBasename, hcl.Pos{}) + if diag == nil { + hclBody := hclFile.Body() + for _, block := range hclBody.Blocks() { + if block.Type() == "terraform" { + if updateTerraformVersion { + for k, _ := range block.Body().Attributes() { + if k == "required_version" { + block.Body().SetAttributeValue("required_version", cty.StringVal(terraformVersion)) + } + } + } + + hasProviderMetaForGoogle := false + hasProviderMetaForGoogleBeta := false + + // Expand template + tmpl, tErr := template.New("modulename").Parse(moduleName) + if tErr != nil { + panic(tErr) + } + expandedBuffer := new(bytes.Buffer) + tErr = tmpl.Execute(expandedBuffer, map[string]string{ + "Module": filepath.Base(filepath.Dir(foundFile)), + }) + if tErr != nil { + panic(tErr) + } + expandedModuleName := expandedBuffer.String() + + for _, tfBlock := range block.Body().Blocks() { + if tfBlock.Type() == "required_providers" && updateProviderVersions { + for k, _ := range tfBlock.Body().Attributes() { + if provider, ok := providerVersionsMap[k]; ok { + tfBlock.Body().SetAttributeValue(k, cty.ObjectVal(map[string]cty.Value{ + "source": cty.StringVal(provider.Source), + "version": cty.StringVal(provider.Version), + })) + } + } + } + + if tfBlock.Type() == "provider_meta" && updateModuleName { + labels := tfBlock.Labels() + if len(labels) > 0 { + if labels[0] == "google" { + hasProviderMetaForGoogle = true + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + } + if labels[0] == "google-beta" { + hasProviderMetaForGoogleBeta = true + tfBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + } + } + } + } + + if updateModuleName { + if !hasProviderMetaForGoogle { + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google"}) + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + block.Body().AppendBlock(providerMetaGoogleBlock) + } + if !hasProviderMetaForGoogleBeta { + providerMetaGoogleBlock := hclwrite.NewBlock("provider_meta", []string{"google-beta"}) + providerMetaGoogleBlock.Body().SetAttributeValue("module_name", cty.StringVal(expandedModuleName)) + block.Body().AppendBlock(providerMetaGoogleBlock) + } + } + } + } + + log.Printf("Updating: %s", foundFile) + info, sErr := os.Stat(foundFile) + if sErr != nil { + panic(sErr) + } + tempFilePath := fmt.Sprintf("%s.tmp", foundFile) + wErr := os.WriteFile(tempFilePath, hclFile.Bytes(), info.Mode()) + if wErr != nil { + panic(wErr) + } + os.Rename(tempFilePath, foundFile) + } else { + panic(diag) + } + } + + if err != nil { + panic(err) + } + log.Printf("All done.") +}