From 7867e0b28a0cac25ce2fc0c3731928affd092b03 Mon Sep 17 00:00:00 2001 From: Erik Kristensen Date: Wed, 15 May 2024 21:09:49 -0600 Subject: [PATCH] gcp-nuke be born :sunrise: --- .github/renovate.json | 85 +++++ .github/workflows/commit-lint.yml | 20 + .github/workflows/docs.yml | 60 +++ .github/workflows/golangci-lint.yml | 21 ++ .github/workflows/goreleaser.yml | 101 ++++++ .github/workflows/semantic-lint.yml | 20 + .github/workflows/semantic.yml | 39 ++ .github/workflows/tests.yml | 21 ++ .gitignore | 12 + .goreleaser.yml | 120 ++++++ .releaserc.yml | 13 + Dockerfile | 26 ++ LICENSE | 15 + Makefile | 8 + README.md | 110 ++++++ cosign.pub | 4 + docs/cli-experimental.md | 35 ++ docs/cli-options.md | 15 + docs/cli-usage.md | 52 +++ docs/config-filtering.md | 341 ++++++++++++++++++ docs/config-presets.md | 53 +++ docs/config.md | 128 +++++++ docs/contributing.md | 174 +++++++++ docs/development.md | 19 + docs/features/all-regions.md | 7 + docs/features/global-filters.md | 7 + docs/features/overview.md | 7 + docs/features/signed-binaries.md | 4 + docs/index.md | 57 +++ docs/installation.md | 32 ++ docs/quick-start.md | 136 +++++++ docs/releases.md | 13 + docs/resources.md | 170 +++++++++ docs/standards.md | 14 + docs/testing.md | 82 +++++ docs/warning.md | 27 ++ go.mod | 67 ++++ go.sum | 262 ++++++++++++++ main.go | 48 +++ mkdocs.yml | 92 +++++ pkg/commands/global/global.go | 67 ++++ pkg/commands/list/list.go | 57 +++ pkg/commands/run/command.go | 278 ++++++++++++++ pkg/common/commands.go | 24 ++ pkg/common/version.go | 32 ++ pkg/gcputil/client.go | 5 + pkg/gcputil/firebase.go | 130 +++++++ pkg/gcputil/gcp.go | 147 ++++++++ pkg/nuke/resource.go | 17 + resources/cloud-function.go | 144 ++++++++ resources/cloud-function2.go | 149 ++++++++ resources/cloud-run.go | 131 +++++++ resources/cloud-sql-instance.go | 156 ++++++++ resources/compute-common-instance-metadata.go | 130 +++++++ resources/compute-disk.go | 108 ++++++ resources/compute-firewall.go | 99 +++++ resources/compute-instance.go | 109 ++++++ resources/firebase-realtime-database.go | 172 +++++++++ resources/firebase-web-app.go | 85 +++++ resources/gke-cluster.go | 151 ++++++++ resources/iam-role.go | 110 ++++++ resources/iam-service-account.go | 128 +++++++ .../iam-workload-identity-pool-provider.go | 118 ++++++ resources/iam-workload-identity-pool.go | 111 ++++++ resources/kms-key.go | 133 +++++++ resources/secretmanager-secret.go | 108 ++++++ resources/storage-bucket-object.go | 102 ++++++ resources/storage-bucket.go | 117 ++++++ resources/vpc-network.go | 128 +++++++ resources/vpc-route.go | 112 ++++++ resources/vpc-router.go | 99 +++++ resources/vpc-subnet.go | 103 ++++++ tools/create-resource/main.go | 156 ++++++++ 73 files changed, 6233 insertions(+) create mode 100644 .github/renovate.json create mode 100644 .github/workflows/commit-lint.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/goreleaser.yml create mode 100644 .github/workflows/semantic-lint.yml create mode 100644 .github/workflows/semantic.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 .releaserc.yml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cosign.pub create mode 100644 docs/cli-experimental.md create mode 100644 docs/cli-options.md create mode 100644 docs/cli-usage.md create mode 100644 docs/config-filtering.md create mode 100644 docs/config-presets.md create mode 100644 docs/config.md create mode 100644 docs/contributing.md create mode 100644 docs/development.md create mode 100644 docs/features/all-regions.md create mode 100644 docs/features/global-filters.md create mode 100644 docs/features/overview.md create mode 100644 docs/features/signed-binaries.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/quick-start.md create mode 100644 docs/releases.md create mode 100644 docs/resources.md create mode 100644 docs/standards.md create mode 100644 docs/testing.md create mode 100644 docs/warning.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 mkdocs.yml create mode 100644 pkg/commands/global/global.go create mode 100644 pkg/commands/list/list.go create mode 100644 pkg/commands/run/command.go create mode 100644 pkg/common/commands.go create mode 100644 pkg/common/version.go create mode 100644 pkg/gcputil/client.go create mode 100644 pkg/gcputil/firebase.go create mode 100644 pkg/gcputil/gcp.go create mode 100644 pkg/nuke/resource.go create mode 100644 resources/cloud-function.go create mode 100644 resources/cloud-function2.go create mode 100644 resources/cloud-run.go create mode 100644 resources/cloud-sql-instance.go create mode 100644 resources/compute-common-instance-metadata.go create mode 100644 resources/compute-disk.go create mode 100644 resources/compute-firewall.go create mode 100644 resources/compute-instance.go create mode 100644 resources/firebase-realtime-database.go create mode 100644 resources/firebase-web-app.go create mode 100644 resources/gke-cluster.go create mode 100644 resources/iam-role.go create mode 100644 resources/iam-service-account.go create mode 100644 resources/iam-workload-identity-pool-provider.go create mode 100644 resources/iam-workload-identity-pool.go create mode 100644 resources/kms-key.go create mode 100644 resources/secretmanager-secret.go create mode 100644 resources/storage-bucket-object.go create mode 100644 resources/storage-bucket.go create mode 100644 resources/vpc-network.go create mode 100644 resources/vpc-route.go create mode 100644 resources/vpc-router.go create mode 100644 resources/vpc-subnet.go create mode 100644 tools/create-resource/main.go diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..24919ef --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,85 @@ +{ + "extends": [ + "config:recommended" + ], + "vulnerabilityAlerts": { + "labels": ["security"], + "automerge": true, + "assignees": ["@ekristen"] + }, + "osvVulnerabilityAlerts": true, + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch" + ], + "matchCurrentVersion": "!/^0/", + "automerge": true + }, + { + "matchDatasources": [ + "go", + "docker" + ], + "groupName": "kubernetes", + "groupSlug": "kubernetes", + "matchPackagePatterns": [ + "^k8s.io/" + ], + "matchPackageNames": [ + "bitnami/kubectl" + ] + }, + { + "matchManagers": [ + "dockerfile" + ], + "matchUpdateTypes": [ + "pin", + "digest" + ], + "automerge": true, + "labels": ["patch"] + }, + { + "matchPackagePatterns": [ + "^golang.*" + ], + "groupName": "golang", + "groupSlug": "golang" + }, + { + "matchFileNames": [ + ".github/workflows/*.yml" + ], + "matchDepTypes": [ + "action" + ], + "matchCurrentVersion": "!/^0/", + "automerge": true, + "labels": ["bot/skip-changelog"] + } + ], + "regexManagers": [ + { + "fileMatch": [ + ".*.go$" + ], + "matchStrings": [ + "\"(?.*)\" \/\/ renovate: datasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + }, + { + "fileMatch": [ + "^.github/workflows/.*" + ], + "matchStrings": [ + "go-version: (?.*?).x\n" + ], + "depNameTemplate": "golang", + "datasourceTemplate": "docker" + } + ] +} \ No newline at end of file diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 0000000..e83fcf5 --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,20 @@ +name: commit-lint + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + pull-requests: read + +jobs: + commit-lint: + name: commit-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: wagoid/commitlint-github-action@v6 \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f167586 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,60 @@ +name: docs + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - docs/** + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - name: setup pages + uses: actions/configure-pages@v4 + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: setup cache + run: | + echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - name: handle cache + uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - name: install mkdocs material + run: | + pip install mkdocs-material + - name: run mkdocs material + run: | + mkdocs build + - name: upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: public/ + - name: deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..58aad26 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,21 @@ +name: golangci-lint +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + golangci-lint: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.x' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 \ No newline at end of file diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml new file mode 100644 index 0000000..9c05dc6 --- /dev/null +++ b/.github/workflows/goreleaser.yml @@ -0,0 +1,101 @@ +name: goreleaser + +on: + workflow_dispatch: + push: + branches: + - main + - next + tags: + - "*" + release: + types: + - published + +permissions: + contents: write + packages: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + if: github.event_name == 'pull_request' + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/checkout@v4 + if: github.event_name != 'pull_request' + with: + fetch-depth: 0 + - name: setup-go + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + - name: setup qemu + id: qemu + uses: docker/setup-qemu-action@v3 + - name: setup docker buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: install cosign + uses: sigstore/cosign-installer@v3 + - name: install quill + env: + QUILL_VERSION: 0.4.1 + run: | + curl -Lo /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz https://github.com/anchore/quill/releases/download/v${QUILL_VERSION}/quill_${QUILL_VERSION}_linux_amd64.tar.gz + tar -xvf /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz -C /tmp + mv /tmp/quill /usr/local/bin/quill + chmod +x /usr/local/bin/quill + - name: set goreleaser default args + if: startsWith(github.ref, 'refs/tags/') == true + run: | + echo "GORELEASER_ARGS=" >> $GITHUB_ENV + - name: set goreleaser args for branch + if: startsWith(github.ref, 'refs/tags/') == false + run: | + echo "GORELEASER_ARGS=--snapshot" >> $GITHUB_ENV + - name: set goreleaser args renovate + if: startsWith(github.ref, 'refs/heads/renovate') == true + run: | + echo "GORELEASER_ARGS=--snapshot --skip publish --skip sign" >> $GITHUB_ENV + - name: setup-quill + uses: 1password/load-secrets-action@v1 + # Extra Safeguard - This ensures the secrets are only loaded on tag and a tag that the repo owner triggered + if: startsWith(github.ref, 'refs/tags/') == true && github.actor == github.repository_owner + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + QUILL_NOTARY_KEY: ${{ secrets.OP_QUILL_NOTARY_KEY }} + QUILL_NOTARY_KEY_ID: ${{ secrets.OP_QUILL_NOTARY_KEY_ID }} + QUILL_NOTARY_ISSUER: ${{ secrets.OP_QUILL_NOTARY_ISSUER }} + QUILL_SIGN_PASSWORD: ${{ secrets.OP_QUILL_SIGN_PASSWORD }} + QUILL_SIGN_P12: ${{ secrets.OP_QUILL_SIGN_P12 }} + - name: run goreleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: latest + args: release --clean ${{ env.GORELEASER_ARGS }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: push docker images (for branches) + if: github.ref == 'refs/heads/main' || github.event.pull_request.base.ref == 'main' + run: | + docker images --format "{{.Repository}}:{{.Tag}}" | grep "${{ github.repository }}" | xargs -L1 docker push + - name: upload artifacts + if: github.event.pull_request.base.ref == 'main' + uses: actions/upload-artifact@v4 + with: + name: binaries + path: releases/*.tar.gz diff --git a/.github/workflows/semantic-lint.yml b/.github/workflows/semantic-lint.yml new file mode 100644 index 0000000..85eb2ee --- /dev/null +++ b/.github/workflows/semantic-lint.yml @@ -0,0 +1,20 @@ +name: semantic-lint + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml new file mode 100644 index 0000000..7392a49 --- /dev/null +++ b/.github/workflows/semantic.yml @@ -0,0 +1,39 @@ +name: semantic +on: + push: + branches: + - main + - next + +permissions: + contents: read # for checkout + +jobs: + release: + name: release + runs-on: ubuntu-latest + permissions: + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: setup node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + - name: generate-token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.BOT2_APP_ID }} + private_key: ${{ secrets.BOT2_APP_PEM }} + revoke: true + - name: release + uses: cycjimmy/semantic-release-action@v4 + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6fb0b13 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: tests +on: + pull_request: + branches: + - main + - next +jobs: + test: + name: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: 1.22.x + - name: download go mods + run: | + go mod download + - name: run go tests + run: | + go test -timeout 60s -run ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7d9029 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.idea +dist +releases +.envrc +.envrc* +.token +.creds* +config/*.yaml +config.yaml +test-*.yaml +config*.yaml +cosign.key diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..fb5e4f1 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,120 @@ +release: + github: + owner: ekristen + name: gcp-nuke +env: + - REGISTRY=ghcr.io + - IMAGE=ekristen/gcp-nuke +builds: + - id: gcp-nuke + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + flags: + - -trimpath + ldflags: + - -s + - -w + - -extldflags="-static" + - -X '{{ .ModulePath }}/pkg/common.SUMMARY=v{{ .Version }}' + - -X '{{ .ModulePath }}/pkg/common.BRANCH={{ .Branch }}' + - -X '{{ .ModulePath }}/pkg/common.VERSION={{ .Tag }}' + - -X '{{ .ModulePath }}/pkg/common.COMMIT={{ .Commit }}' + mod_timestamp: '{{ .CommitTimestamp }}' + hooks: + post: + - cmd: | + {{- if eq .Os "darwin" -}} + quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv + {{- else -}} + true + {{- end -}} + env: + - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log +sboms: + - artifacts: archive +archives: + - id: gcp-nuke + builds: + - gcp-nuke + name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}" + format_overrides: + - goos: windows + format: zip +dockers: + - id: linux-amd64 + ids: + - gcp-nuke + use: buildx + goos: linux + goarch: amd64 + dockerfile: Dockerfile + image_templates: + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-amd64' + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-amd64-{{ .Timestamp }}' + build_flag_templates: + - "--platform=linux/amd64" + - "--target=goreleaser" + - "--pull" + - "--build-arg=PROJECT_NAME={{.ProjectName}}" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - '--label=org.opencontainers.image.source={{replace (replace (replace .GitURL "git@" "https://") ".git" "") "github.com:" "github.com/"}}' + + - id: linux-arm64 + ids: + - gcp-nuke + use: buildx + goos: linux + goarch: arm64 + dockerfile: Dockerfile + image_templates: + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-arm64' + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-arm64-{{ .Timestamp }}' + build_flag_templates: + - "--platform=linux/arm64" + - "--target=goreleaser" + - "--pull" + - "--build-arg=PROJECT_NAME={{.ProjectName}}" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - '--label=org.opencontainers.image.source={{replace (replace (replace .GitURL "git@" "https://") ".git" "") "github.com:" "github.com/"}}' +docker_manifests: + - name_template: '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}' + image_templates: + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-amd64' + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:v{{ .Version }}-arm64' + - name_template: '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-{{ .Timestamp }}' + image_templates: + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-amd64-{{ .Timestamp }}' + - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE }}:{{ replace .Branch "/" "-" }}-{{ .ShortCommit }}-arm64-{{ .Timestamp }}' +signs: + - ids: + - default + - darwin + cmd: cosign + signature: "${artifact}.sig" + certificate: "${artifact}.pem" + args: ["sign-blob", "--yes", "--oidc-provider=github", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"] + artifacts: all +docker_signs: + - ids: + - default + artifacts: all + cmd: cosign + args: ["sign", "--yes", "--oidc-provider=github", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"] +checksum: + name_template: "checksums.txt" +snapshot: + name_template: '{{ trimprefix .Summary "v" }}' +# We are skipping changelog because we are using semantic release +changelog: + skip: true \ No newline at end of file diff --git a/.releaserc.yml b/.releaserc.yml new file mode 100644 index 0000000..95e2dc9 --- /dev/null +++ b/.releaserc.yml @@ -0,0 +1,13 @@ +plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - "@semantic-release/github" +branches: + - name: +([0-9])?(.{+([0-9]),x}).x + - name: main + prerelease: true + channel: beta + - name: next + prerelease: true + - name: pre/rc + prerelease: '${name.replace(/^pre\\//g, "")}' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3e1e278 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1.7-labs +ARG PROJECT_NAME=gcp-nuke + +FROM cgr.dev/chainguard/wolfi-base:latest as base +ARG PROJECT_NAME +RUN apk add --no-cache ca-certificates +RUN addgroup -S ${PROJECT_NAME} && adduser -S ${PROJECT_NAME} -G ${PROJECT_NAME} + +FROM docker.io/library/golang:1.22 AS build +ARG PROJECT_NAME +COPY / /src +WORKDIR /src +RUN \ + --mount=type=cache,target=/go/pkg \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -o bin/${PROJECT_NAME} main.go + +FROM base AS goreleaser +ARG PROJECT_NAME +COPY ${PROJECT_NAME} /usr/local/bin/${PROJECT_NAME} +USER ${PROJECT_NAME} + +FROM base +ARG PROJECT_NAME +COPY --from=build /src/bin/${PROJECT_NAME} /usr/local/bin/${PROJECT_NAME} +USER ${PROJECT_NAME} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a9fe2d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Copyright 2024 Erik Kristensen +Copyright 2023 CloudCover + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d28495 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +docs-build: + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material build + +docs-serve: + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + +docs-seed: + cp README.md docs/index.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b6ce25 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# gcp-nuke + +[![license](https://img.shields.io/github/license/ekristen/gcp-nuke.svg)](https://github.com/ekristen/gcp-nuke/blob/main/LICENSE) +[![release](https://img.shields.io/github/release/ekristen/gcp-nuke.svg)](https://github.com/ekristen/gcp-nuke/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/ekristen/gcp-nuke)](https://goreportcard.com/report/github.com/ekristen/gcp-nuke) +[![Maintainability](https://api.codeclimate.com/v1/badges/51b67f545bfb93ecab2f/maintainability)](https://codeclimate.com/github/ekristen/gcp-nuke/maintainability) + +**This is potentially very destructive! Use at your own risk!** + +**Status:** Beta. Tool is stable, but could experience odd behaviors with some resources. + +## Overview + +Remove all resources from a GCP Project. + +**gcp-nuke** is in beta, but it is likely that not all GCP resources are covered by it. Be encouraged to add missing +resources and create a Pull Request or to create an [Issue](https://github.com/ekristen/gcp-nuke/issues/new). + +## Documentation + +All documentation is in the [docs/](docs) directory and is built using [Material for Mkdocs](https://squidfunk.github.io/mkdocs-material/). + +It is hosted at [https://ekristen.github.io/gcp-nuke/](https://ekristen.github.io/gcp-nuke/). + +## Attribution, License, and Copyright + +This tool was written using [libnuke](https://github.com/ekristen/libnuke) at it's core. It shares similarities and commonalities with [aws-nuke](https://github.com/ekristen/aws-nuke) +and [azure-nuke](https://github.com/ekristen/azure-nuke). These tools would not have been possible without the hard work +that came before me on the original tool by the team and contributors over at [rebuy-de](https://github.com/rebuy-de) and their original work +on [rebuy-de/aws-nuke](https://github.com/rebuy-de/aws-nuke). + +This tool is licensed under the MIT license as well. See the [LICENSE](LICENSE) file for more information. Reference +was made to [dshelley66/gcp-nuke](https://github.com/dshelley66/gcp-nuke) during the creation of this tool therefore I +included them in the license copyright although no direct code was used. + +## Usage + +**Note:** all cli flags can also be expressed as environment variables. + +**By default, no destructive actions will be taken.** + +### Example - Dry Run only + +```bash +gcp-nuke run \ + --config test-config.yaml \ + --project-id playground-12345 +``` + +### Example - No Dry Run (DESTRUCTIVE) + +To actually destroy you must add the `--no-dry-run` cli parameter. + +```bash +gcp-nuke run \ + --config=test-config.yaml \ + --project-id playground-12345 \ + --no-dry-run +``` + +## Authentication + +Authentication is only supported via a Service Account either by Key or via Workload Identity. + +### Service Account Key + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json +``` + +### Federated Token (Kubernetes) + +**coming soon** + +## Configuring + +The entire configuration of the tool is done via a single YAML file. + +### Example Configuration + +**Note:** you must add at least one entry to the blocklist. + +```yaml +regions: + - global + - eastus + +blocklist: + - 00001111-2222-3333-4444-555566667777 + +accounts: # i.e. projects but due to the commonality of libnuke, it's accounts here universally between tools + playground-12345: + presets: + - common + filters: + IAMRole: + - property: Name + type: contains + value: CustomRole + IAMServiceAccount: + - property: Name + type: contains + value: custom-service-account + +presets: + common: + filters: + VPC: + - default +``` diff --git a/cosign.pub b/cosign.pub new file mode 100644 index 0000000..4ed64ef --- /dev/null +++ b/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEA1o5xWW+qAADp9GUR5xGzoDT1pct +7LOMCnvrhu3kaV3mqEmQx6N2rVIlCPv1KFZHHfyozA0GSQaOEb0Eodbnew== +-----END PUBLIC KEY----- diff --git a/docs/cli-experimental.md b/docs/cli-experimental.md new file mode 100644 index 0000000..bef1350 --- /dev/null +++ b/docs/cli-experimental.md @@ -0,0 +1,35 @@ +# Experimental Features + +## Overview + +These are the experimental features hidden behind feature flags that are currently available in gcp-nuke. They are all +disabled by default. These are switches that changes the actual behavior of the tool itself. Changing the behavior of +a resource is done via resource settings. + +!!! note + The original tool had configuration options called `feature-flags` which were used to enable/disable certain + behaviors with resources, those are now called settings and `feature-flags` have been deprecated in the config. + +## Usage + +```console +gcp-nuke run --feature-flag "wait-on-dependencies" +``` + +**Note:** other CLI arguments are omitted for brevity. + +## Available Feature Flags + +- `wait-on-dependencies` - This feature flag will cause gcp-nuke to wait for all resource type dependencies to be + deleted before deleting the next resource type. + +### wait-on-dependencies + +This feature flag will cause gcp-nuke to wait for all resource type dependencies to be deleted before deleting the next +resource type. This is useful for resources that have dependencies on other resources. For example, an IAM Role that has +an attached policy. + +The problem is that if you delete the IAM Role first, it will fail because it has a dependency on the policy. + +This feature flag will cause gcp-nuke to wait for all resources of a given type to be deleted before deleting the next +resource type. This will reduce the number of errors and unnecessary API calls. \ No newline at end of file diff --git a/docs/cli-options.md b/docs/cli-options.md new file mode 100644 index 0000000..107f45b --- /dev/null +++ b/docs/cli-options.md @@ -0,0 +1,15 @@ +# Options + +This is not a comprehensive list of options, but rather a list of features that I think are worth highlighting. + +## Skip Prompts + +`--no-prompt` will skip the prompt to verify you want to run the command. This is useful if you are running in a CI/CD environment. +`--prompt-delay` will set the delay before the command runs. This is useful if you want to give yourself time to cancel the command. + +## Logging + +- `--log-level` will set the log level. This is useful if you want to see more or less information in the logs. +- `--log-caller` will log the caller (aka line number and file). This is useful if you are debugging. +- `--log-disable-color` will disable log coloring. This is useful if you are running in an environment that does not support color. +- `--log-full-timestamp` will force log output to always show full timestamp. This is useful if you want to see the full timestamp in the logs. \ No newline at end of file diff --git a/docs/cli-usage.md b/docs/cli-usage.md new file mode 100644 index 0000000..2f85688 --- /dev/null +++ b/docs/cli-usage.md @@ -0,0 +1,52 @@ +# Usage + +## gcp-nuke + +```console +NAME: + gcp-nuke - remove everything from an gcp account + +USAGE: + gcp-nuke [global options] command [command options] + +VERSION: + 1.0.0-dev + +AUTHOR: + Erik Kristensen + +COMMANDS: + run, nuke run nuke against an gcp account and remove everything from it + resource-types, list-resources list available resources to nuke + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --help, -h show help + --version, -v print the version +``` + +## gcp-nuke run + +```console +NAME: + gcp-nuke run - run nuke against an gcp account and remove everything from it + +USAGE: + gcp-nuke run [command options] [arguments...] + +OPTIONS: + --config value path to config file (default: "config.yaml") + --include value, --target value [ --include value, --target value ] only run against these resource types + --exclude value [ --exclude value ] exclude these resource types + --cloud-control value [ --cloud-control value ] use these resource types with the Cloud Control API instead of the default + --quiet hide filtered messages (default: false) + --no-dry-run actually run the removal of the resources after discovery (default: false) + --no-prompt, --force disable prompting for verification to run (default: false) + --prompt-delay value, --force-sleep value seconds to delay after prompt before running (minimum: 3 seconds) (default: 10) + --feature-flag value [ --feature-flag value ] enable experimental behaviors that may not be fully tested or supported + --log-level value, -l value Log Level (default: "info") [$LOGLEVEL] + --log-caller log the caller (aka line number and file) (default: false) + --log-disable-color disable log coloring (default: false) + --log-full-timestamp force log output to always show full timestamp (default: false) + --help, -h show help +``` diff --git a/docs/config-filtering.md b/docs/config-filtering.md new file mode 100644 index 0000000..4ab2042 --- /dev/null +++ b/docs/config-filtering.md @@ -0,0 +1,341 @@ +!!! warning + Filtering is a powerful tool, but it is also a double-edged sword. It is easy to make mistakes in the filter + configuration. Also, since gcp-nuke is in continuous development, there is always a possibility to introduce new + bugs, no matter how careful we review new code. + +# Filtering + +Filtering is used to exclude or include resources from being deleted. This is important for a number of reasons to +include but limited to removing the user that runs the tool. + +!!! note + Filters are `OR'd` together. This means that if a resource matches any filter, it will be excluded from deletion. + Currently, there is no way to do `AND'ing` of filters. + +## Global + +Filters are traditionally done against a specific resource. However, `__global__` as been introduced as a unique +resource type that can be used to apply filters to all defined resources. It's all or nothing, global cannot be used to +against some resources and not others. + +Global works by taking all filters defined under `__global__` and prepends to any filters found for a resource type. If +a resource does NOT have any filters defined, the `__global__` ones will still be used. + +### Example + +In this example, we are ignoring all resources that have the tag `gcp-nuke` set to `ignore`. Additionally filtering +a specific instance by its `id`. When the `ComputeInstance` resource is processed, it will have both filters applied. These + +```yaml +__global__: + - property: label:gcp-nuke + value: "ignore" + +ComputeInstance: + - "test-instance-01b489457a60298dd" +``` + +This will ultimately render as the following filters for the `ComputeInstance` resource: + +```yaml +ComputeInstance: + - "test-instance-01b489457a60298dd" + - property: label:gcp-nuke + value: "ignore" +``` + +## Types + +The following are comparisons that you can use to filter resources. These are used in the configuration file. + +- `exact` +- `contains` +- `glob` +- `regex` +- `dateOlderThan` + +To use a non-default comparison type, it is required to specify an object with `type` and `value` instead of the +plain string. + +These types can be used to simplify the configuration. For example, it is possible to protect all access keys of a +single user by using `glob`: + +```yaml +IAMServiceAccountKey: +- type: glob + value: "admin -> *" +``` + +### Exact + +The identifier must exactly match the given string. **This is the default.** + +Exact is just that, an exact match to a resource. The following examples are identical for the `exact` filter. + +```yaml +IAMRole: +- custom-role +- type: exact + value: custom-role +``` + +### Contains + +The `contains` filter is a simple string contains match. The following examples are identical for the `contains` filter. + +```yaml +IAMRole: + - type: contains + value: Nuke +``` + +### Glob + +The identifier must match against the given [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)). This means the string might contain +wildcards like `*` and `?`. Note that globbing is designed for file paths, so the wildcards do not match the directory +separator (`/`). Details about the glob pattern can be found in the [library documentation](https://godoc.org/github.com/mb0/glob) + +```yaml +IAMUser: + - type: glob + value: "gcp-nuke*" +``` + +### Regex + +The identifier must match against the given regular expression. Details about the syntax can be found +in the [library documentation](https://golang.org/pkg/regexp/syntax/). + +```yaml +IAMUser: + - type: regex + value: "gcp-nuke.*" +``` + +### DateOlderThan + +This works by parsing the specified property into a timestamp and comparing it to the current time minus the specified +duration. The duration is specified in the `value` field. The duration syntax is based on golang's duration syntax. + +> ParseDuration parses a duration string. A duration string is a possibly signed sequence of decimal numbers, each with +> optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), +> "ms", "s", "m", "h". + +Full details on duration syntax can be found in the [time library documentation](https://golang.org/pkg/time/#ParseDuration). + +The value from the property is parsed as a timestamp and the following are the supported formats: + +- `2006-01-02` +- `2006/01/02` +- `2006-01-02T15:04:05Z` +- `2006-01-02T15:04:05.999999999Z07:00` +- `2006-01-02T15:04:05Z07:00` + +In the follow example we are filtering EC2 Images that have a `CreationDate` older than 1 hour. + +```yaml +ComputeDisk: + - type: dateOlderThan + property: CreationDate + value: 1h +``` + +## Properties + +By default, when writing a filter if you do not specify a property, it will use the `Name` property. However, resources +that do no support Properties, gcp-nuke will fall back to what is called the `Legacy String`, it's essentially a +function that returns a string representation of the resource. + +Some resources support filtering via properties. When a resource support these properties, they will be listed in +the output like in this example: + +```log +global - IAMUserPolicyAttachment - 'admin -> AdministratorAccess' - [RoleName: "admin", PolicyArn: "arn:gcp:iam::gcp:policy/AdministratorAccess", PolicyName: "AdministratorAccess"] - would remove +``` + +To use properties, it is required to specify an object with `properties` and `value` instead of the plain string. + +These types can be used to simplify the configuration. For example, it is possible to protect all access keys +of a single user: + +```yaml +IAMUserAccessKey: + - property: UserName + value: "admin" +``` + +## Inverting + +Any filter result can be inverted by using `invert: true`, for example: + +```yaml +ComputeInstance: + - property: Name + value: "foo" + invert: true +``` + +In this case *any* CloudFormationStack ***but*** the ones called "foo" will be filtered. Be aware that *gcp-nuke* +internally takes every resource and applies every filter on it. If a filter matches, it marks the node as filtered. + +## Example + +It is also possible to use Filter Properties and Filter Types together. For example to protect all Hosted Zone of a +specific TLD: + +```yaml +ComputeInstance: + - property: Name + type: glob + value: "*.testing" +``` + +## Project Level + +It is possible to filter this is important for not deleting the current user for example or for resources like S3 +Buckets which have a globally shared namespace and might be hard to recreate. Currently, the filtering is based on +the resource identifier. The identifier will be printed as the first step of *gcp-nuke* (eg `i-01b489457a60298dd` +for an EC2 instance). + +!!! warning + **Even with filters you should not run gcp-nuke on any gcp account, where you cannot afford to lose all resources. + It is easy to make mistakes in the filter configuration. Also, since gcp-nuke is in continuous development, there is + always a possibility to introduce new bugs, no matter how careful we review new code.** + +The filters are part of the account-specific configuration and are grouped by resource types. This is an example of a +config that deletes all resources but the `admin` user with its access permissions and two access keys: + +```yaml +--- +regions: + - global + - us-central1 + +blocklist: + - bootstrap-12345 + +accounts: + playground-12345: + filters: + IAMUser: + - "admin" + IAMUserPolicyAttachment: + - "admin -> AdministratorAccess" + IAMUserAccessKey: + - "admin -> AKSDAFRETERSDF" + - "admin -> AFGDSGRTEWSFEY" +``` + +Any resource whose resource identifier exactly matches any of the filters in the list will be skipped. These will +be marked as "filtered by config" on the *gcp-nuke* run. + + +## Presets + +It might be the case that some filters are the same across multiple accounts. +This especially could happen, if provisioning tools like Terraform are used or +if IAM resources follow the same pattern. + +For this case *gcp-nuke* supports presets of filters, that can applied on +multiple accounts. A configuration could look like this: + +```yaml +--- +regions: + - global + - us-central1 + +account-blocklist: + - bootstrap-12345 + +accounts: + playground-12345: + presets: + - "common" + dev-9484: + presets: + - "common" + - "terraform" + sandbox-134313: + presets: + - "common" + - "terraform" + filters: + IAMRole: + - "notebook" + +presets: + terraform: + filters: + StorageBucket: + - type: glob + value: "my-statebucket-*" + DynamoDBTable: + - "terraform-lock" + common: + filters: + IAMRole: + - custom-role +``` + +## Included and Excluding + +*gcp-nuke* deletes a lot of resources and there might be added more at any release. Eventually, every resource should +get deleted. You might want to restrict which resources to delete. There are multiple ways to configure this. + +One way are filters, which already got mentioned. This requires to know the identifier of each resource. It is also +possible to prevent whole resource types (eg `StorageBucket`) from getting deleted with two methods. + +It is also possible to configure the resource types in the config file like in these examples: + +```yaml +regions: + - us-central1 + +blocklist: + - playground-12345 + +resource-types: + # Specifying this in the configuration will ensure that only these three + # resources are targeted by gcp-nuke during it's run. + includes: + - StorageBucketObject + - StorageBucket + - IAMRole + +accounts: + playground-12345: {} +``` + +```yaml +regions: + - us-central1 + +blocklist: + - production-12345 + +resource-types: + # Specifying this in the configuration will ensure that these resources + # will be specifically excluded from gcp-nuke during it's run. + excludes: + - IAMRole + +accounts: + playground-12345: {} +``` + +If `includes` are specified in multiple places (e.g. CLI and account specific), then a resource type must be specified +in all places. In other words each configuration limits the previous ones. + +If an `exclude` is used, then all its resource types will not be deleted. + +**Hint:** You can see all available resource types with this command: + +```bash +gcp-nuke resource-types +``` + +It is also possible to include and exclude resources using the command line arguments: + +- The `--include` flag limits nuking to the specified resource types. +- The `--exclude` flag prevent nuking of the specified resource types. \ No newline at end of file diff --git a/docs/config-presets.md b/docs/config-presets.md new file mode 100644 index 0000000..5033dfe --- /dev/null +++ b/docs/config-presets.md @@ -0,0 +1,53 @@ +# Presets + +It might be the case that some filters are the same across multiple accounts. This especially could happen, if +provisioning tools like Terraform are used or if IAM resources follow the same pattern. + +For this case *gcp-nuke* supports presets of filters, that can applied on multiple accounts. + +`Presets` are defined globally. They can then be referenced in the `accounts` section of the configuration. + +A preset configuration could look like this: + +```yaml +presets: + common: + filters: + IAMAccountSettingPasswordPolicy: + - custom + IAMRole: + - "OrganizationAccountAccessRole" +``` + +An account referencing a preset would then look something like this: + +```yaml +accounts: + playground-12345: + presets: + - common +``` + +Putting it all together it would look something like this: + +```yaml +blocklist: + - 0012345678 + +regions: + - global + - us-east-1 + +accounts: + 1234567890: + presets: + - common + +presets: + common: + filters: + IAMAccountSettingPasswordPolicy: + - custom + IAMRole: + - OrganizationAccountAccessRole +``` \ No newline at end of file diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..bb2394a --- /dev/null +++ b/docs/config.md @@ -0,0 +1,128 @@ +# Config + +The configuration is the user supplied configuration that is used to drive the nuke process. The configuration is a YAML file that is loaded from the path specified by the --config flag. + +## Sections + +The configuration is broken down into the following sections: + +- [blocklist](#blocklist) +- [regions](#regions) +- [accounts](#accounts) + - [presets](#presets) + - [filters](#filters) + - [resource-types](#resource-types) + - [includes](#includes) + - [excludes](#excludes) + - [cloud-control](#cloud-control) + - targets (deprecated, use includes) +- [resource-types](#resource-types) + - [includes](#includes) + - [excludes](#excludes) + - [cloud-control](#cloud-control) + - targets (deprecated, use includes) +- [feature-flags](#feature-flags) (deprecated, use settings instead) +- [settings](#settings) +- [presets](#global-presets) + +## Simple Example + +```yaml +blocklist: + - bootstrap-12345 + +regions: + - global + - us-central1 + +accounts: + playground-12345: + filters: + IAMRole: + - "admin" + +resource-types: + includes: + - IAMRole + +settings: + CloudSQLInstance: + DisableDeletionProtection: true +``` + +## Blocklist + +The blocklist is simply a list of Accounts that the tool cannot run against. This is to protect the user from accidentally +running the tool against the wrong account. The blocklist must always be populated with at least one entry. + +## Regions + +The regions is a list of GCP regions that the tool will run against. The tool will run against all regions specified in the +configuration. If no regions are listed, then the tool will **NOT** run against any region. Regions must be explicitly +provided. + +### All Regions + +You may specify the special region `all` to run against all enabled regions. This will run against all regions that are +enabled in the account. It will not run against regions that are disabled. It will also automatically include the +special region `global` which is for specific global resources. + +!!! important + The use of `all` will ignore all other regions specified in the configuration. It will only run against regions + that are enabled in the account. + +## Projects + +!!! important + Projects uses the `accounts` key in the configuration. This is due to the commonality of libnuke between tools. + +The accounts section is a map of GCP Project IDs to their configuration. The ID is the key and the value is the +configuration for that project. + +The configuration for each project is broken down into the following sections: + +- presets +- filters +- resource-types + - includes + - excludes + +### Presets + +Presets under an account entry is a list of strings that must map to a globally defined preset in the configuration. + +### Filters + +Filters is a map of filters against resource types. To learn more about filters, see the [Filtering](./config-filtering.md) + +**Note:** filters can be defined at the account level and at the preset level. + +## Resource Types + +Resource types is a map of resource types to their configuration. The resource type is the key and the value is the +configuration for that resource type. + +The configuration for each resource type is broken down into the following sections: + +- includes +- excludes + +### Includes + +Includes are a list of resource types the tool will run against. If no includes are specified, then the tool will run against +all resource types. + +### Excludes + +Excludes are a list of resource types the tool will not run against. If no excludes are specified, then the tool will run +against all resource types unless Includes is specified. + +## Settings + +Settings are a map of resource types to their settings configuration. This allows for alternative behavior when removing +resources. If a resource has a setting alternative, and you'd like to use its behavior, then you can specify the resource +type in the `settings` section. + +## Global Presets + +To read more on global presets, see the [Presets](./config-presets.md) documentation. \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..ed8ed7f --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,174 @@ +# Contributing + +Thank you for wanting to contribute to *gcp-nuke*. + +Because of the amount of gcp services and their rate of change, we rely on your participation. For the same reason we +can only act retroactive on changes of gcp services. Otherwise, it would be a full time job to keep up with gcp. + +## How Can I Contribute? + +### Some Resource Is Not Supported by *gcp-nuke* + +If a resource is not yet supported by *gcp-nuke*, you have two options to resolve this: + +* File [an issue](https://github.com/ekristen/gcp-nuke/issues/new) and describe which resource is missing. This way someone can take care of it. +* Add the resource yourself and open a Pull Request. Please follow the guidelines below to see how to create + such a resource. + +### Some Resource Does Not Get Deleted + +Please check the following points before creating a bug issue: + +* Is the resource actually supported by *gcp-nuke*? If not, please follow the guidelines above. +* Are there permission problems? In this case *gcp-nuke* will print errors that usually contain the status code `403`. +* Did you just get scared by an error that was printed? *gcp-nuke* does not know about dependencies between resources. + To work around this it will just retry deleting all resources in multiple iterations. Therefore, it is normal that + there are a lot of dependency errors in the first one. The iterations are separated by lines starting with + `Removal requested:` and only the errors in the last block indicate actual errors. + +File [an issue](https://github.com/ekristen/gcp-nuke/issues/new) and describe as accurately as possible how to generate the resource on gcp that cause the +errors in *gcp-nuke*. Ideally this is provided in a reproducible way like a Terraform template or gcp CLI commands. + +### I Have Ideas to Improve *gcp-nuke* + +You should take these steps if you have an idea how to improve *gcp-nuke*: + +1. Check the [issues page](https://github.com/ekristen/gcp-nuke/issues), whether someone already had the same or a similar idea. +2. Also check the [closed issues](https://github.com/ekristen/gcp-nuke/issues?utf8=%E2%9C%93&q=is%3Aissue), because this might have already been implemented, but not yet released. Also, + the idea might not be viable for obvious reasons. +3. Join the discussion, if there is already a related issue. If this is not the case, open a new issue and describe + your idea. Afterward, we can discuss this idea and form a proposal. + +### I Just Have a Question + +Please use [GitHub Discussions](https://github.com/ekristen/gcp-nuke/discussions) + +## Resource Guidelines + +### Tooling + +Checkout the documentation around [resources](https://ekristen.github.io/gcp-nuke/resources/) as it provides resource +format and a tool to help generate the resource. + +### Consider Pagination + +Most gcp resources are paginated and all resources should handle that. + +### Use Properties Instead of String Functions + +Currently, each resource can offer two functions to describe itself, that are used by the user to identify it and by +*gcp-nuke* to filter it. + +The String function is deprecated: + +```golang +func (r *Resource) String() string +``` + +The Properties function should be used instead: + +```golang +func (r *Resource) Properties() types.Properties +``` + +**Note:** The interface for the String function is still there, because not all resources are migrated yet. Please use +the Properties function for new resources. + +### Filter Resources That Cannot Get Removed + +Some gcp APIs list resources, that cannot be deleted. For example: + +* Resources that are already deleted, but still listed for some time (e.g. EC2 Instances) +* Resources that are created by gcp, but cannot be deleted by the user (e.g. some IAM Roles) + +Those resources should be excluded in the filter step, rather than in the list step. + +## Styleguide + +### Go + +#### golangci-lint + +There is an extensive golangci-lint configuration in the repository. Please make sure to run `golangci-lint run` before +committing any changes. + +#### Code Format + +Like almost all Go projects, we are using `go fmt` as a single source of truth for formatting the source code. Please +use `go fmt` before committing any change. + +#### Import Format + +1. Standard library imports +2. Third party imports +3. gcp SDK imports +4. ekristen/libnuke imports +5. Local package imports + +##### Example Import Format + +```golang +package example + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/gcp/gcp-sdk-go-v2/gcp" + "github.com/gcp/gcp-sdk-go-v2/service/s3" + + "github.com/ekristen/libnuke/pkg/settings" + + "github.com/ekristen/gcp-nuke/pkg/types" +) +``` + +### Git + +#### Pull Requests + +We default to squash merge pull requests. This means that the commit history of the pull request will be squashed into a +single commit and then merged into the main branch. This keeps the commit history clean and easy to read. + +#### Commits + +We are using the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for our commit +messages. This allows us to automatically generate a changelog and version numbers. + +All commits in a pull request must follow this format or the GitHub Actions will fail. + +#### Signed Commits + +We require that all commits be signed. + +```console +git config --global commit.gpgsign true +``` + +#### Setup Email + +We prefer having the commit linked to the GitHub account, that is creating the Pull Request. To make this happen, +*git* must be configured with an email, that is registered with a GitHub account. + +To set the email for all git commits, you can use this command: + +```bash +git config --global user.email "email@example.com" +``` + +If you want to change the email only for the *gcp-nuke* repository, you can skip the `--global` flag. You have to +make sure that you are executing this in the *gcp-nuke* directory: + +```bash +git config user.email "email@example.com" +``` + +If you already committed something with a wrong email, you can use this command: + +```bash +git commit --amend --author="Author Name " +``` + +This changes the email of the latest commit. If you have multiple commits in your branch, please squash them and +change the author afterwards. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..e68b382 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,19 @@ +# Development + +## Building + +The following will build the binary for the current platform and place it in the `releases` directory. + +```console +goreleaser build --clean --snapshot --single-target +``` + +## Documentation + +This is built using Material for MkDocs and can be run very easily locally providing you have docker available. + +### Running Locally + +```console +make docs-serve +``` diff --git a/docs/features/all-regions.md b/docs/features/all-regions.md new file mode 100644 index 0000000..31ec985 --- /dev/null +++ b/docs/features/all-regions.md @@ -0,0 +1,7 @@ +# Feature: All Regions + +There is a special region called `all` that can be provided to the regions block in the configuration. If `all` is +provided then the special `global` region and all regions that are enabled for the account will automatically be +included. Any other regions that are provided will be **ignored**. + +See [Full Documentation](../config.md#all-regions) for more information. \ No newline at end of file diff --git a/docs/features/global-filters.md b/docs/features/global-filters.md new file mode 100644 index 0000000..0fdde23 --- /dev/null +++ b/docs/features/global-filters.md @@ -0,0 +1,7 @@ +# Global Filters + +Global filters are filters that are applied to all resources. They are defined using a special resource name called +`__global__`. The global filters are pre-pended to all resources before any other filters for the specific resource +are applied. + +[Full Documentation](../config-filtering.md#global) \ No newline at end of file diff --git a/docs/features/overview.md b/docs/features/overview.md new file mode 100644 index 0000000..e679205 --- /dev/null +++ b/docs/features/overview.md @@ -0,0 +1,7 @@ +There are a number of new features with this version these features range from usability to debugging purposes. + +Some of the new features include: + +- [Global Filters](global-filters.md) +- [Run Against All Regions](all-regions.md) +- [Signed Binaries](signed-binaries.md) diff --git a/docs/features/signed-binaries.md b/docs/features/signed-binaries.md new file mode 100644 index 0000000..a399af0 --- /dev/null +++ b/docs/features/signed-binaries.md @@ -0,0 +1,4 @@ +# Signed Binaries + +All darwin binaries are now signed and notarized by Apple. This means that you can now run the binaries without +needing to bypass the security settings on your Mac. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6ed2941 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,57 @@ +Remove all resources from an gcp account. + +**gcp-nuke** is stable, but it is likely that not all gcp resources are covered by it. Be encouraged to add missing +resources and create a Pull Request or to create an [Issue](https://github.com/ekristen/gcp-nuke/issues/new). + +!!! danger "Destructive Tool" + Be aware that this is a very destructive tool, hence you have to be very careful while using it. Otherwise, + you might delete production data. + +This is not a comprehensive list, but here are some of the highlights: + +## History of this Fork + +This is a full fork of the original tool written by the folks over at [rebuy-de](https://github.com/rebuy-de). This fork became necessary +after attempting to make contributions and respond to issues to learn that the current maintainers only have time to +work on the project about once a month and while receptive to bringing in other people to help maintain, made it clear +it would take time, but overall got the feeling that they wanted to maintain full control, which is understandable. +Considering the feedback cycle was already weeks on initial communications, I had to make the hard decision to fork +and maintain it. + +Since then the rebuy-de team has taken up interest in responding to their issues and pull requests, however there are a +lot of outstanding feature requests and other tasks not being tackled, therefore I have decided to continue +maintaining this fork as I have a few things I want to do with it that I don't think they will be interested. + +### Continued Attribution + +I want to make it clear that I am not trying to take credit for the work of the original authors, and I will continue +to give them credit for their work. I also want to make sure any contributors are also recognized and attributed for +their work. Since this has diverged from the upstream, I've written tooling and scripts to cherry-pick commits from +upstream and apply them to this fork, then modify the resources to work with the new library and submit as a PR to this +fork. + +## Introducing libnuke + +Officially over the Christmas break of 2023, I decided to create [libnuke](https://github.com/ekristen/libnuke) which +is a library that can be used to create similar tools for other cloud providers. This library is used by both this tool, +gcp-nuke, and [azure-nuke](https://github.com/ekristen/azure-nuke) and soon [gcp-nuke](https://github.com/ekristen/gcp-nuke). + +I also needed a version of this tool for Azure and GCP, and initially I just copied and altered the code I needed for +Azure, but I didn't want to have to maintain multiple copies of the same code, so I decided to create +[libnuke](https://github.com/ekristen/libnuke) to abstract all the code that was common between the two tools and write proper unit tests for it. + +## Why a rewrite? + +I decided to rewrite this tool for a few reasons: + +- [x] I wanted to improve the build process by using `goreleaser` +- [x] I wanted to improve the release process by using `goreleaser` and publishing multi-architecture images +- [x] I also wanted to start signing all the releases +- [x] I wanted to add additional tests and improve the test coverage, more tests on the way for individual resources. + - [libnuke](https://github.com/ekristen/libnuke) is at 94%+ overall test coverage. +- [x] I wanted to reduce the maintenance burden by abstracting the core code into a library +- [x] I wanted to make adding additional resources more easy and lowering the barrier to entry +- [x] I wanted to add a lot more documentation and examples +- [x] I wanted to take steps to make way for gcp SDK Version 2 +- [ ] I wanted to add a DAG for dependencies between resource types and individual resources (this is still a work in progress) + - This will improve the process of deleting resources that have dependencies on other resources and reduce errors and unnecessary API calls. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..f8a3f52 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,32 @@ +# Install + +## Install the pre-compiled binary + +### Homebrew Tap (MacOS/Linux) + +```console +brew install ekristen/tap/gcp-nuke +``` + +## Releases + +You can download pre-compiled binaries from the [releases](https://github.com/ekristen/gcp-nuke/releases) page. + +## Docker + +Registries: + +- [ghcr.io/ekristen/gcp-nuke](https://github.com/ekristen/gcp-nuke/pkgs/container/gcp-nuke) + +You can run **gcp-nuke** with Docker by using a command like this: + +## Source + +To compile **gcp-nuke** from source you need a working [Golang](https://golang.org/doc/install) development environment and [goreleaser](https://goreleaser.com/install/). + +**gcp-nuke** uses go modules and so the clone path should not matter. Then simply change directory into the clone and run: + +```bash +goreleaser build --clean --snapshot --single-target +``` + diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..50b22a7 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,136 @@ +# First Run + +## First Configuration + +First you need to create a config file for *gcp-nuke*. This is a minimal one: + +```yaml +regions: + - global + - us-central1 + +blocklist: + - production-12345 + +accounts: + playground-12345: {} # gcp-nuke-example +``` + +## First Run (Dry Run) + +With this config we can run *gcp-nuke*: + +```bash +$ gcp-nuke nuke -c config/nuke-config.yaml +gcp-nuke version v1.0.39.gc2f318f - Fri Jul 28 16:26:41 CEST 2017 - c2f318f37b7d2dec0e646da3d4d05ab5296d5bce + +Do you really want to nuke the account with the ID 000000000000 and the alias 'gcp-nuke-example'? +Do you want to continue? Enter account alias to continue. +> gcp-nuke-example + +us-east-1 - EC2DHCPOption - 'dopt-bf2ec3d8' - would remove +us-east-1 - EC2Instance - 'i-01b489457a60298dd' - would remove +us-east-1 - EC2KeyPair - 'test' - would remove +us-east-1 - EC2NetworkACL - 'acl-6482a303' - cannot delete default VPC +us-east-1 - EC2RouteTable - 'rtb-ffe91e99' - would remove +us-east-1 - EC2SecurityGroup - 'sg-220e945a' - cannot delete group 'default' +us-east-1 - EC2SecurityGroup - 'sg-f20f958a' - would remove +us-east-1 - EC2Subnet - 'subnet-154d844e' - would remove +us-east-1 - EC2Volume - 'vol-0ddfb15461a00c3e2' - would remove +us-east-1 - EC2VPC - 'vpc-c6159fa1' - would remove +us-east-1 - IAMUserAccessKey - 'my-user -> ABCDEFGHIJKLMNOPQRST' - would remove +us-east-1 - IAMUserPolicyAttachment - 'my-user -> AdministratorAccess' - [UserName: "my-user", PolicyArn: "arn:gcp:iam::gcp:policy/AdministratorAccess", PolicyName: "AdministratorAccess"] - would remove +us-east-1 - IAMUser - 'my-user' - would remove +Scan complete: 13 total, 11 nukeable, 2 filtered. + +Would delete these resources. Provide --no-dry-run to actually destroy resources. +``` + +As we see, *gcp-nuke* only lists all found resources and exits. This is because the `--no-dry-run` flag is missing. +Also, it wants to delete the administrator. We don't want to do this, because we use this user to access our account. +Therefore, we have to extend the config, so it ignores this user: + +```yaml +regions: + - global + - us-central1 + +blocklist: + - production-12345 + +accounts: + playground-12345: # gcp-nuke-example + filters: + IAMUser: + - "my-user" + IAMUserPolicyAttachment: + - "my-user -> AdministratorAccess" + IAMUserAccessKey: + - "my-user -> ABCDEFGHIJKLMNOPQRST" +``` + +## Second Run (No Dry Run) + +!!! warning +This will officially remove resources from your gcp project. Make sure you really want to do this! + +```bash +$ gcp-nuke nuke --config config/nuke-config.yml --no-dry-run +gcp-nuke version v1.0.39.gc2f318f - Fri Jul 28 16:26:41 CEST 2017 - c2f318f37b7d2dec0e646da3d4d05ab5296d5bce + +Do you really want to nuke the account with the ID 000000000000 and the alias 'gcp-nuke-example'? +Do you want to continue? Enter account alias to continue. +> gcp-nuke-example + +us-east-1 - EC2DHCPOption - 'dopt-bf2ec3d8' - would remove +us-east-1 - EC2Instance - 'i-01b489457a60298dd' - would remove +us-east-1 - EC2KeyPair - 'test' - would remove +us-east-1 - EC2NetworkACL - 'acl-6482a303' - cannot delete default VPC +us-east-1 - EC2RouteTable - 'rtb-ffe91e99' - would remove +us-east-1 - EC2SecurityGroup - 'sg-220e945a' - cannot delete group 'default' +us-east-1 - EC2SecurityGroup - 'sg-f20f958a' - would remove +us-east-1 - EC2Subnet - 'subnet-154d844e' - would remove +us-east-1 - EC2Volume - 'vol-0ddfb15461a00c3e2' - would remove +us-east-1 - EC2VPC - 'vpc-c6159fa1' - would remove +us-east-1 - IAMUserAccessKey - 'my-user -> ABCDEFGHIJKLMNOPQRST' - filtered by config +us-east-1 - IAMUserPolicyAttachment - 'my-user -> AdministratorAccess' - [UserName: "my-user", PolicyArn: "arn:gcp:iam::gcp:policy/AdministratorAccess", PolicyName: "AdministratorAccess"] - would remove +us-east-1 - IAMUser - 'my-user' - filtered by config +Scan complete: 13 total, 8 nukeable, 5 filtered. + +Do you really want to nuke these resources on the account with the ID 000000000000 and the alias 'gcp-nuke-example'? +Do you want to continue? Enter account alias to continue. +> gcp-nuke-example + +us-east-1 - EC2DHCPOption - 'dopt-bf2ec3d8' - failed +us-east-1 - EC2Instance - 'i-01b489457a60298dd' - triggered remove +us-east-1 - EC2KeyPair - 'test' - triggered remove +us-east-1 - EC2RouteTable - 'rtb-ffe91e99' - failed +us-east-1 - EC2SecurityGroup - 'sg-f20f958a' - failed +us-east-1 - EC2Subnet - 'subnet-154d844e' - failed +us-east-1 - EC2Volume - 'vol-0ddfb15461a00c3e2' - failed +us-east-1 - EC2VPC - 'vpc-c6159fa1' - failed +us-east-1 - S3Object - 's3://rebuy-terraform-state-138758637120/run-terraform.lock' - triggered remove + +Removal requested: 2 waiting, 6 failed, 5 skipped, 0 finished + +us-east-1 - EC2DHCPOption - 'dopt-bf2ec3d8' - failed +us-east-1 - EC2Instance - 'i-01b489457a60298dd' - waiting +us-east-1 - EC2KeyPair - 'test' - removed +us-east-1 - EC2RouteTable - 'rtb-ffe91e99' - failed +us-east-1 - EC2SecurityGroup - 'sg-f20f958a' - failed +us-east-1 - EC2Subnet - 'subnet-154d844e' - failed +us-east-1 - EC2Volume - 'vol-0ddfb15461a00c3e2' - failed +us-east-1 - EC2VPC - 'vpc-c6159fa1' - failed + +Removal requested: 1 waiting, 6 failed, 5 skipped, 1 finished + +--- truncating long output --- +``` + +As you see *gcp-nuke* now tries to delete all resources which aren't filtered, without caring about the dependencies +between them. This results in API errors which can be ignored. These errors are shown at the end of the *gcp-nuke* run, +if they keep to appear. + +*gcp-nuke* retries deleting all resources until all specified ones are deleted or until there are only resources +with errors left. + diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..ed30150 --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,13 @@ +# Releases + +Releases are performed automatically based on semantic commits. The versioning is then determined from the commits. +We use a tool called [semantic-release](https://semantic-release.gitbook.io/) to determine if a release should occur +and what the version should be. This takes away the need for humans to be involved. + +## Release Assets + +You can find Linux, macOS and Windows binaries on the [releases page](https://github.com/ekristen/gcp-nuke/releases), but we also provide containerized +versions on [ghcr.io/ekristen/gcp-nuke](https://ghcr.io/ekristen/gcp-nuke). + +Both are available for multiple architectures (amd64, arm64 & armv7) using Docker manifests. You can reference the +main tag on any system and get the correct docker image automatically. \ No newline at end of file diff --git a/docs/resources.md b/docs/resources.md new file mode 100644 index 0000000..39f963e --- /dev/null +++ b/docs/resources.md @@ -0,0 +1,170 @@ +# Resources + +Resources are the core of the tool, they are what is used to list and remove resources from gcp. The resources are +broken down into separate files. + +When creating a resource there's the base resource type, then there's the `Lister` type that returns a list of resources +that it discovers. Those resources are then filtered by any filtering criteria on the resource itself. + +## Anatomy of a Resource + +The anatomy of a resource is fairly simple, it's broken down into a few parts: + +- `Resource` - This is the base resource type that is used to define the resource. +- `Lister` - This is the type that is used to list the resources. + +### Resource + +The resource must have the `func Remove() error` method defined on it, this is what is used to remove the resource. + +It can optionally have the following methods defined: + +- `func Filter() error` - This is used to pre-filter resources, usually based on internal criteria, like system defaults. +- `func String() string` - This is used to print the resource in a human-readable format. +- `func Properties() types.Properties` - This is used to print the resource in a human-readable format. + +```go +package resources + +import ( + "context" + + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +type ExampleResource struct { + ID *string +} + +func (r *ExampleResource) Remove(_ context.Context) error { + // remove the resource, an error will put the resource in failed state + // resources in failed state are retried a number of times + return nil +} + +func (r *ExampleResource) Filter() error { + // filter the resource, this is useful for built-in resources that cannot + // be removed, like a gcp managed resource, return an error here to filter + // it before it even gets to the user supplied filters. + return nil +} + +func (r *ExampleResource) String() string { + // return a string representation of the resource, this is legacy, but still + // used for a number of reasons. + return *r.ID +} +``` + +## Lister + +The lister must have the `func List(ctx context.Context, o interface{}) ([]resource.Resource, error)` method defined on it. + +```go +package resources + +import ( + "context" + + "github.com/ekristen/libnuke/pkg/resource" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +type ExampleResourceLister struct{} + +func (l *ExampleResourceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + + var resources []resource.Resource + + // list the resources and add to resources slice + + return resources, nil +} +``` + +### Example + +```go +package resources + +import ( + "context" + + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +type ExampleResourceLister struct{} + +func (l *ExampleResourceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + + var resources []resource.Resource + + // list the resources and add to resources slice + + return resources, nil +} + +// ----------------------------------------------------------------------------- + +type ExampleResource struct { + ID *string +} + +func (r *ExampleResource) Remove(_ context.Context) error { + // remove the resource, an error will put the resource in failed state + // resources in failed state are retried a number of times + return nil +} + +func (r *ExampleResource) Filter() error { + // filter the resource, this is useful for built-in resources that cannot + // be removed, like an gcp managed resource, return an error here to filter + // it before it even gets to the user supplied filters. + return nil +} + +func (r *ExampleResource) String() string { + // return a string representation of the resource, this is legacy, but still + // used for a number of reasons. + return *r.ID +} + +func (r *ExampleResource) Properties() types.Properties { + // return a properties representation of the resource + props := types.NewProperties() + props.Set("ID", r.ID) + return props +} +``` + +## Creating a new resource + +Creating a new resources is fairly straightforward and a template is provided for you, along with a tool to help you +generate the boilerplate code. + +Currently, the code is generated using a tool that is located in `tools/create-resource/main.go` and can be run like so: + +!!! note + At present, the tool does not check if the service or the resource type is valid, this is purely a helper tool to + generate the boilerplate code. + +```bash +go run tools/create-resource/main.go +``` + +This will output the boilerplate code to stdout, so you can copy and paste it into the appropriate file or you can +redirect to a file like so: + +```bash +go run tools/create-resource/main.go > resources/.go +``` + diff --git a/docs/standards.md b/docs/standards.md new file mode 100644 index 0000000..cb6af9c --- /dev/null +++ b/docs/standards.md @@ -0,0 +1,14 @@ +# Standards + +This is an attempt to describe the standards we use in developing this project. Currently most of the standards +are captured in the contributing guide. + +## Git + +See the [contributing guide](contributing.md) for more information. + +## CLI Standards + +- Use `--no-` prefix for boolean flags + + diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..1a84893 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,82 @@ +# Testing + +This is not a lot of test coverage around the resources themselves. This is due to the cost of running the tests. However, +[libnuke](https://github.com/ekristen/libnuke) is extensively tested for functionality to ensure a smooth experience. + +Generally speaking, the tests are split into two categories: + +1. Tool Testing +2. Resource Testing + +Furthermore, for resource testing, these are broken down into two additional categories: + +1. Mock Tests +2. Integration Tests + +## Tool Testing + +These are unit tests written against non resource focused code inside the `pkg/` directory. + +## Resource Testing + +These are unit tests written against the resources in the `resources/` directory. + +### Mock Tests + +These are tests where the GCP API calls are mocked out. This is done to ensure that the code is working as expected. +Currently, there are only two services mocked out for testing, IAM and CloudFormation. + +#### Adding Additional Mocks + +To add another service to be mocked out, you will need to do the following: + +1. Identify the service in the GCP SDK for Go +2. Create a new file in the `resources/` directory called `service_mock_test.go` +3. Add the following code to the file: (replace `` with actual service name) + ```go + //go:generate ../mocks/generate_mocks.sh iface + package resources + + // Note: empty on purpose, this file exist purely to generate mocks for the service + ``` +4. Run `make generate` to generate the mocks +5. Add tests to the `resources/_mock_test.go` file. +6. Run `make test` to ensure the tests pass +7. Submit a PR with the changes + +### Integration Tests + +These are tests where the GCP API calls are called directly and tested against a live GCP account. These tests are +behind a build flag (`-tags=integration`), so they are not run by default. To run these tests, you will need to run the following: + +```bash +make test-integration +``` + +#### Adding Additional Integration Tests + +To add another integration test, you will need to do the following: + +1. Create a new file in the `resources/` directory called `_test.go` +2. Add the following code to the file: (replace `` with actual resource name) + ```go + //go:build integration + + package resources + + import ( + "testing" + + "github.com/GCP/GCP-sdk-go/GCP/session" + "github.com/GCP/GCP-sdk-go/service/" + ) + + func Test_ExampleResource_Remove(t *testing.T) { + // 1. write code to create resource in GCP using golang sdk + // 2. stub the resource struct out that is defined in .go file + // 3. call the Remove() function + // 4. assert that the resource was removed + } + ``` +3. Run `make test-integration` to ensure the tests pass +4. Submit a PR with the changes diff --git a/docs/warning.md b/docs/warning.md new file mode 100644 index 0000000..ad743de --- /dev/null +++ b/docs/warning.md @@ -0,0 +1,27 @@ +# Caution and Warning + +!!! warning + **We strongly advise you to not run this application on any GCP project, where you cannot afford to lose + all resources.** + +To reduce the blast radius of accidents, there are some safety precautions: + +1. By default, **gcp-nuke** only lists all nuke-able resources. You need to add `--no-dry-run` to actually delete + resources. +2. **gcp-nuke** asks you twice to confirm the deletion by entering the project alias. The first time is directly + after the start and the second time after listing all nuke-able resources. + + !!! note "ProTip" + This can be disabled by adding `--no-prompt` to the command line. + +3. The config file contains a blocklist field. If the Project ID of the project you want to nuke is part of this + blocklist, **gcp-nuke** will abort. It is recommended, that you add every production project to this blocklist. +4. To ensure you don't just ignore the blocklisting feature, the blocklist must contain at least one Project ID. +5. The config file contains project specific settings (e.g. filters). The project you want to nuke must be explicitly + listed there. +6. To ensure to not accidentally delete a random project, it is required to specify a config file. It is recommended + to have only a single config file and add it to a central repository. This way the blocklist is easier to manage and + keep up to date. + +Feel free to create an issue, if you have any ideas to improve the safety procedures. + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..528c1ed --- /dev/null +++ b/go.mod @@ -0,0 +1,67 @@ +module github.com/ekristen/gcp-nuke + +go 1.22 + +require ( + cloud.google.com/go/compute v1.26.0 + github.com/ekristen/libnuke v0.14.3-0.20240514022239-6fae2a990fdf + github.com/fatih/color v1.17.0 + github.com/gotidy/ptr v1.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/urfave/cli/v2 v2.27.1 + google.golang.org/api v0.180.0 +) + +require ( + cloud.google.com/go v0.112.2 // indirect + cloud.google.com/go/auth v0.4.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/container v1.35.1 // indirect + cloud.google.com/go/firestore v1.15.0 // indirect + cloud.google.com/go/functions v1.16.2 // indirect + cloud.google.com/go/iam v1.1.8 // indirect + cloud.google.com/go/kms v1.16.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect + cloud.google.com/go/run v1.3.7 // indirect + cloud.google.com/go/secretmanager v1.13.0 // indirect + cloud.google.com/go/storage v1.41.0 // indirect + firebase.google.com/go v3.13.0+incompatible // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gertd/go-pluralize v0.2.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stevenle/topsort v0.2.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..973c663 --- /dev/null +++ b/go.sum @@ -0,0 +1,262 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= +cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg= +cloud.google.com/go/compute v1.26.0/go.mod h1:T9RIRap4pVHCGUkVFRJ9hygT3KCXjip41X1GgWtBBII= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/container v1.35.1 h1:Vbu/3PZNrgV1Z5DGcRubQdUccX/uMUDNc+NgHNIfbEk= +cloud.google.com/go/container v1.35.1/go.mod h1:udm8fgLm3TtpnjFN4QLLjZezAIIp/VnMo316yIRVRQU= +cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= +cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/functions v1.16.2 h1:83bd2lCgtu2nLbX2jrqsrQhIs7VuVA1N6Op5syeRVIg= +cloud.google.com/go/functions v1.16.2/go.mod h1:+gMvV5E3nMb9EPqX6XwRb646jTyVz8q4yk3DD6xxHpg= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY= +cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc= +cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= +cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go/run v1.3.7 h1:E4Z5e681Qh7UJrJRMCgYhp+3tkcoXiaKGh3UZmUPaAQ= +cloud.google.com/go/run v1.3.7/go.mod h1:iEUflDx4Js+wK0NzF5o7hE9Dj7QqJKnRj0/b6rhVq20= +cloud.google.com/go/secretmanager v1.13.0 h1:nQ/Ca2Gzm/OEP8tr1hiFdHRi5wAnAmsm9qTjwkivyrQ= +cloud.google.com/go/secretmanager v1.13.0/go.mod h1:yWdfNmM2sLIiyv6RM6VqWKeBV7CdS0SO3ybxJJRhBEs= +cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0= +cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= +firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= +firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ekristen/libnuke v0.14.3-0.20240513171000-37bb6e85611c h1:VfZ8FW3eJroG6YOKIESEeBHmz19PJgLvYVxe/msu9Nc= +github.com/ekristen/libnuke v0.14.3-0.20240513171000-37bb6e85611c/go.mod h1:riI1tjCf6r+et/9oUBd1vQeFmn2Sn6UeFUR0nWkMeYw= +github.com/ekristen/libnuke v0.14.3-0.20240514010903-6345e83a32bc h1:F8yor6Z247JZxONh+Fjtnob7pf1OPcNlB3TcWIrBSyw= +github.com/ekristen/libnuke v0.14.3-0.20240514010903-6345e83a32bc/go.mod h1:riI1tjCf6r+et/9oUBd1vQeFmn2Sn6UeFUR0nWkMeYw= +github.com/ekristen/libnuke v0.14.3-0.20240514013525-d2db7f29e31c h1:Vy6l+vhijFgOdZNKgkTfCNKRFLr2pOyKGkYUAnfRaTg= +github.com/ekristen/libnuke v0.14.3-0.20240514013525-d2db7f29e31c/go.mod h1:riI1tjCf6r+et/9oUBd1vQeFmn2Sn6UeFUR0nWkMeYw= +github.com/ekristen/libnuke v0.14.3-0.20240514022239-6fae2a990fdf h1:iPlsTjfmQmhSFXJH3gIXhUiwx2onzMqwUaoHbEQYXmM= +github.com/ekristen/libnuke v0.14.3-0.20240514022239-6fae2a990fdf/go.mod h1:riI1tjCf6r+et/9oUBd1vQeFmn2Sn6UeFUR0nWkMeYw= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/gotidy/ptr v1.4.0 h1:7++suUs+HNHMnyz6/AW3SE+4EnBhupPSQTSI7QNijVc= +github.com/gotidy/ptr v1.4.0/go.mod h1:MjRBG6/IETiiZGWI8LrRtISXEji+8b/jigmj2q0mEyM= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 h1:NK3O7S5FRD/wj7ORQ5C3Mx1STpyEMuFe+/F0Lakd1Nk= +github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4/go.mod h1:FqD3ES5hx6zpzDainDaHgkTIqrPaI9uX4CVWqYZoQjY= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stevenle/topsort v0.2.0 h1:LLWgtp34HPX6/RBDRS0kElVxGOTzGBLI1lSAa5Lb46k= +github.com/stevenle/topsort v0.2.0/go.mod h1:ck2WG2/ZrOr6dLApQ/5Xrqy5wv3T0qhKYWE7r9tkibc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.179.0 h1:QyHDLm/HqM7ysaHgGO0wu7P4NbwbimnOoKxu5Cfdx8s= +google.golang.org/api v0.179.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= +google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= +google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= +google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= +google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6954be7 --- /dev/null +++ b/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "os" + "path" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/ekristen/gcp-nuke/pkg/common" + + _ "github.com/ekristen/gcp-nuke/pkg/commands/list" + _ "github.com/ekristen/gcp-nuke/pkg/commands/run" + + _ "github.com/ekristen/gcp-nuke/resources" +) + +func main() { + defer func() { + if r := recover(); r != nil { + // log panics forces exit + if _, ok := r.(*logrus.Entry); ok { + os.Exit(1) + } + panic(r) + } + }() + + app := cli.NewApp() + app.Name = path.Base(os.Args[0]) + app.Usage = "remove everything from an azure tenant" + app.Version = common.AppVersion.Summary + app.Authors = []*cli.Author{ + { + Name: "Erik Kristensen", + Email: "erik@erikkristensen.com", + }, + } + + app.Commands = common.GetCommands() + app.CommandNotFound = func(context *cli.Context, command string) { + logrus.Fatalf("Command %s not found.", command) + } + + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f6f5399 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,92 @@ +site_name: GCP Nuke +site_url: https://ekristen.github.io/gcp-nuke +site_author: Erik Kristensen +site_description: >- + GCP Nuke is a tool to clean up your GCP project by nuking (deleting) all resources within it. + +repo_name: ekristen/gcp-nuke +repo_url: https://github.com/ekristen/gcp-nuke + +copyright: Copyright © 2024 - Erik Kristensen + +site_dir: public + +# Configuration +theme: + name: material + language: en + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/link + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/toggle-switch + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: indigo + toggle: + icon: material/toggle-switch-off + name: Switch to system preference + features: + - navigation.footer + - navigation.indexes + - navigation.path + - navigation.sections + - navigation.tabs + - toc.follow + - toc.integrate + - content.code.annotate + - content.code.copy + - content.tooltips + - search.highlight + - search.share + - search.suggest + +# Plugins +plugins: + - search + +# Extensions +markdown_extensions: + - admonition + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +# Page tree +nav: + - Getting Started: + - Overview: index.md + - Warning: warning.md + - Install: installation.md + - Quick Start: quick-start.md + - Features: + - Overview: features/overview.md + - Global Filters: features/global-filters.md + - All Regions: features/all-regions.md + - Signed Binaries: features/signed-binaries.md + - CLI: + - Usage: cli-usage.md + - Options: cli-options.md + - Experimental: cli-experimental.md + - Config: + - Overview: config.md + - Filtering: config-filtering.md + - Presets: config-presets.md + - Development: + - Overview: development.md + - Contributing: contributing.md + - Standards: standards.md + - Resources: resources.md + - Releases: releases.md + - Testing: testing.md \ No newline at end of file diff --git a/pkg/commands/global/global.go b/pkg/commands/global/global.go new file mode 100644 index 0000000..d104fe8 --- /dev/null +++ b/pkg/commands/global/global.go @@ -0,0 +1,67 @@ +package global + +import ( + "fmt" + "path" + "runtime" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func Flags() []cli.Flag { + globalFlags := []cli.Flag{ + &cli.StringFlag{ + Name: "log-level", + Usage: "Log Level", + Aliases: []string{"l"}, + EnvVars: []string{"LOGLEVEL"}, + Value: "info", + }, + &cli.BoolFlag{ + Name: "log-caller", + Usage: "log the caller (aka line number and file)", + }, + &cli.BoolFlag{ + Name: "log-disable-color", + Usage: "disable log coloring", + }, + &cli.BoolFlag{ + Name: "log-full-timestamp", + Usage: "force log output to always show full timestamp", + }, + } + + return globalFlags +} + +func Before(c *cli.Context) error { + formatter := &logrus.TextFormatter{ + DisableColors: c.Bool("log-disable-color"), + FullTimestamp: c.Bool("log-full-timestamp"), + } + if c.Bool("log-caller") { + logrus.SetReportCaller(true) + + formatter.CallerPrettyfier = func(f *runtime.Frame) (string, string) { + return "", fmt.Sprintf("%s:%d", path.Base(f.File), f.Line) + } + } + + logrus.SetFormatter(formatter) + + switch c.String("log-level") { + case "trace": + logrus.SetLevel(logrus.TraceLevel) + case "debug": + logrus.SetLevel(logrus.DebugLevel) + case "info": + logrus.SetLevel(logrus.InfoLevel) + case "warn": + logrus.SetLevel(logrus.WarnLevel) + case "error": + logrus.SetLevel(logrus.ErrorLevel) + } + + return nil +} diff --git a/pkg/commands/list/list.go b/pkg/commands/list/list.go new file mode 100644 index 0000000..c7f097c --- /dev/null +++ b/pkg/commands/list/list.go @@ -0,0 +1,57 @@ +package list + +import ( + "fmt" + "sort" + + "github.com/fatih/color" + "github.com/urfave/cli/v2" + + "github.com/ekristen/libnuke/pkg/registry" + + "github.com/ekristen/gcp-nuke/pkg/commands/global" + "github.com/ekristen/gcp-nuke/pkg/common" + "github.com/ekristen/gcp-nuke/pkg/nuke" + + _ "github.com/ekristen/gcp-nuke/resources" +) + +func execute(c *cli.Context) error { + ls := registry.GetNames() + + sort.Strings(ls) + + for _, name := range ls { + reg := registry.GetRegistration(name) + + if reg.AlternativeResource != "" { + color.New(color.Bold).Printf("%-55s\n", name) + color.New(color.Bold, color.FgYellow).Printf(" > %-55s", reg.AlternativeResource) + color.New(color.FgCyan).Printf("alternative resource\n") + } else { + color.New(color.Bold).Printf("%-55s", name) + c := color.FgGreen + if reg.Scope == nuke.Organization { + c = color.FgHiGreen + } else if reg.Scope == nuke.Project { + c = color.FgHiBlue + } + color.New(c).Printf(fmt.Sprintf("%s\n", string(reg.Scope))) + } + } + + return nil +} + +func init() { + cmd := &cli.Command{ + Name: "resource-types", + Aliases: []string{"list-resources"}, + Usage: "list available resources to nuke", + Flags: global.Flags(), + Before: global.Before, + Action: execute, + } + + common.RegisterCommand(cmd) +} diff --git a/pkg/commands/run/command.go b/pkg/commands/run/command.go new file mode 100644 index 0000000..b999b25 --- /dev/null +++ b/pkg/commands/run/command.go @@ -0,0 +1,278 @@ +package run + +import ( + "context" + "fmt" + "github.com/ekristen/gcp-nuke/pkg/gcputil" + "github.com/ekristen/gcp-nuke/pkg/nuke" + "github.com/ekristen/libnuke/pkg/scanner" + "github.com/ekristen/libnuke/pkg/types" + "github.com/gotidy/ptr" + "slices" + "strings" + "time" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/ekristen/gcp-nuke/pkg/commands/global" + "github.com/ekristen/gcp-nuke/pkg/common" + libconfig "github.com/ekristen/libnuke/pkg/config" + libnuke "github.com/ekristen/libnuke/pkg/nuke" + "github.com/ekristen/libnuke/pkg/registry" +) + +func execute(c *cli.Context) error { + ctx, cancel := context.WithCancel(c.Context) + defer cancel() + + gcp, err := gcputil.New(ctx, c.String("project-id")) + if err != nil { + return err + } + + //if !gcp.HasOrganizations() { + // return fmt.Errorf("no organizations found") + //} + + if !gcp.HasProjects() { + return fmt.Errorf("no projects found") + } + + logrus.Trace("preparing to run nuke") + + params := &libnuke.Parameters{ + Force: c.Bool("force"), + ForceSleep: c.Int("force-sleep"), + Quiet: c.Bool("quiet"), + NoDryRun: c.Bool("no-dry-run"), + Includes: c.StringSlice("include"), + Excludes: c.StringSlice("exclude"), + } + + parsedConfig, err := libconfig.New(libconfig.Options{ + Path: c.Path("config"), + Deprecations: registry.GetDeprecatedResourceTypeMapping(), + }) + if err != nil { + return err + } + + projectID := c.String("project-id") + + projectConfig := parsedConfig.Accounts[projectID] + + filters, err := parsedConfig.Filters(projectID) + if err != nil { + return err + } + + n := libnuke.New(params, filters, parsedConfig.Settings) + + n.SetRunSleep(5 * time.Second) + + projectResourceTypes := types.ResolveResourceTypes( + registry.GetNamesForScope(nuke.Project), + []types.Collection{ + n.Parameters.Includes, + parsedConfig.ResourceTypes.GetIncludes(), + projectConfig.ResourceTypes.GetIncludes(), + }, + []types.Collection{ + n.Parameters.Excludes, + parsedConfig.ResourceTypes.Excludes, + projectConfig.ResourceTypes.Excludes, + }, + nil, + nil, + ) + + /* + tenantConfig := parsedConfig.Accounts[c.String("tenant-id")] + tenantResourceTypes := types.ResolveResourceTypes( + registry.GetNamesForScope(nuke.Organization), + []types.Collection{ + n.Parameters.Includes, + parsedConfig.ResourceTypes.GetIncludes(), + tenantConfig.ResourceTypes.GetIncludes(), + }, + []types.Collection{ + n.Parameters.Excludes, + parsedConfig.ResourceTypes.Excludes, + tenantConfig.ResourceTypes.Excludes, + }, + nil, + nil, + ) + + subResourceTypes := types.ResolveResourceTypes( + registry.GetNamesForScope(nuke.Project), + []types.Collection{ + n.Parameters.Includes, + parsedConfig.ResourceTypes.GetIncludes(), + tenantConfig.ResourceTypes.GetIncludes(), + }, + []types.Collection{ + n.Parameters.Excludes, + parsedConfig.ResourceTypes.Excludes, + tenantConfig.ResourceTypes.Excludes, + }, + nil, + nil, + ) + + if slices.Contains(parsedConfig.Regions, "global") { + if err := n.RegisterScanner(nuke.Organization, scanner.New("tenant/all", tenantResourceTypes, &nuke.ListerOpts{})); err != nil { + return err + } + } + + logrus.Debug("registering scanner for tenant subscription resources") + for _, subscriptionId := range tenant.SubscriptionIds { + logrus.Debug("registering scanner for subscription resources") + if err := n.RegisterScanner(nuke.Subscription, libnuke.NewScanner("tenant/sub", subResourceTypes, &nuke.ListerOpts{ + Authorizers: tenant.Authorizers, + TenantId: tenant.ID, + SubscriptionId: subscriptionId, + })); err != nil { + return err + } + } + + + for _, region := range parsedConfig.Regions { + if region == "global" { + continue + } + + for _, subscriptionId := range tenant.SubscriptionIds { + logrus.Debug("registering scanner for subscription resources") + + for i, resourceGroup := range tenant.ResourceGroups[subscriptionId] { + logrus.Debugf("registering scanner for resource group resources: rg/%s", resourceGroup) + if err := n.RegisterScanner(nuke.ResourceGroup, libnuke.NewScanner(fmt.Sprintf("%s/rg%d", region, i), rgResourceTypes, &nuke.ListerOpts{ + Authorizers: tenant.Authorizers, + TenantId: tenant.ID, + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + Location: region, + })); err != nil { + return err + } + } + } + } + */ + + // GCP rest clients have to be closed, this ensures that they are closed properly + defer func() { + for _, l := range registry.GetListers() { + lc, ok := l.(registry.ListerWithClose) + if ok { + lc.Close() + } + } + }() + + if slices.Contains(parsedConfig.Regions, "all") { + parsedConfig.Regions = gcp.Regions + + logrus.Info( + `"all" detected in region list, only enabled regions and "global" will be used, all others ignored`) + + if len(parsedConfig.Regions) > 1 { + logrus.Warnf(`additional regions defined along with "all", these will be ignored!`) + } + + logrus.Infof("The following regions are enabled for the account (%d total):", len(parsedConfig.Regions)) + + printableRegions := make([]string, 0) + for i, region := range parsedConfig.Regions { + printableRegions = append(printableRegions, region) + if i%6 == 0 { // print 5 regions per line + logrus.Infof("> %s", strings.Join(printableRegions, ", ")) + printableRegions = make([]string, 0) + } else if i == len(parsedConfig.Regions)-1 { + logrus.Infof("> %s", strings.Join(printableRegions, ", ")) + } + } + } + + // Register the scanners for each region that is defined in the configuration. + for _, regionName := range parsedConfig.Regions { + if err := n.RegisterScanner(nuke.Project, scanner.New(regionName, projectResourceTypes, &nuke.ListerOpts{ + Project: ptr.String(projectID), + Region: ptr.String(regionName), + Zones: gcp.GetZones(regionName), + })); err != nil { + return err + } + } + + logrus.Debug("running ...") + + return n.Run(c.Context) +} + +func init() { + flags := []cli.Flag{ + &cli.PathFlag{ + Name: "config", + Usage: "path to config file", + Value: "config.yaml", + }, + &cli.StringSliceFlag{ + Name: "include", + Usage: "only include this specific resource", + }, + &cli.StringSliceFlag{ + Name: "exclude", + Usage: "exclude this specific resource (this overrides everything)", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "hide filtered messages from display", + }, + &cli.BoolFlag{ + Name: "no-dry-run", + Usage: "actually run the removal of the resources after discovery", + }, + &cli.BoolFlag{ + Name: "no-prompt", + Usage: "disable prompting for verification to run", + Aliases: []string{"force"}, + }, + &cli.IntFlag{ + Name: "prompt-delay", + Usage: "seconds to delay after prompt before running (minimum: 3 seconds)", + Value: 10, + Aliases: []string{"force-sleep"}, + }, + &cli.StringSliceFlag{ + Name: "feature-flag", + Usage: "enable experimental behaviors that may not be fully tested or supported", + }, + &cli.StringFlag{ + Name: "credentials-file", + Usage: "the credentials file to use for authentication", + EnvVars: []string{"GOOGLE_CREDENTIALS"}, + }, + &cli.StringFlag{ + Name: "project-id", + Usage: "which GCP project should be nuked", + Required: true, + }, + } + + cmd := &cli.Command{ + Name: "run", + Aliases: []string{"nuke"}, + Usage: "run nuke against a GCP project to remove all configured resources", + Flags: append(flags, global.Flags()...), + Before: global.Before, + Action: execute, + } + + common.RegisterCommand(cmd) +} diff --git a/pkg/common/commands.go b/pkg/common/commands.go new file mode 100644 index 0000000..1f44bfb --- /dev/null +++ b/pkg/common/commands.go @@ -0,0 +1,24 @@ +package common + +import ( + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var commands []*cli.Command + +// Commander -- +type Commander interface { + Execute(c *cli.Context) +} + +// RegisterCommand -- +func RegisterCommand(command *cli.Command) { + logrus.Debugln("Registering", command.Name, "command...") + commands = append(commands, command) +} + +// GetCommands -- +func GetCommands() []*cli.Command { + return commands +} diff --git a/pkg/common/version.go b/pkg/common/version.go new file mode 100644 index 0000000..fed48f3 --- /dev/null +++ b/pkg/common/version.go @@ -0,0 +1,32 @@ +package common + +// NAME of the App +var NAME = "gcp-nuke" + +// SUMMARY of the Version +var SUMMARY = "1.0.0-dev" + +// BRANCH of the Version +var BRANCH = "dev" + +var COMMIT = "dirty" + +// AppVersion -- +var AppVersion AppVersionInfo + +// AppVersionInfo -- +type AppVersionInfo struct { + Name string + Branch string + Summary string + Commit string +} + +func init() { + AppVersion = AppVersionInfo{ + Name: NAME, + Branch: BRANCH, + Summary: SUMMARY, + Commit: COMMIT, + } +} diff --git a/pkg/gcputil/client.go b/pkg/gcputil/client.go new file mode 100644 index 0000000..cf18134 --- /dev/null +++ b/pkg/gcputil/client.go @@ -0,0 +1,5 @@ +package gcputil + +type Client interface { + Close() error +} diff --git a/pkg/gcputil/firebase.go b/pkg/gcputil/firebase.go new file mode 100644 index 0000000..082ed1d --- /dev/null +++ b/pkg/gcputil/firebase.go @@ -0,0 +1,130 @@ +package gcputil + +import ( + "context" + "encoding/json" + "fmt" + "github.com/sirupsen/logrus" + "io" + "net/http" + + "golang.org/x/oauth2/google" +) + +// FirebaseDBClient is a client to interact with the Firebase Realtime Database API +type FirebaseDBClient struct { + httpClient *http.Client +} + +// DatabaseInstance represents a Firebase Realtime Database instance +type DatabaseInstance struct { + Name string `json:"name"` + Project string `json:"project"` + DatabaseURL string `json:"databaseUrl"` + Type string `json:"type"` + State string `json:"state"` +} + +// NewFirebaseDBClient creates a new Firebase Realtime Database client to interact with the +// https://firebasedatabase.googleapis.com endpoints as there is no official golang client library +func NewFirebaseDBClient(ctx context.Context) (*FirebaseDBClient, error) { + client, err := google.DefaultClient(ctx, + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/firebase", + "https://www.googleapis.com/auth/firebase.readonly", + ) + if err != nil { + return nil, fmt.Errorf("failed to parse JWT from credentials: %v", err) + } + + return &FirebaseDBClient{ + httpClient: client, + }, nil +} + +// ListDatabaseRegions lists Firebase Realtime Database regions +func (c *FirebaseDBClient) ListDatabaseRegions() []string { + return []string{ + "us-central1", + "europe-west1", + "asia-southeast1", + } +} + +// ListDatabaseInstances lists Firebase Realtime Database instances +func (c *FirebaseDBClient) ListDatabaseInstances(ctx context.Context, parent string) ([]*DatabaseInstance, error) { + url1 := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances", parent) + logrus.Tracef("url: %s", url1) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url1, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error requesting database instances: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + d, _ := io.ReadAll(resp.Body) + fmt.Println(string(d)) + return nil, fmt.Errorf("API request error: status %d", resp.StatusCode) + } + + var instances struct { + Instances []*DatabaseInstance `json:"instances"` + } + if err = json.NewDecoder(resp.Body).Decode(&instances); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + return instances.Instances, nil +} + +// DeleteDatabaseInstance deletes a Firebase Realtime Database instance +func (c *FirebaseDBClient) DeleteDatabaseInstance(ctx context.Context, parent, name string) error { + url := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances/%s", parent, name) + logrus.Tracef("url: %s", url) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("error deleting database instance: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request error: status %d", resp.StatusCode) + } + + return nil +} + +// DisableDatabaseInstance disables a Firebase Realtime Database instance +func (c *FirebaseDBClient) DisableDatabaseInstance(ctx context.Context, parent, name string) error { + url := fmt.Sprintf("https://firebasedatabase.googleapis.com/v1beta/%s/instances/%s:disable", parent, name) + logrus.Tracef("url: %s", url) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("error disabling database instance: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request error: status %d", resp.StatusCode) + } + + return nil +} diff --git a/pkg/gcputil/gcp.go b/pkg/gcputil/gcp.go new file mode 100644 index 0000000..017f334 --- /dev/null +++ b/pkg/gcputil/gcp.go @@ -0,0 +1,147 @@ +package gcputil + +import ( + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "context" + "errors" + "github.com/sirupsen/logrus" + "google.golang.org/api/cloudresourcemanager/v3" + "google.golang.org/api/iterator" + "log" + "strings" +) + +type Organization struct { + Name string + DisplayName string +} + +func (o *Organization) ID() string { + return strings.Split(o.Name, "organizations/")[1] +} + +type Project struct { + Name string + ProjectID string +} + +func (p *Project) ID() string { + return strings.Split(p.Name, "projects/")[1] +} + +type GCP struct { + Organizations []*Organization + Projects []*Project + Regions []string + + zones map[string][]string +} + +func (g *GCP) HasOrganizations() bool { + if g.Organizations == nil { + return false + } + return len(g.Organizations) > 0 +} + +func (g *GCP) HasProjects() bool { + if g.Projects == nil { + return false + } + return len(g.Projects) > 0 +} + +func (g *GCP) GetZones(region string) []string { + return g.zones[region] +} + +func New(ctx context.Context, projectID string) (*GCP, error) { + gcp := &GCP{ + Organizations: make([]*Organization, 0), + Projects: make([]*Project, 0), + Regions: []string{"global"}, + zones: make(map[string][]string), + } + + service, err := cloudresourcemanager.NewService(ctx) + if err != nil { + return nil, err + } + + req := service.Organizations.Search() + if resp, err := req.Do(); err != nil { + return nil, err + } else { + for _, org := range resp.Organizations { + newOrg := &Organization{ + Name: org.Name, + DisplayName: org.DisplayName, + } + + gcp.Organizations = append(gcp.Organizations, newOrg) + + logrus.WithFields(logrus.Fields{ + "name": newOrg.Name, + "displayName": newOrg.DisplayName, + "id": newOrg.ID(), + }).Trace("organization found") + } + } + + // Request to list projects + preq := service.Projects.Search() + if err := preq.Pages(ctx, func(page *cloudresourcemanager.SearchProjectsResponse) error { + for _, project := range page.Projects { + newProject := &Project{ + Name: project.Name, + ProjectID: project.ProjectId, + } + gcp.Projects = append(gcp.Projects, newProject) + + logrus.WithFields(logrus.Fields{ + "name": newProject.Name, + "project.id": newProject.ProjectID, + "id": newProject.ID(), + }).Trace("project found") + } + return nil + }); err != nil { + return nil, err + } + + c, err := compute.NewRegionsRESTClient(ctx) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + defer c.Close() + + // Build the request to list regions + regionReq := &computepb.ListRegionsRequest{ + Project: projectID, + } + + it := c.List(ctx, regionReq) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + return nil, err + } + + gcp.Regions = append(gcp.Regions, resp.GetName()) + + if gcp.zones[resp.GetName()] == nil { + gcp.zones[resp.GetName()] = make([]string, 0) + } + + for _, z := range resp.GetZones() { + zoneShort := strings.Split(z, "/")[len(strings.Split(z, "/"))-1] + gcp.zones[resp.GetName()] = append(gcp.zones[resp.GetName()], zoneShort) + } + } + + return gcp, nil +} diff --git a/pkg/nuke/resource.go b/pkg/nuke/resource.go new file mode 100644 index 0000000..b465698 --- /dev/null +++ b/pkg/nuke/resource.go @@ -0,0 +1,17 @@ +package nuke + +import ( + "github.com/ekristen/libnuke/pkg/registry" +) + +const ( + Workspace registry.Scope = "workspace" + Organization registry.Scope = "organization" + Project registry.Scope = "project" +) + +type ListerOpts struct { + Project *string + Region *string + Zones []string +} diff --git a/resources/cloud-function.go b/resources/cloud-function.go new file mode 100644 index 0000000..dc4f04e --- /dev/null +++ b/resources/cloud-function.go @@ -0,0 +1,144 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + "strings" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + "cloud.google.com/go/functions/apiv1" + "cloud.google.com/go/functions/apiv1/functionspb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const CloudFunctionResource = "CloudFunction" + +func init() { + registry.Register(®istry.Registration{ + Name: CloudFunctionResource, + Scope: nuke.Project, + Lister: &CloudFunctionLister{}, + }) +} + +type CloudFunctionLister struct { + svc *functions.CloudFunctionsClient +} + +func (l *CloudFunctionLister) Close() { + if l.svc != nil { + l.svc.Close() + } +} + +func (l *CloudFunctionLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = functions.NewCloudFunctionsRESTClient(ctx) + if err != nil { + return nil, err + } + + // TODO: determine locations, cache and skip locations not supported + } + + req := &functionspb.ListFunctionsRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region), + } + it := l.svc.ListFunctions(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate cloud functions") + break + } + + nameParts := strings.Split(resp.Name, "/") + name := nameParts[len(nameParts)-1] + + resources = append(resources, &CloudFunction{ + svc: l.svc, + FullName: ptr.String(resp.Name), + Name: ptr.String(name), + Project: opts.Project, + Region: opts.Region, + Labels: resp.Labels, + Status: ptr.String(resp.Status.String()), + }) + } + + return resources, nil +} + +type CloudFunction struct { + svc *functions.CloudFunctionsClient + removeOp *functions.DeleteFunctionOperation + Project *string + Region *string + FullName *string `property:"-"` + Name *string `property:"Name"` + Status *string + Labels map[string]string +} + +func (r *CloudFunction) Remove(ctx context.Context) (err error) { + r.removeOp, err = r.svc.DeleteFunction(ctx, &functionspb.DeleteFunctionRequest{ + Name: *r.FullName, + }) + if err != nil && strings.Contains(err.Error(), "proto") && strings.Contains(err.Error(), "missing") { + err = nil + } + if err != nil { + logrus.WithError(err).Debug("error encountered on remove") + } + return err +} + +func (r *CloudFunction) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *CloudFunction) String() string { + return *r.Name +} + +func (r *CloudFunction) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + if err := r.removeOp.Poll(ctx); err != nil { + logrus. + WithField("resource", CloudFunctionResource). + WithError(err). + Trace("remove op polling encountered error") + return err + } + + if !r.removeOp.Done() { + return fmt.Errorf("operation still in progress") + } + + return nil +} diff --git a/resources/cloud-function2.go b/resources/cloud-function2.go new file mode 100644 index 0000000..7005c6b --- /dev/null +++ b/resources/cloud-function2.go @@ -0,0 +1,149 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + "strings" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + "cloud.google.com/go/functions/apiv2" + "cloud.google.com/go/functions/apiv2/functionspb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const CloudFunction2Resource = "CloudFunction2" + +func init() { + registry.Register(®istry.Registration{ + Name: CloudFunction2Resource, + Scope: nuke.Project, + Lister: &CloudFunction2Lister{}, + }) +} + +type CloudFunction2Lister struct { + svc *functions.FunctionClient +} + +func (l *CloudFunction2Lister) Close() { + if l.svc != nil { + l.svc.Close() + } +} + +func (l *CloudFunction2Lister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = functions.NewFunctionRESTClient(ctx) + if err != nil { + return nil, err + } + + // TODO: determine locations, cache and skip locations not supported + } + + req := &functionspb.ListFunctionsRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region), + } + it := l.svc.ListFunctions(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate cloud functions") + break + } + + nameParts := strings.Split(resp.Name, "/") + name := nameParts[len(nameParts)-1] + + resources = append(resources, &CloudFunction2{ + svc: l.svc, + FullName: ptr.String(resp.Name), + Name: ptr.String(name), + Project: opts.Project, + Region: opts.Region, + Labels: resp.Labels, + State: ptr.String(resp.State.String()), + }) + } + + return resources, nil +} + +type CloudFunction2 struct { + svc *functions.FunctionClient + removeOp *functions.DeleteFunctionOperation + Project *string + Region *string + FullName *string `property:"-"` + Name *string `property:"Name"` + Labels map[string]string + State *string +} + +func (r *CloudFunction2) Remove(ctx context.Context) (err error) { + r.removeOp, err = r.svc.DeleteFunction(ctx, &functionspb.DeleteFunctionRequest{ + Name: *r.FullName, + }) + if err != nil && strings.Contains(err.Error(), "proto") && strings.Contains(err.Error(), "missing") { + err = nil + } + if err != nil { + logrus.WithError(err).Debug("error encountered on remove") + } + return err +} + +func (r *CloudFunction2) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *CloudFunction2) String() string { + return *r.Name +} + +func (r *CloudFunction2) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + if err := r.removeOp.Poll(ctx); err != nil { + if strings.Contains(err.Error(), "proto") && strings.Contains(err.Error(), "missing") { + err = nil + } + if err != nil { + logrus. + WithField("resource", CloudFunction2Resource). + WithError(err). + Trace("remove op polling encountered error") + return err + } + } + + if !r.removeOp.Done() { + return fmt.Errorf("operation still in progress") + } + + return nil +} diff --git a/resources/cloud-run.go b/resources/cloud-run.go new file mode 100644 index 0000000..efbbc1a --- /dev/null +++ b/resources/cloud-run.go @@ -0,0 +1,131 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + "strings" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + "cloud.google.com/go/run/apiv2" + "cloud.google.com/go/run/apiv2/runpb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const CloudRunResource = "CloudRun" + +func init() { + registry.Register(®istry.Registration{ + Name: CloudRunResource, + Scope: nuke.Project, + Lister: &CloudRunLister{}, + }) +} + +type CloudRunLister struct { + svc *run.ServicesClient +} + +func (l *CloudRunLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = run.NewServicesClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + req := &runpb.ListServicesRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region), + } + it := l.svc.ListServices(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + nameParts := strings.Split(resp.Name, "/") + name := nameParts[len(nameParts)-1] + + resources = append(resources, &CloudRun{ + svc: l.svc, + FullName: ptr.String(resp.Name), + Name: ptr.String(name), + Project: opts.Project, + Region: opts.Region, + Labels: resp.Labels, + }) + } + + return resources, nil +} + +type CloudRun struct { + svc *run.ServicesClient + removeOp *run.DeleteServiceOperation + Project *string + Region *string + FullName *string + Name *string + Labels map[string]string +} + +func (r *CloudRun) Filter() error { + if r.Labels != nil && r.Labels["goog-managed-by"] == "cloudfunctions" { + return errors.New("cannot remove cloud run that is managed by cloud functions") + } + + return nil +} + +func (r *CloudRun) Remove(ctx context.Context) (err error) { + r.removeOp, err = r.svc.DeleteService(ctx, &runpb.DeleteServiceRequest{ + Name: *r.FullName, + }) + return err +} + +func (r *CloudRun) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *CloudRun) String() string { + return *r.Name +} + +func (r *CloudRun) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + if _, err := r.removeOp.Poll(ctx); err != nil { + logrus.WithError(err).Trace("network remove op polling encountered error") + return err + } + + return nil +} diff --git a/resources/cloud-sql-instance.go b/resources/cloud-sql-instance.go new file mode 100644 index 0000000..35b5c77 --- /dev/null +++ b/resources/cloud-sql-instance.go @@ -0,0 +1,156 @@ +package resources + +import ( + "context" + "fmt" + "github.com/ekristen/libnuke/pkg/settings" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" + sqladmin "google.golang.org/api/sqladmin/v1beta4" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const CloudSQLInstanceResource = "CloudSQLInstance" + +func init() { + registry.Register(®istry.Registration{ + Name: CloudSQLInstanceResource, + Scope: nuke.Project, + Lister: &CloudSQLInstanceLister{}, + Settings: []string{ + "DisableDeletionProtection", + }, + }) +} + +type CloudSQLInstanceLister struct { + svc *sqladmin.Service +} + +func (l *CloudSQLInstanceLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = sqladmin.NewService(ctx) + if err != nil { + return nil, err + } + } + + resp, err := l.svc.Instances.List(*opts.Project).Context(ctx).Do() + if err != nil { + return nil, err + } + + for _, instance := range resp.Items { + if instance.Region != *opts.Region { + continue + } + + resources = append(resources, &CloudSQLInstance{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: ptr.String(instance.Name), + State: ptr.String(instance.State), + Labels: instance.Settings.UserLabels, + CreationDate: ptr.String(instance.CreateTime), + DatabaseVersion: ptr.String(instance.DatabaseVersion), + instanceSettings: instance.Settings, + }) + } + + return resources, nil +} + +type CloudSQLInstance struct { + svc *sqladmin.Service + deleteOp *sqladmin.Operation + settings *settings.Setting + + Project *string + Region *string + Name *string + State *string + Labels map[string]string + CreationDate *string + DatabaseVersion *string + + instanceSettings *sqladmin.Settings +} + +func (r *CloudSQLInstance) Settings(setting *settings.Setting) { + r.settings = setting +} + +func (r *CloudSQLInstance) Remove(ctx context.Context) (err error) { + if disableErr := r.disableDeletionProtection(ctx); disableErr != nil { + return disableErr + } + + r.deleteOp, err = r.svc.Instances.Delete(*r.Project, *r.Name).Context(ctx).Do() + return err +} + +func (r *CloudSQLInstance) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *CloudSQLInstance) String() string { + return *r.Name +} + +func (r *CloudSQLInstance) HandleWait(ctx context.Context) error { + if r.deleteOp == nil { + return nil + } + + if op, err := r.svc.Operations.Get(*r.Project, r.deleteOp.Name).Context(ctx).Do(); err == nil { + if op.Status == "DONE" { + if op.Error != nil { + return fmt.Errorf("delete error on '%s': %s", op.TargetLink, op.Error.Errors[0].Message) + } + } + } else { + return err + } + + return nil +} + +func (r *CloudSQLInstance) disableDeletionProtection(ctx context.Context) error { + if r.settings != nil && r.settings.Get("DisableDeletionProtection").(bool) { + logrus.Trace("disabling deletion protection") + + r.instanceSettings.DeletionProtectionEnabled = false + op, err := r.svc.Instances.Update(*r.Project, *r.Name, &sqladmin.DatabaseInstance{ + Settings: r.instanceSettings, + }).Context(ctx).Do() + if err != nil { + return err + } + + for { + op, err = r.svc.Operations.Get(*r.Project, op.Name).Context(ctx).Do() + if err != nil { + return err + } + if op.Status == "DONE" { + break + } + } + } + return nil +} diff --git a/resources/compute-common-instance-metadata.go b/resources/compute-common-instance-metadata.go new file mode 100644 index 0000000..b8e545b --- /dev/null +++ b/resources/compute-common-instance-metadata.go @@ -0,0 +1,130 @@ +package resources + +import ( + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "context" + "fmt" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" + "strings" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const ComputeCommonInstanceMetadataResource = "ComputeCommonInstanceMetadata" + +func init() { + registry.Register(®istry.Registration{ + Name: ComputeCommonInstanceMetadataResource, + Scope: nuke.Project, + Lister: &ComputeCommonInstanceMetadataLister{}, + }) +} + +type ComputeCommonInstanceMetadataLister struct { + svc *compute.ProjectsClient +} + +func (l *ComputeCommonInstanceMetadataLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewProjectsRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &computepb.GetProjectRequest{ + Project: *opts.Project, + } + + proj, err := l.svc.Get(ctx, req) + if err != nil { + return nil, err + } + + resources = append(resources, &ComputeCommonInstanceMetadata{ + svc: l.svc, + Project: proj.Name, + Fingerprint: proj.CommonInstanceMetadata.Fingerprint, + Items: proj.CommonInstanceMetadata.Items, + }) + + return resources, nil +} + +type ComputeCommonInstanceMetadata struct { + svc *compute.ProjectsClient + removeOp *compute.Operation + Project *string + Fingerprint *string + Items []*computepb.Items +} + +func (r *ComputeCommonInstanceMetadata) Filter() error { + if *r.Fingerprint == "n0oRtEB90Lw=" { + return fmt.Errorf("already in default configuration") + } + return nil +} + +func (r *ComputeCommonInstanceMetadata) Remove(ctx context.Context) (err error) { + r.removeOp, err = r.svc.SetCommonInstanceMetadata(ctx, &computepb.SetCommonInstanceMetadataProjectRequest{ + Project: *r.Project, + MetadataResource: &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: ptr.String("enable-oslogin"), + Value: ptr.String("true"), + }, + }, + }, + }) + return err +} + +func (r *ComputeCommonInstanceMetadata) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *ComputeCommonInstanceMetadata) String() string { + return "common-instance-metadata" +} + +func (r *ComputeCommonInstanceMetadata) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + if err := r.removeOp.Poll(ctx); err != nil { + if strings.Contains(err.Error(), "proto") && strings.Contains(err.Error(), "missing") { + err = nil + } + if err != nil { + logrus. + WithField("resource", ComputeCommonInstanceMetadataResource). + WithError(err). + Trace("remove op polling encountered error") + return err + } + } + + if !r.removeOp.Done() { + return fmt.Errorf("operation still in progress") + } + + return nil +} diff --git a/resources/compute-disk.go b/resources/compute-disk.go new file mode 100644 index 0000000..2f3e96e --- /dev/null +++ b/resources/compute-disk.go @@ -0,0 +1,108 @@ +package resources + +import ( + "context" + "errors" + "github.com/gotidy/ptr" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const ComputeDiskResource = "ComputeDisk" + +func init() { + registry.Register(®istry.Registration{ + Name: ComputeDiskResource, + Scope: nuke.Project, + Lister: &ComputeDiskLister{}, + }) +} + +type ComputeDiskLister struct { + svc *compute.DisksClient +} + +func (l *ComputeDiskLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewDisksRESTClient(ctx) + if err != nil { + return nil, err + } + } + + for _, zone := range opts.Zones { + req := &computepb.ListDisksRequest{ + Project: *opts.Project, + Zone: zone, + } + + it := l.svc.List(ctx, req) + + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate compute disks") + break + } + + resources = append(resources, &ComputeDisk{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + Zone: ptr.String(zone), + Labels: resp.Labels, + }) + } + } + + return resources, nil +} + +type ComputeDisk struct { + svc *compute.DisksClient + Project *string + Region *string + Name *string + Zone *string + Labels map[string]string +} + +func (r *ComputeDisk) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteDiskRequest{ + Project: *r.Project, + Zone: *r.Zone, + Disk: *r.Name, + }) + return err +} + +func (r *ComputeDisk) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *ComputeDisk) String() string { + return *r.Name +} diff --git a/resources/compute-firewall.go b/resources/compute-firewall.go new file mode 100644 index 0000000..ee83d99 --- /dev/null +++ b/resources/compute-firewall.go @@ -0,0 +1,99 @@ +package resources + +import ( + "context" + "errors" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const ComputeFirewallResource = "ComputeFirewall" + +func init() { + registry.Register(®istry.Registration{ + Name: ComputeFirewallResource, + Scope: nuke.Project, + Lister: &ComputeFirewallLister{}, + }) +} + +type ComputeFirewallLister struct { + svc *compute.FirewallsClient +} + +func (l *ComputeFirewallLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewFirewallsRESTClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + req := &computepb.ListFirewallsRequest{ + Project: *opts.Project, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &ComputeFirewall{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + }) + } + + return resources, nil +} + +type ComputeFirewall struct { + svc *compute.FirewallsClient + Project *string + Region *string + Name *string +} + +func (r *ComputeFirewall) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteFirewallRequest{ + Project: *r.Project, + Firewall: *r.Name, + }) + return err +} + +func (r *ComputeFirewall) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *ComputeFirewall) String() string { + return *r.Name +} diff --git a/resources/compute-instance.go b/resources/compute-instance.go new file mode 100644 index 0000000..50e9464 --- /dev/null +++ b/resources/compute-instance.go @@ -0,0 +1,109 @@ +package resources + +import ( + "context" + "errors" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const ComputeInstanceResource = "ComputeInstance" + +func init() { + registry.Register(®istry.Registration{ + Name: ComputeInstanceResource, + Scope: nuke.Project, + Lister: &ComputeInstanceLister{}, + }) +} + +type ComputeInstanceLister struct { + svc *compute.InstancesClient +} + +func (l *ComputeInstanceLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional and zonal") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewInstancesRESTClient(ctx) + if err != nil { + return nil, err + } + } + + for _, zone := range opts.Zones { + req := &computepb.ListInstancesRequest{ + Project: *opts.Project, + Zone: zone, + } + + it := l.svc.List(ctx, req) + + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate compute instances") + break + } + + resources = append(resources, &ComputeInstance{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + Zone: ptr.String(zone), + CreationTimestamp: resp.CreationTimestamp, + Labels: resp.Labels, + }) + } + } + + return resources, nil +} + +type ComputeInstance struct { + svc *compute.InstancesClient + Project *string + Region *string + Name *string + Zone *string + CreationTimestamp *string + Labels map[string]string +} + +func (r *ComputeInstance) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteInstanceRequest{ + Project: *r.Project, + Zone: *r.Zone, + Instance: *r.Name, + }) + return err +} + +func (r *ComputeInstance) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *ComputeInstance) String() string { + return *r.Name +} diff --git a/resources/firebase-realtime-database.go b/resources/firebase-realtime-database.go new file mode 100644 index 0000000..0e6c942 --- /dev/null +++ b/resources/firebase-realtime-database.go @@ -0,0 +1,172 @@ +package resources + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/gotidy/ptr" + + firebase "firebase.google.com/go" + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/settings" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/gcputil" + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const FirebaseRealtimeDatabaseResource = "FirebaseRealtimeDatabase" + +func init() { + registry.Register(®istry.Registration{ + Name: FirebaseRealtimeDatabaseResource, + Scope: nuke.Project, + Lister: &FirebaseRealtimeDatabaseLister{}, + Settings: []string{ + "EmptyDefaultDatabase", + }, + }) +} + +type FirebaseRealtimeDatabaseLister struct { + svc *gcputil.FirebaseDBClient +} + +func (l *FirebaseRealtimeDatabaseLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = gcputil.NewFirebaseDBClient(ctx) + if err != nil { + return nil, err + } + } + + supportedRegions := l.svc.ListDatabaseRegions() + if !slices.Contains(supportedRegions, *opts.Region) { + return nil, liberror.ErrSkipRequest("region is not supported") + } + + resp, err := l.svc.ListDatabaseInstances(ctx, fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region)) + if err != nil { + return nil, err + } + + for _, instance := range resp { + nameParts := strings.Split(instance.Name, "/") + name := nameParts[len(nameParts)-1] + + if instance.Type == "DEFAULT_DATABASE" && instance.State == "DISABLED" { + continue + } + + resources = append(resources, &FirebaseRealtimeDatabase{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: ptr.String(name), + FullName: ptr.String(instance.Name), + Type: ptr.String(instance.Type), + State: ptr.String(instance.State), + URL: ptr.String(instance.DatabaseURL), + }) + + } + + return resources, nil +} + +type FirebaseRealtimeDatabase struct { + svc *gcputil.FirebaseDBClient + settings *settings.Setting + Project *string + Region *string + Name *string + FullName *string `property:"-"` + Type *string + State *string + URL *string +} + +func (r *FirebaseRealtimeDatabase) Remove(ctx context.Context) error { + if err := r.EmptyDefaultDatabase(ctx); err != nil { + return err + } + + if err := r.DisableDatabaseInstance(ctx); err != nil { + return err + } + + return r.DeleteDatabaseInstance(ctx) +} + +func (r *FirebaseRealtimeDatabase) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *FirebaseRealtimeDatabase) Settings(settings *settings.Setting) { + r.settings = settings +} + +func (r *FirebaseRealtimeDatabase) String() string { + return *r.Name +} + +func (r *FirebaseRealtimeDatabase) DeleteDatabaseInstance(ctx context.Context) error { + // If it is the default database, it cannot be deleted only disabled. + if *r.Type == "DEFAULT_DATABASE" { + return nil + } + + return r.svc.DeleteDatabaseInstance(ctx, + fmt.Sprintf("projects/%s/locations/%s", *r.Project, *r.Region), *r.Name) +} + +func (r *FirebaseRealtimeDatabase) EmptyDefaultDatabase(ctx context.Context) error { + if r.settings == nil { + return nil + } + + // If it is not the default database then we just skip + if *r.Type != "DEFAULT_DATABASE" { + return nil + } + + // If the setting is not enabled, then we just skip + if !r.settings.Get("EmptyDefaultDatabase").(bool) { + return nil + } + + firebaseApp, err := firebase.NewApp(ctx, &firebase.Config{ + DatabaseURL: *r.URL, + }) + if err != nil { + return err + } + + firebaseDb, err := firebaseApp.Database(ctx) + if err != nil { + return err + } + + return firebaseDb.NewRef("/").Delete(ctx) +} + +func (r *FirebaseRealtimeDatabase) DisableDatabaseInstance(ctx context.Context) error { + if err := r.svc.DisableDatabaseInstance(ctx, + fmt.Sprintf("projects/%s/locations/%s", *r.Project, *r.Region), *r.Name); err != nil { + return err + } + + return nil +} diff --git a/resources/firebase-web-app.go b/resources/firebase-web-app.go new file mode 100644 index 0000000..3179890 --- /dev/null +++ b/resources/firebase-web-app.go @@ -0,0 +1,85 @@ +package resources + +import ( + "context" + "fmt" + "github.com/gotidy/ptr" + "google.golang.org/api/firebase/v1beta1" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const FirebaseWebAppResource = "FirebaseWebApp" + +func init() { + registry.Register(®istry.Registration{ + Name: FirebaseWebAppResource, + Scope: nuke.Project, + Lister: &FirebaseWebAppLister{}, + }) +} + +type FirebaseWebAppLister struct { + svc *firebase.Service +} + +func (l *FirebaseWebAppLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = firebase.NewService(ctx) + if err != nil { + return nil, err + } + } + + resp, err := l.svc.Projects.WebApps.List(fmt.Sprintf("projects/%s", *opts.Project)).Context(ctx).Do() + if err != nil { + return nil, err + } + + for _, app := range resp.Apps { + resources = append(resources, &FirebaseWebApp{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: ptr.String(app.Name), + }) + } + + return resources, nil +} + +type FirebaseWebApp struct { + svc *firebase.Service + Project *string + Region *string + Name *string +} + +func (r *FirebaseWebApp) Remove(ctx context.Context) error { + _, err := r.svc.Projects.WebApps.Remove(*r.Name, &firebase.RemoveWebAppRequest{ + AllowMissing: true, + Immediate: true, + }).Context(ctx).Do() + return err +} + +func (r *FirebaseWebApp) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *FirebaseWebApp) String() string { + return *r.Name +} diff --git a/resources/gke-cluster.go b/resources/gke-cluster.go new file mode 100644 index 0000000..1af3642 --- /dev/null +++ b/resources/gke-cluster.go @@ -0,0 +1,151 @@ +package resources + +import ( + "context" + "fmt" + "github.com/gotidy/ptr" + "strings" + + "cloud.google.com/go/container/apiv1" + "cloud.google.com/go/container/apiv1/containerpb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const GKEClusterResource = "GKECluster" + +func init() { + registry.Register(®istry.Registration{ + Name: GKEClusterResource, + Scope: nuke.Project, + Lister: &GKEClusterLister{}, + }) +} + +type GKEClusterLister struct { + svc *container.ClusterManagerClient +} + +func (l *GKEClusterLister) ListClusters(ctx context.Context, project, location string) ([]resource.Resource, error) { + var resources []resource.Resource + + req := &containerpb.ListClustersRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", project, location), + } + + resp, err := l.svc.ListClusters(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to list GKE clusters: %v", err) + } + + for _, cluster := range resp.Clusters { + region := location + zone := "" + if len(strings.Split(location, "-")) > 2 { + region = strings.Join(strings.Split(location, "-")[:2], "-") + zone = location + } + + resources = append(resources, &GKECluster{ + svc: l.svc, + Project: ptr.String(project), + Region: ptr.String(region), + Name: ptr.String(cluster.Name), + Zone: ptr.String(zone), + Status: ptr.String(cluster.Status.String()), + }) + } + + return resources, nil +} + +func (l *GKEClusterLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional and zonal") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = container.NewClusterManagerClient(ctx) + if err != nil { + return nil, err + } + } + + locations := []string{*opts.Region} + locations = append(locations, opts.Zones...) + + for _, loc := range locations { + clusters, err := l.ListClusters(ctx, *opts.Project, loc) + if err != nil { + return nil, err + } + resources = append(resources, clusters...) + } + + return resources, nil +} + +type GKECluster struct { + svc *container.ClusterManagerClient + removeOp *containerpb.Operation + Project *string + Region *string + Name *string + Zone *string + Status *string +} + +func (r *GKECluster) Remove(ctx context.Context) error { + var err error + location := r.Region + if *r.Zone != "" { + location = r.Zone + } + + r.removeOp, err = r.svc.DeleteCluster(ctx, &containerpb.DeleteClusterRequest{ + Name: fmt.Sprintf("projects/%s/locations/%s/clusters/%s", *r.Project, *location, *r.Name), + }) + return err +} + +func (r *GKECluster) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *GKECluster) String() string { + return *r.Name +} + +func (r *GKECluster) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + // TODO: this does not properly handle the wait it somehow ends in an error. + var err error + r.removeOp, err = r.svc.GetOperation(ctx, &containerpb.GetOperationRequest{ + Name: r.removeOp.Name, + }) + if err != nil { + return err + } + + if r.removeOp.Status != containerpb.Operation_DONE { + return liberror.ErrWaitResource("waiting for operation to complete") + } + + if r.removeOp.GetError() != nil { + return fmt.Errorf("operation failed: %v", r.removeOp.GetError()) + } + + return nil +} diff --git a/resources/iam-role.go b/resources/iam-role.go new file mode 100644 index 0000000..26f64c0 --- /dev/null +++ b/resources/iam-role.go @@ -0,0 +1,110 @@ +package resources + +import ( + "context" + "fmt" + "github.com/gotidy/ptr" + "strings" + + iamadmin "cloud.google.com/go/iam/admin/apiv1" + "cloud.google.com/go/iam/admin/apiv1/adminpb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const IAMRoleResource = "IAMRole" + +func init() { + registry.Register(®istry.Registration{ + Name: IAMRoleResource, + Scope: nuke.Project, + Lister: &IAMRoleLister{}, + }) +} + +type IAMRoleLister struct { + svc *iamadmin.IamClient +} + +func (l *IAMRoleLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = iamadmin.NewIamClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + pageToken := "" + req := &adminpb.ListRolesRequest{ + Parent: fmt.Sprintf("projects/%s", *opts.Project), + PageToken: pageToken, + } + + for { + resp, err := l.svc.ListRoles(ctx, req) + if err != nil { + return nil, err + } + + for _, role := range resp.GetRoles() { + roleParts := strings.Split(role.GetName(), "/") + roleName := roleParts[len(roleParts)-1] + resources = append(resources, &IAMRole{ + svc: l.svc, + Project: opts.Project, + Name: ptr.String(roleName), + Etag: role.Etag, + Stage: ptr.String(role.GetStage().String()), + Deleted: ptr.Bool(role.Deleted), + }) + } + + if resp.GetNextPageToken() == "" { + break + } + + req.PageToken = resp.GetNextPageToken() + } + + return resources, nil +} + +type IAMRole struct { + svc *iamadmin.IamClient + Project *string + Name *string + Stage *string + Etag []byte + Deleted *bool +} + +func (r *IAMRole) Remove(ctx context.Context) error { + _, err := r.svc.DeleteRole(ctx, &adminpb.DeleteRoleRequest{ + Name: fmt.Sprintf("projects/%s/roles/%s", *r.Project, *r.Name), + Etag: r.Etag, + }) + return err +} + +func (r *IAMRole) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *IAMRole) String() string { + return *r.Name +} diff --git a/resources/iam-service-account.go b/resources/iam-service-account.go new file mode 100644 index 0000000..fa0f55f --- /dev/null +++ b/resources/iam-service-account.go @@ -0,0 +1,128 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/ekristen/libnuke/pkg/settings" + "github.com/gotidy/ptr" + "strings" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + iamadmin "cloud.google.com/go/iam/admin/apiv1" + "cloud.google.com/go/iam/admin/apiv1/adminpb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const IAMServiceAccountResource = "IAMServiceAccount" + +func init() { + registry.Register(®istry.Registration{ + Name: IAMServiceAccountResource, + Scope: nuke.Project, + Lister: &IAMServiceAccountLister{}, + Settings: []string{ + "DeleteDefaultServiceAccounts", + }, + }) +} + +type IAMServiceAccountLister struct { + svc *iamadmin.IamClient +} + +func (l *IAMServiceAccountLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = iamadmin.NewIamClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + req := &adminpb.ListServiceAccountsRequest{ + Name: fmt.Sprintf("projects/%s", *opts.Project), + } + it := l.svc.ListServiceAccounts(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + nameParts := strings.Split(resp.Name, "/") + name := nameParts[len(nameParts)-1] + + resources = append(resources, &IAMServiceAccount{ + svc: l.svc, + Project: opts.Project, + Name: ptr.String(name), + FullName: ptr.String(resp.Name), + Description: ptr.String(resp.Description), + }) + } + + return resources, nil +} + +type IAMServiceAccount struct { + svc *iamadmin.IamClient + settings *settings.Setting + Project *string + Region *string + Name *string + FullName *string `property:"-"` + Description *string +} + +func (r *IAMServiceAccount) Filter() error { + deleteDefaultServiceAccounts := false + if r.settings != nil && r.settings.Get("DeleteDefaultServiceAccounts").(bool) { + deleteDefaultServiceAccounts = true + } + if !strings.Contains(*r.Name, ".iam.gserviceaccount.com") && !deleteDefaultServiceAccounts { + return fmt.Errorf("will not remove default service account") + } + + return nil +} + +func (r *IAMServiceAccount) Remove(ctx context.Context) error { + return r.svc.DeleteServiceAccount(ctx, &adminpb.DeleteServiceAccountRequest{ + Name: *r.Name, + }) +} + +func (r *IAMServiceAccount) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *IAMServiceAccount) String() string { + return *r.Name +} + +func (r *IAMServiceAccount) Settings(settings *settings.Setting) { + r.settings = settings +} diff --git a/resources/iam-workload-identity-pool-provider.go b/resources/iam-workload-identity-pool-provider.go new file mode 100644 index 0000000..64611d3 --- /dev/null +++ b/resources/iam-workload-identity-pool-provider.go @@ -0,0 +1,118 @@ +package resources + +import ( + "context" + "github.com/ekristen/gcp-nuke/pkg/nuke" + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + "github.com/gotidy/ptr" + "google.golang.org/api/iam/v1" + "strings" +) + +const IAMWorkloadIdentityPoolProviderProviderResource = "IAMWorkloadIdentityPoolProvider" + +func init() { + registry.Register(®istry.Registration{ + Name: IAMWorkloadIdentityPoolProviderProviderResource, + Scope: nuke.Project, + Lister: &IAMWorkloadIdentityPoolProviderLister{}, + }) +} + +type IAMWorkloadIdentityPoolProviderLister struct { + svc *iam.Service +} + +func (l *IAMWorkloadIdentityPoolProviderLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = iam.NewService(ctx) + if err != nil { + return nil, err + } + } + + workloadIdentityPoolLister := &IAMWorkloadIdentityPoolLister{} + workloadIdentityPools, err := workloadIdentityPoolLister.ListPools(ctx, opts) + if err != nil { + return nil, err + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + for _, workloadIdentityPool := range workloadIdentityPools { + var nextPageToken string + + for { + call := l.svc.Projects.Locations.WorkloadIdentityPools.Providers.List(workloadIdentityPool.Name) + if nextPageToken != "" { + call.PageToken(nextPageToken) + } + + resp, err := call.Context(ctx).Do() + if err != nil { + return nil, err + } + + for _, provider := range resp.WorkloadIdentityPoolProviders { + providerNameParts := strings.Split(provider.Name, "/") + providerName := providerNameParts[len(providerNameParts)-1] + + poolNameParts := strings.Split(workloadIdentityPool.Name, "/") + poolName := poolNameParts[len(poolNameParts)-1] + + resources = append(resources, &IAMWorkloadIdentityPoolProvider{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: ptr.String(providerName), + Pool: ptr.String(poolName), + Disabled: ptr.Bool(provider.Disabled), + DisplayName: ptr.String(provider.DisplayName), + ExpireTime: ptr.String(provider.ExpireTime), + }) + } + + nextPageToken = resp.NextPageToken + if nextPageToken == "" { + break + } + } + } + + return resources, nil +} + +type IAMWorkloadIdentityPoolProvider struct { + svc *iam.Service + Project *string + Region *string + Name *string + Pool *string + Disabled *bool + DisplayName *string + ExpireTime *string +} + +func (r *IAMWorkloadIdentityPoolProvider) Remove(ctx context.Context) error { + _, err := r.svc.Projects.Locations.WorkloadIdentityPools.Delete(*r.Name).Context(ctx).Do() + return err +} + +func (r *IAMWorkloadIdentityPoolProvider) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *IAMWorkloadIdentityPoolProvider) String() string { + return *r.Name +} diff --git a/resources/iam-workload-identity-pool.go b/resources/iam-workload-identity-pool.go new file mode 100644 index 0000000..4edcf3f --- /dev/null +++ b/resources/iam-workload-identity-pool.go @@ -0,0 +1,111 @@ +package resources + +import ( + "context" + "fmt" + "github.com/ekristen/gcp-nuke/pkg/nuke" + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + "github.com/gotidy/ptr" + "google.golang.org/api/iam/v1" + "strings" +) + +const IAMWorkloadIdentityPoolResource = "IAMWorkloadIdentityPool" + +func init() { + registry.Register(®istry.Registration{ + Name: IAMWorkloadIdentityPoolResource, + Scope: nuke.Project, + Lister: &IAMWorkloadIdentityPoolLister{}, + }) +} + +type IAMWorkloadIdentityPoolLister struct { + svc *iam.Service +} + +func (l *IAMWorkloadIdentityPoolLister) ListPools(ctx context.Context, opts *nuke.ListerOpts) ([]*iam.WorkloadIdentityPool, error) { + if l.svc == nil { + var err error + l.svc, err = iam.NewService(ctx) + if err != nil { + return nil, err + } + } + + resourceName := fmt.Sprintf("projects/%s/locations/global", *opts.Project) + var nextPageToken string + + var allPools []*iam.WorkloadIdentityPool + + for { + call := l.svc.Projects.Locations.WorkloadIdentityPools.List(resourceName) + if nextPageToken != "" { + call.PageToken(nextPageToken) + } + + resp, err := call.Context(ctx).Do() + if err != nil { + return nil, err + } + + allPools = append(allPools, resp.WorkloadIdentityPools...) + + nextPageToken = resp.NextPageToken + if nextPageToken == "" { + break + } + } + + return allPools, nil +} + +func (l *IAMWorkloadIdentityPoolLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + workloadIdentityPools, err := l.ListPools(ctx, opts) + if err != nil { + return nil, err + } + + for _, pool := range workloadIdentityPools { + poolNameParts := strings.Split(pool.Name, "/") + poolName := poolNameParts[len(poolNameParts)-1] + resources = append(resources, &IAMWorkloadIdentityPool{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: ptr.String(poolName), + }) + } + + return resources, nil +} + +type IAMWorkloadIdentityPool struct { + svc *iam.Service + Project *string + Region *string + Name *string +} + +func (r *IAMWorkloadIdentityPool) Remove(ctx context.Context) error { + _, err := r.svc.Projects.Locations.WorkloadIdentityPools.Delete(*r.Name).Context(ctx).Do() + return err +} + +func (r *IAMWorkloadIdentityPool) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *IAMWorkloadIdentityPool) String() string { + return *r.Name +} diff --git a/resources/kms-key.go b/resources/kms-key.go new file mode 100644 index 0000000..f9482a3 --- /dev/null +++ b/resources/kms-key.go @@ -0,0 +1,133 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const KMSKeyResource = "KMSKey" + +func init() { + registry.Register(®istry.Registration{ + Name: KMSKeyResource, + Scope: nuke.Project, + Lister: &KMSKeyLister{}, + }) +} + +type KMSKeyLister struct { + svc *kms.KeyManagementClient +} + +func (l *KMSKeyLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = kms.NewKeyManagementRESTClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the aws sdk instead is a jumping off point + req := &kmspb.ListKeyRingsRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", *opts.Project, *opts.Region), + } + it := l.svc.ListKeyRings(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &KMSKey{ + svc: l.svc, + Name: ptr.String(resp.Name), + Project: opts.Project, + }) + } + + return resources, nil +} + +type KMSKey struct { + svc *kms.KeyManagementClient + Project *string + Region *string + Name *string +} + +func (r *KMSKey) Remove(ctx context.Context) error { + reqKeyVersions := &kmspb.ListCryptoKeyVersionsRequest{ + Parent: *r.Name, + } + itKeyVersions := r.svc.ListCryptoKeyVersions(ctx, reqKeyVersions) + for { + respKeyVersions, err := itKeyVersions.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + if isDestroyed(respKeyVersions) { + continue + } + + reqKeyVersion := &kmspb.DestroyCryptoKeyVersionRequest{ + Name: respKeyVersions.Name, + } + _, err = r.svc.DestroyCryptoKeyVersion(ctx, reqKeyVersion) + if err != nil { + return err + } + } + + return nil +} + +func (r *KMSKey) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *KMSKey) String() string { + return *r.Name +} + +// isDestroyed checks if a key version is destroyed +func isDestroyed(keyVersion *kmspb.CryptoKeyVersion) bool { + if keyVersion == nil { + return true + } + return keyVersion.State == kmspb.CryptoKeyVersion_DESTROYED || + keyVersion.State == kmspb.CryptoKeyVersion_DESTROY_SCHEDULED +} diff --git a/resources/secretmanager-secret.go b/resources/secretmanager-secret.go new file mode 100644 index 0000000..6fd0b05 --- /dev/null +++ b/resources/secretmanager-secret.go @@ -0,0 +1,108 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + "strings" + "time" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const SecretManagerSecretResource = "SecretManagerSecret" + +func init() { + registry.Register(®istry.Registration{ + Name: SecretManagerSecretResource, + Scope: nuke.Project, + Lister: &SecretManagerSecretLister{}, + }) +} + +type SecretManagerSecretLister struct { + svc *secretmanager.Client +} + +func (l *SecretManagerSecretLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = secretmanager.NewRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &secretmanagerpb.ListSecretsRequest{ + Parent: fmt.Sprintf("projects/%s", *opts.Project), + } + it := l.svc.ListSecrets(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + nameParts := strings.Split(resp.Name, "/") + name := nameParts[len(nameParts)-1] + + resources = append(resources, &SecretManagerSecret{ + svc: l.svc, + FullName: ptr.String(resp.Name), + Name: ptr.String(name), + Project: opts.Project, + CreateTime: resp.CreateTime.AsTime(), + Labels: resp.Labels, + }) + } + + return resources, nil +} + +type SecretManagerSecret struct { + svc *secretmanager.Client + Project *string + Region *string + FullName *string + Name *string + CreateTime time.Time + Labels map[string]string +} + +func (r *SecretManagerSecret) Remove(ctx context.Context) error { + return r.svc.DeleteSecret(ctx, &secretmanagerpb.DeleteSecretRequest{ + Name: *r.FullName, + }) +} + +func (r *SecretManagerSecret) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *SecretManagerSecret) String() string { + return *r.Name +} diff --git a/resources/storage-bucket-object.go b/resources/storage-bucket-object.go new file mode 100644 index 0000000..b893e2e --- /dev/null +++ b/resources/storage-bucket-object.go @@ -0,0 +1,102 @@ +package resources + +import ( + "context" + "errors" + "github.com/gotidy/ptr" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + "cloud.google.com/go/storage" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const StorageBucketObjectResource = "StorageBucketObject" + +func init() { + registry.Register(®istry.Registration{ + Name: StorageBucketObjectResource, + Scope: nuke.Project, + Lister: &StorageBucketObjectLister{}, + }) +} + +type StorageBucketObjectLister struct { + svc *storage.Client +} + +func (l *StorageBucketObjectLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = storage.NewClient(ctx) + if err != nil { + return nil, err + } + } + + bucketLister := &StorageBucketLister{} + buckets, err := bucketLister.ListBuckets(ctx, opts) + if err != nil { + return nil, err + } + + for _, bucket := range buckets { + it := l.svc.Bucket(bucket.Name).Objects(ctx, nil) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &StorageBucketObject{ + svc: l.svc, + Name: ptr.String(resp.Name), + Bucket: ptr.String(bucket.Name), + Project: opts.Project, + Metadata: resp.Metadata, + }) + } + } + + return resources, nil +} + +type StorageBucketObject struct { + svc *storage.Client + Project *string + Region *string + Name *string + Bucket *string + Metadata map[string]string +} + +func (r *StorageBucketObject) Remove(ctx context.Context) error { + return r.svc.Bucket(*r.Bucket).Object(*r.Name).Delete(ctx) +} + +func (r *StorageBucketObject) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *StorageBucketObject) String() string { + return *r.Name +} diff --git a/resources/storage-bucket.go b/resources/storage-bucket.go new file mode 100644 index 0000000..79c622c --- /dev/null +++ b/resources/storage-bucket.go @@ -0,0 +1,117 @@ +package resources + +import ( + "context" + "errors" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + "cloud.google.com/go/storage" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const StorageBucketResource = "StorageBucket" + +func init() { + registry.Register(®istry.Registration{ + Name: StorageBucketResource, + Scope: nuke.Project, + Lister: &StorageBucketLister{}, + }) +} + +type StorageBucketLister struct { + svc *storage.Client +} + +func (l *StorageBucketLister) Close() { + if l.svc != nil { + l.svc.Close() + } +} + +func (l *StorageBucketLister) ListBuckets(ctx context.Context, opts *nuke.ListerOpts) ([]*storage.BucketAttrs, error) { + if l.svc == nil { + var err error + l.svc, err = storage.NewClient(ctx) + if err != nil { + return nil, err + } + } + + var allBuckets []*storage.BucketAttrs + + it := l.svc.Buckets(ctx, *opts.Project) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + allBuckets = append(allBuckets, resp) + } + + return allBuckets, nil +} + +func (l *StorageBucketLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + buckets, err := l.ListBuckets(ctx, opts) + if err != nil { + return nil, err + } + + for _, bucket := range buckets { + if bucket.Location != *opts.Region { + continue + } + + resources = append(resources, &StorageBucket{ + svc: l.svc, + Name: ptr.String(bucket.Name), + Project: opts.Project, + Labels: bucket.Labels, + Region: ptr.String(bucket.Location), + }) + } + + return resources, nil +} + +type StorageBucket struct { + svc *storage.Client + Project *string + Region *string + Name *string + Labels map[string]string +} + +func (r *StorageBucket) Remove(ctx context.Context) error { + return r.svc.Bucket(*r.Name).Delete(ctx) +} + +func (r *StorageBucket) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *StorageBucket) String() string { + return *r.Name +} diff --git a/resources/vpc-network.go b/resources/vpc-network.go new file mode 100644 index 0000000..9dde4a1 --- /dev/null +++ b/resources/vpc-network.go @@ -0,0 +1,128 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/sirupsen/logrus" + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const VPCNetworkResource = "VPCNetwork" + +func init() { + registry.Register(®istry.Registration{ + Name: VPCNetworkResource, + Scope: nuke.Project, + Lister: &VPCNetworkLister{}, + DependsOn: []string{ + VPCSubnetResource, + VPCRouteResource, + }, + }) +} + +type VPCNetworkLister struct { + svc *compute.NetworksClient +} + +func (l *VPCNetworkLister) Close() { + if l.svc != nil { + l.svc.Close() + } +} + +func (l *VPCNetworkLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + resources := make([]resource.Resource, 0) + + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + if l.svc == nil { + var err error + l.svc, err = compute.NewNetworksRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &computepb.ListNetworksRequest{ + Project: *opts.Project, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &VPCNetwork{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + }) + } + + return resources, nil +} + +type VPCNetwork struct { + svc *compute.NetworksClient + removeOp *compute.Operation + Project *string + Name *string +} + +func (r *VPCNetwork) Remove(ctx context.Context) error { + var err error + r.removeOp, err = r.svc.Delete(ctx, &computepb.DeleteNetworkRequest{ + Project: *r.Project, + Network: *r.Name, + }) + return err +} + +// HandleWait is a hook into the libnuke resource lifecycle to allow for waiting on a resource to be removed +// because certain GCP resources are async and require waiting for the operation to complete, this allows for +// polling of the operation until it is complete. Otherwise, remove is only called once and the resource is +// left in a permanent wait if the operation fails. +func (r *VPCNetwork) HandleWait(ctx context.Context) error { + if r.removeOp == nil { + return nil + } + + if err := r.removeOp.Poll(ctx); err != nil { + logrus.WithError(err).Trace("network remove op polling encountered error") + return err + } + + if r.removeOp.Done() { + if r.removeOp.Proto().GetError() != nil { + removeErr := fmt.Errorf("delete error on '%s': %s", r.removeOp.Proto().GetTargetLink(), r.removeOp.Proto().GetHttpErrorMessage()) + logrus.WithError(removeErr).WithField("status_code", r.removeOp.Proto().GetError()).Error("unable to delete network") + return removeErr + } + } + + return nil +} + +func (r *VPCNetwork) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} diff --git a/resources/vpc-route.go b/resources/vpc-route.go new file mode 100644 index 0000000..6d00295 --- /dev/null +++ b/resources/vpc-route.go @@ -0,0 +1,112 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "github.com/gotidy/ptr" + "github.com/sirupsen/logrus" + "strings" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const VPCRouteResource = "VPCRoute" + +func init() { + registry.Register(®istry.Registration{ + Name: VPCRouteResource, + Scope: nuke.Project, + Lister: &VPCRouteLister{}, + }) +} + +type VPCRouteLister struct { + svc *compute.RoutesClient +} + +func (l *VPCRouteLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region != "global" { + return nil, liberror.ErrSkipRequest("resource is global") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewRoutesRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &computepb.ListRoutesRequest{ + Project: *opts.Project, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &VPCRoute{ + svc: l.svc, + Project: opts.Project, + Network: ptr.String(strings.Split(*resp.Network, "/")[len(strings.Split(*resp.Network, "/"))-1]), + Name: resp.Name, + RouteType: resp.RouteType, + Description: resp.Description, + }) + } + + return resources, nil +} + +type VPCRoute struct { + svc *compute.RoutesClient + Project *string + Region *string + Name *string + Network *string + RouteType *string + Description *string `property:"-"` +} + +func (r *VPCRoute) Filter() error { + if strings.HasPrefix(*r.Description, "Default local route to the subnetwork") { + return fmt.Errorf("unable to remove default local route") + } + return nil +} + +func (r *VPCRoute) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteRouteRequest{ + Project: *r.Project, + Route: *r.Name, + }) + return err +} + +func (r *VPCRoute) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *VPCRoute) String() string { + return *r.Name +} diff --git a/resources/vpc-router.go b/resources/vpc-router.go new file mode 100644 index 0000000..fd25417 --- /dev/null +++ b/resources/vpc-router.go @@ -0,0 +1,99 @@ +package resources + +import ( + "context" + "errors" + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const VPCRouterResource = "VPCRouter" + +func init() { + registry.Register(®istry.Registration{ + Name: VPCRouterResource, + Scope: nuke.Project, + Lister: &VPCRouterLister{}, + }) +} + +type VPCRouterLister struct { + svc *compute.RoutersClient +} + +func (l *VPCRouterLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = compute.NewRoutersRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &computepb.ListRoutersRequest{ + Project: *opts.Project, + Region: *opts.Region, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &VPCRouter{ + svc: l.svc, + Project: opts.Project, + Region: opts.Region, + Name: resp.Name, + }) + } + + return resources, nil +} + +type VPCRouter struct { + svc *compute.RoutersClient + Project *string + Region *string + Name *string +} + +func (r *VPCRouter) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteRouterRequest{ + Project: *r.Project, + Region: *r.Region, + Router: *r.Name, + }) + return err +} + +func (r *VPCRouter) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *VPCRouter) String() string { + return *r.Name +} diff --git a/resources/vpc-subnet.go b/resources/vpc-subnet.go new file mode 100644 index 0000000..5468928 --- /dev/null +++ b/resources/vpc-subnet.go @@ -0,0 +1,103 @@ +package resources + +import ( + "context" + "errors" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const VPCSubnetResource = "VPCSubnet" + +func init() { + registry.Register(®istry.Registration{ + Name: VPCSubnetResource, + Scope: nuke.Project, + Lister: &VPCSubnetLister{}, + }) +} + +type VPCSubnetLister struct { + svc *compute.SubnetworksClient +} + +func (l *VPCSubnetLister) Close() { + if l.svc != nil { + l.svc.Close() + } +} + +func (l *VPCSubnetLister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + resources := make([]resource.Resource, 0) + + if l.svc == nil { + var err error + l.svc, err = compute.NewSubnetworksRESTClient(ctx) + if err != nil { + return nil, err + } + } + + req := &computepb.ListSubnetworksRequest{ + Project: *opts.Project, + Region: *opts.Region, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &VPCSubnet{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + Region: opts.Region, + }) + } + + return resources, nil +} + +type VPCSubnet struct { + svc *compute.SubnetworksClient + Project *string + Name *string + Region *string +} + +func (r *VPCSubnet) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &computepb.DeleteSubnetworkRequest{ + Project: *r.Project, + Region: *r.Region, + Subnetwork: *r.Name, + }) + return err +} + +func (r *VPCSubnet) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} diff --git a/tools/create-resource/main.go b/tools/create-resource/main.go new file mode 100644 index 0000000..c12f849 --- /dev/null +++ b/tools/create-resource/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/gertd/go-pluralize" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "os" + "strings" + "text/template" +) + +const resourceTemplate = `package resources + +import ( + "context" + "errors" + + "github.com/sirupsen/logrus" + + "google.golang.org/api/iterator" + + {{.Service}} "cloud.google.com/go/{{.Service}}/apiv1" + "cloud.google.com/go/{{.Service}}/apiv1/{{.Service}}pb" + + liberror "github.com/ekristen/libnuke/pkg/errors" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/gcp-nuke/pkg/nuke" +) + +const {{.Combined}}Resource = "{{.Combined}}" + +func init() { + registry.Register(®istry.Registration{ + Name: {{.Combined}}Resource, + Scope: nuke.Project, + Lister: &{{.Combined}}Lister{}, + }) +} + +type {{.Combined}}Lister struct{ + svc *{{.Service}}.{{.ResourceTypeTitlePlural}}Client +} + +func (l *{{.Combined}}Lister) List(ctx context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + if *opts.Region == "global" { + return nil, liberror.ErrSkipRequest("resource is regional") + } + + var resources []resource.Resource + + if l.svc == nil { + var err error + l.svc, err = {{.Service}}.New{{.ResourceTypeTitlePlural}}RESTClient(ctx) + if err != nil { + return nil, err + } + } + + // NOTE: you might have to modify the code below to actually work, this currently does not + // inspect the google go sdk instead is a jumping off point + req := &{{.Service}}pb.List{{.ResourceTypeTitlePlural}}Request{ + Project: *opts.Project, + } + it := l.svc.List(ctx, req) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + logrus.WithError(err).Error("unable to iterate networks") + break + } + + resources = append(resources, &{{.Combined}}{ + svc: l.svc, + Name: resp.Name, + Project: opts.Project, + }) + } + + return resources, nil +} + +type {{.Combined}} struct { + svc *{{.Service}}.{{.ResourceTypeTitlePlural}}Client + Project *string + Region *string + Name *string +} + +func (r *{{.Combined}}) Remove(ctx context.Context) error { + _, err := r.svc.Delete(ctx, &{{.Service}}pb.Delete{{.ResourceTypeTitle}}Request{ + Project: *r.Project, + Name: *r.Name, + }) + return err +} + +func (r *{{.Combined}}) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *{{.Combined}}) String() string { + return *r.Name +} +` + +func main() { + args := os.Args[1:] + + if len(args) != 2 { + fmt.Println("usage: create-resource ") + os.Exit(1) + } + + service := args[0] + resourceType := args[1] + + caser := cases.Title(language.English) + pluralize := pluralize.NewClient() + + data := struct { + Service string + ServiceTitle string + ResourceType string + ResourceTypeTitle string + ResourceTypeTitlePlural string + Combined string + }{ + Service: strings.ToLower(service), + ServiceTitle: caser.String(service), + ResourceType: resourceType, + ResourceTypeTitle: caser.String(resourceType), + ResourceTypeTitlePlural: caser.String(pluralize.Plural(resourceType)), + Combined: fmt.Sprintf("%s%s", caser.String(service), caser.String(resourceType)), + } + + tmpl, err := template.New("resource").Parse(resourceTemplate) + if err != nil { + panic(err) + } + + var tpl bytes.Buffer + if err := tmpl.Execute(&tpl, data); err != nil { + panic(err) + } + + fmt.Println(tpl.String()) +}