├── .github └── workflows │ ├── main.yaml │ ├── pr-build.yaml │ ├── pr-label.yaml │ ├── rebase.yaml │ ├── release.yaml │ └── scan.yaml ├── .gitignore ├── .goreleaser.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.release ├── Formula └── gitops-zombies.rb ├── LICENSE ├── Makefile ├── README.md ├── assets └── logo.png ├── cmd ├── completion.go └── main.go ├── go.mod ├── go.sum ├── hack ├── code-gen.Dockerfile └── code-gen.sh ├── pkg ├── apis │ └── gitopszombies │ │ └── v1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go ├── collector │ ├── resource.go │ └── resource_test.go └── detector │ ├── blacklist.go │ ├── clients.go │ ├── detector.go │ ├── gitops_resources.go │ └── resources.go └── renovate.json /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 14 | - name: Setup Go 15 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 16 | with: 17 | go-version: 1.19.x 18 | - name: Restore Go cache 19 | uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | - name: Tests 26 | run: make test 27 | - name: Send go coverage report 28 | uses: shogo82148/actions-goveralls@31ee804b8576ae49f6dc3caa22591bc5080e7920 #v1.6.0 29 | with: 30 | path-to-profile: coverage.out 31 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yaml: -------------------------------------------------------------------------------- 1 | name: pr-build 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 16 | - name: Setup Go 17 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 18 | with: 19 | go-version: 1.20.x 20 | - name: Restore Go cache 21 | uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 22 | with: 23 | path: ~/go/pkg/mod 24 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | ${{ runner.os }}-go- 27 | - name: fmt 28 | run: make fmt 29 | - name: vet 30 | run: make vet 31 | - name: Linting 32 | run: make lint 33 | - name: Tests 34 | run: make test 35 | - name: Send go coverage report 36 | uses: shogo82148/actions-goveralls@31ee804b8576ae49f6dc3caa22591bc5080e7920 #v1.6.0 37 | with: 38 | path-to-profile: coverage.out 39 | - name: Check if working tree is dirty 40 | run: | 41 | if [[ $(git diff --stat) != '' ]]; then 42 | git --no-pager diff 43 | echo 'run make test and commit changes' 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /.github/workflows/pr-label.yaml: -------------------------------------------------------------------------------- 1 | name: pr-label 2 | on: pull_request 3 | jobs: 4 | size-label: 5 | runs-on: ubuntu-latest 6 | if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} 7 | steps: 8 | - name: size-label 9 | uses: "pascalgn/size-label-action@a4655c448bb838e8d73b81e97fd0831bb4cbda1e" 10 | env: 11 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 12 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yaml: -------------------------------------------------------------------------------- 1 | name: rebase 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | rebase: 11 | if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the latest code 15 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 16 | with: 17 | fetch-depth: 0 18 | - name: Automatic Rebase 19 | uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 #1.8 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | permissions: 8 | contents: write # needed to write releases 9 | id-token: write # needed for keyless signing 10 | packages: write # needed for ghcr access 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 22 | with: 23 | go-version: '1.20' 24 | - name: Restore Go cache 25 | uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | - name: Docker Login 32 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2.1.0 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | - name: Setup Cosign 38 | uses: sigstore/cosign-installer@9e9de2292db7abb3f51b7f4808d98f0d347a8919 # v3.0.2 39 | - uses: anchore/sbom-action/download-syft@422cb34a0f8b599678c41b21163ea6088edb2624 # v0.14.1 40 | - name: Create release and SBOM 41 | if: startsWith(github.ref, 'refs/tags/v') 42 | uses: goreleaser/goreleaser-action@v4 43 | with: 44 | version: latest 45 | args: release --rm-dist --skip-validate 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | REPO_TOKEN: ${{ secrets.REPO_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/scan.yaml: -------------------------------------------------------------------------------- 1 | name: Scan 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '18 10 * * 3' 10 | 11 | permissions: 12 | contents: read # for actions/checkout to fetch code 13 | security-events: write # for codeQL to write security events 14 | 15 | jobs: 16 | fossa: 17 | name: FOSSA 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 21 | - name: Run FOSSA scan and upload build data 22 | uses: fossa-contrib/fossa-action@6728dc6fe9a068c648d080c33829ffbe56565023 #v2.0.0 23 | with: 24 | # FOSSA Push-Only API Token 25 | fossa-api-key: 956b9b92c5b16eeca1467cebe104f2c3 26 | github-token: ${{ github.token }} 27 | 28 | codeql: 29 | name: CodeQL 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@d944b3423d194ae3a11d1d7291ab2f38eb94207a #codeql-bundle-20221020 36 | with: 37 | languages: go 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@d944b3423d194ae3a11d1d7291ab2f38eb94207a #codeql-bundle-20221020 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@d944b3423d194ae3a11d1d7291ab2f38eb94207a #codeql-bundle-20221020 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | bin 3 | coverage.out 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: gitops-zombies 2 | 3 | builds: 4 | - id: cli 5 | main: ./cmd/ 6 | binary: gitops-zombies 7 | goos: 8 | - linux 9 | - darwin 10 | - windows 11 | env: 12 | - CGO_ENABLED=0 13 | 14 | archives: 15 | - id: cli 16 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 17 | builds: 18 | - cli 19 | 20 | checksum: 21 | name_template: 'checksums.txt' 22 | 23 | source: 24 | enabled: true 25 | name_template: "{{ .ProjectName }}_{{ .Version }}_source_code" 26 | 27 | changelog: 28 | use: github-native 29 | 30 | sboms: 31 | - id: source 32 | artifacts: source 33 | documents: 34 | - "{{ .ProjectName }}_{{ .Version }}_sbom.spdx.json" 35 | 36 | dockers: 37 | - image_templates: 38 | - ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-amd64 39 | dockerfile: Dockerfile.release 40 | use: buildx 41 | ids: 42 | - cli 43 | build_flag_templates: 44 | - --platform=linux/amd64 45 | - --label=org.opencontainers.image.title={{ .ProjectName }} 46 | - --label=org.opencontainers.image.description={{ .ProjectName }} 47 | - --label=org.opencontainers.image.url=https://github.com/raffis/{{ .ProjectName }} 48 | - --label=org.opencontainers.image.source=https://github.com/raffis/{{ .ProjectName }} 49 | - --label=org.opencontainers.image.version={{ .Version }} 50 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} 51 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 52 | - --label=org.opencontainers.image.licenses=Apache-2.0 53 | - image_templates: 54 | - "ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-arm64v8" 55 | goarch: arm64 56 | dockerfile: Dockerfile.release 57 | use: buildx 58 | ids: 59 | - cli 60 | build_flag_templates: 61 | - --platform=linux/arm64/v8 62 | - --label=org.opencontainers.image.title={{ .ProjectName }} 63 | - --label=org.opencontainers.image.description={{ .ProjectName }} 64 | - --label=org.opencontainers.image.url=https://github.com/raffis/{{ .ProjectName }} 65 | - --label=org.opencontainers.image.source=https://github.com/raffis/{{ .ProjectName }} 66 | - --label=org.opencontainers.image.version={{ .Version }} 67 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} 68 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 69 | - --label=org.opencontainers.image.licenses=Apache-2.0 70 | 71 | docker_manifests: 72 | - name_template: ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }} 73 | image_templates: 74 | - ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-amd64 75 | - ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-arm64v8 76 | - name_template: ghcr.io/raffis/{{ .ProjectName }}:latest 77 | image_templates: 78 | - ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-amd64 79 | - ghcr.io/raffis/{{ .ProjectName }}:v{{ .Version }}-arm64v8 80 | 81 | brews: 82 | - ids: 83 | - cli 84 | tap: 85 | owner: raffis 86 | name: gitops-zombies 87 | token: "{{ .Env.REPO_TOKEN }}" 88 | description: Identify kubernetes resources which are not managed by GitOps 89 | homepage: https://github.com/raffis/{{ .ProjectName }} 90 | folder: Formula 91 | test: | 92 | system "#{bin}/gitops-zombies -h" 93 | 94 | signs: 95 | - cmd: cosign 96 | certificate: "${artifact}.pem" 97 | env: 98 | - COSIGN_EXPERIMENTAL=1 99 | args: 100 | - sign-blob 101 | - "--output-certificate=${certificate}" 102 | - "--output-signature=${signature}" 103 | - "${artifact}" 104 | - --yes 105 | artifacts: checksum 106 | output: true 107 | 108 | docker_signs: 109 | - cmd: cosign 110 | env: 111 | - COSIGN_EXPERIMENTAL=1 112 | artifacts: images 113 | output: true 114 | args: 115 | - 'sign' 116 | - '${artifact}' 117 | - --yes 118 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @raffis 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Maintainer Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/). 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Release process 2 | 3 | ### Create release 4 | 1. Merge all pr's to master which need to be part of the new release 5 | 2. Create pr to master with kustomization bump (new deployment version) 6 | 3. Push a tag following semantic versioning prefixed by 'v'. Do not create a github release, this is done automatically. 7 | 4. Create a new pr and bump the helm chart version as well as the appVersion 8 | 9 | ### Helm chart change only 10 | Create a PR and bump the chart version alongside all other changes. 11 | If the chart version is not bumped the pr validation jobs will fail. 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 as builder 2 | 3 | WORKDIR /workspace 4 | # Copy the Go Modules manifests 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | # cache deps before building and copying source so that we don't need to re-download as much 8 | # and so that source changes don't invalidate our downloaded layer 9 | RUN go mod download 10 | 11 | # Copy the go source 12 | COPY cmd cmd 13 | 14 | # Build 15 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o gitops-zombies cmd/* 16 | 17 | FROM alpine:3.18 as gitops-zombies-cli 18 | WORKDIR / 19 | COPY --from=builder /workspace/gitops-zombies /usr/bin/ 20 | USER 65532:65532 21 | 22 | ENTRYPOINT ["/usr/bin/gitops-zombies"] 23 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18 2 | WORKDIR / 3 | COPY gitops-zombies /usr/bin/gitops-zombies 4 | USER 65532:65532 5 | 6 | ENTRYPOINT ["/usr/bin/gitops-zombies"] 7 | -------------------------------------------------------------------------------- /Formula/gitops-zombies.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class GitopsZombies < Formula 6 | desc "Identify kubernetes resources which are not managed by GitOps" 7 | homepage "https://github.com/raffis/gitops-zombies" 8 | version "0.0.9" 9 | 10 | on_macos do 11 | if Hardware::CPU.arm? 12 | url "https://github.com/raffis/gitops-zombies/releases/download/v0.0.9/gitops-zombies_0.0.9_darwin_arm64.tar.gz" 13 | sha256 "727660ec8c7f337b6e207b1dbceb10bdb70039bacf136b4c560b08a0abc0294b" 14 | 15 | def install 16 | bin.install "gitops-zombies" 17 | end 18 | end 19 | if Hardware::CPU.intel? 20 | url "https://github.com/raffis/gitops-zombies/releases/download/v0.0.9/gitops-zombies_0.0.9_darwin_amd64.tar.gz" 21 | sha256 "c32e2fce82f5aede464b04b5d23f5d2bf45d0fd3156a1430c5f0d7fe25ba475a" 22 | 23 | def install 24 | bin.install "gitops-zombies" 25 | end 26 | end 27 | end 28 | 29 | on_linux do 30 | if Hardware::CPU.arm? && Hardware::CPU.is_64_bit? 31 | url "https://github.com/raffis/gitops-zombies/releases/download/v0.0.9/gitops-zombies_0.0.9_linux_arm64.tar.gz" 32 | sha256 "802ea6c98e213bfed8f0dca1adfa4cf14710377a573375ce01f6b767eec3621b" 33 | 34 | def install 35 | bin.install "gitops-zombies" 36 | end 37 | end 38 | if Hardware::CPU.intel? 39 | url "https://github.com/raffis/gitops-zombies/releases/download/v0.0.9/gitops-zombies_0.0.9_linux_amd64.tar.gz" 40 | sha256 "eb79eb025955e188262ddfbf1ff9a971c452c823359e77e867717ee4bd9bc2ff" 41 | 42 | def install 43 | bin.install "gitops-zombies" 44 | end 45 | end 46 | end 47 | 48 | test do 49 | system "#{bin}/gitops-zombies -h" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION?=$(shell grep 'VERSION' cmd/main.go | awk '{ print $$4 }' | head -n 1 | tr -d '"') 2 | DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s) 3 | # Architecture to use envtest with 4 | ENVTEST_ARCH ?= amd64 5 | 6 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 7 | ifeq (,$(shell go env GOBIN)) 8 | GOBIN=$(shell go env GOPATH)/bin 9 | else 10 | GOBIN=$(shell go env GOBIN) 11 | endif 12 | 13 | rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2)) $(filter $(subst *,%,$(2)),$(d))) 14 | 15 | all: lint test build 16 | 17 | tidy: 18 | go mod tidy -compat=1.20 19 | 20 | fmt: 21 | go fmt ./... 22 | 23 | test: 24 | go test -coverprofile coverage.out -v ./... 25 | 26 | GOLANGCI_LINT = $(GOBIN)/golangci-lint 27 | golangci-lint: ## Download golint locally if necessary. 28 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2) 29 | 30 | lint: golangci-lint 31 | golangci-lint run 32 | 33 | vet: 34 | go vet ./... 35 | 36 | code-gen: 37 | ./hack/code-gen.sh 38 | 39 | build: 40 | CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/gitops-zombies ./cmd 41 | 42 | .PHONY: install 43 | install: 44 | CGO_ENABLED=0 go install ./cmd 45 | 46 | # go-install-tool will 'go install' any package $2 and install it to $1 47 | define go-install-tool 48 | @[ -f $(1) ] || { \ 49 | set -e ;\ 50 | TMP_DIR=$$(mktemp -d) ;\ 51 | cd $$TMP_DIR ;\ 52 | go mod init tmp ;\ 53 | echo "Downloading $(2)" ;\ 54 | env -i bash -c "GOBIN=$(GOBIN) PATH=$(PATH) GOPATH=$(shell go env GOPATH) GOCACHE=$(shell go env GOCACHE) go install $(2)" ;\ 55 | rm -rf $$TMP_DIR ;\ 56 | } 57 | endef 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitOps zombies 2 | 3 | ![Release](https://img.shields.io/github/v/release/raffis/gitops-zombies) 4 | [![release](https://github.com/raffis/gitops-zombies/actions/workflows/release.yaml/badge.svg)](https://github.com/raffis/gitops-zombies/actions/workflows/release.yaml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/raffis/gitops-zombies)](https://goreportcard.com/report/github.com/raffis/gitops-zombies) 6 | [![Coverage Status](https://coveralls.io/repos/github/raffis/gitops-zombies/badge.svg?branch=main)](https://coveralls.io/github/raffis/gitops-zombies?branch=main) 7 | 8 | This simple tool will help you find kubernetes resources which are not managed via GitOps (flux2). 9 | 10 |

logo

11 | 12 | ## How does it work? 13 | 14 | gitops-zombies discovers all apis installed on a cluster and identify resources which are not part of a Kustomization or a HelmRelease. 15 | It also acknowledges the following facts: 16 | 17 | * Ignores resources which are owned by a parent resource (For example pods which are created by a deployment) 18 | * Ignores resources which are considered dynamic (metrics, leases, events, endpoints, ...) 19 | * Filter out resources which are created by the apiserver itself (like default rbacs) 20 | * Filters secrets which are managed by other parties including helm or ServiceAccount tokens 21 | * Checks if the referenced HelmRelease or Kustomization exists 22 | * Checks if resources are still part of the kustomization inventory 23 | * Supports cross cluster kustomizations 24 | 25 | 26 | ## How do I install it? 27 | 28 | ``` 29 | brew tap raffis/gitops-zombies 30 | brew install gitops-zombies 31 | ``` 32 | 33 | ## How to use 34 | 35 | ``` 36 | gitops-zombies 37 | ``` 38 | 39 | A more advanced call might include a filter like the following to exclude certain resources which are considered dynamic (besides the builtin exclusions): 40 | ``` 41 | gitops-zombies --context staging -l app.kubernetes.io/managed-by!=kops,app.kubernetes.io/name!=velero,io.cilium.k8s.policy.cluster!=default 42 | ``` 43 | 44 | Also you might want to exclude some specific resources based on their names. It can be achieved through YAML configuration: 45 | ```yaml 46 | --- 47 | apiVersion: gitopszombies/v1 48 | kind: Config 49 | excludeResources: 50 | - name: default 51 | apiVersion: v1 52 | kind: ServiceAccount 53 | - name: velero-capi-backup-.* 54 | namespace: velero 55 | apiVersion: velero.io/v1 56 | kind: Backup 57 | cluster: management 58 | ``` 59 | 60 | ## CLI reference 61 | 62 | ``` 63 | Finds all kubernetes resources from all installed apis on a kubernetes cluste and evaluates whether they are managed by a flux kustomization or a helmrelease. 64 | 65 | Usage: 66 | gitops-zombies [flags] 67 | 68 | Flags: 69 | --add_dir_header If true, adds the file directory to the header of the log messages 70 | --alsologtostderr log to standard error as well as files (no effect when -logtostderr=true) 71 | --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. 72 | --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. 73 | --as-uid string UID to impersonate for the operation. 74 | --cache-dir string Default cache directory (default "~/.kube/cache") 75 | --certificate-authority string Path to a cert file for the certificate authority 76 | --client-certificate string Path to a client certificate file for TLS 77 | --client-key string Path to a client key file for TLS 78 | --cluster string The name of the kubeconfig cluster to use 79 | --config string Config file (default "~/.gitops-zombies.yaml") 80 | --context string The name of the kubeconfig context to use 81 | --disable-compression If true, opt-out of response compression for all requests to the server 82 | --exclude-cluster strings Exclude cluster from zombie detection (default none) 83 | --fail Exit with an exit code > 0 if zombies are detected 84 | -h, --help help for gitops-zombies 85 | -a, --include-all Includes resources which are considered dynamic resources 86 | --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure 87 | --kubeconfig string Path to the kubeconfig file to use for CLI requests. 88 | --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) 89 | --log_dir string If non-empty, write log files in this directory (no effect when -logtostderr=true) 90 | --log_file string If non-empty, use this log file (no effect when -logtostderr=true) 91 | --log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) 92 | --logtostderr log to standard error instead of files (default true) 93 | -n, --namespace string If present, the namespace scope for this CLI request 94 | --no-stream Display discovered resources at the end instead of live 95 | --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true) 96 | -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file, custom-columns, custom-columns-file, wide). See custom columns [https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [https://kubernetes.io/docs/reference/kubectl/jsonpath/]. 97 | --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") 98 | -l, --selector string Label selector (Is used for all apis) 99 | -s, --server string The address and port of the Kubernetes API server 100 | --skip_headers If true, avoid header prefixes in the log messages 101 | --skip_log_headers If true, avoid headers when opening log files (no effect when -logtostderr=true) 102 | --stderrthreshold severity logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false) (default 2) 103 | --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used 104 | --token string Bearer token for authentication to the API server 105 | --user string The name of the kubeconfig user to use 106 | -v, --v Level number for the log level verbosity 107 | --version Print version and exit 108 | --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging 109 | ``` 110 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raffis/gitops-zombies/19a416b0d63df6a4747954d33d846ceba435680c/assets/logo.png -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | ) 9 | 10 | func contextsCompletionFunc(kubeconfigArgs *genericclioptions.ConfigFlags, toComplete string) ([]string, cobra.ShellCompDirective) { 11 | rawConfig, err := kubeconfigArgs.ToRawKubeConfigLoader().RawConfig() 12 | if err != nil { 13 | return completionError(err) 14 | } 15 | 16 | var comps []string 17 | 18 | for name := range rawConfig.Contexts { 19 | if strings.HasPrefix(name, toComplete) { 20 | comps = append(comps, name) 21 | } 22 | } 23 | 24 | return comps, cobra.ShellCompDirectiveNoFileComp 25 | } 26 | 27 | func completionError(err error) ([]string, cobra.ShellCompDirective) { 28 | cobra.CompError(err.Error()) 29 | return nil, cobra.ShellCompDirectiveError 30 | } 31 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | 13 | gitopszombiesv1 "github.com/raffis/gitops-zombies/pkg/apis/gitopszombies/v1" 14 | "github.com/raffis/gitops-zombies/pkg/detector" 15 | "github.com/spf13/cobra" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/cli-runtime/pkg/genericclioptions" 19 | "k8s.io/client-go/kubernetes/scheme" 20 | "k8s.io/client-go/rest" 21 | "k8s.io/client-go/util/homedir" 22 | "k8s.io/klog/v2" 23 | k8sget "k8s.io/kubectl/pkg/cmd/get" 24 | ) 25 | 26 | const ( 27 | version = "0.0.0-dev" 28 | commit = "none" 29 | date = "unknown" 30 | ) 31 | 32 | type args struct { 33 | gitopszombiesv1.Config 34 | version bool 35 | } 36 | 37 | const ( 38 | statusOK = iota 39 | statusFail 40 | statusZombiesDetected 41 | 42 | statusAnnotation = "status" 43 | 44 | flagExcludeCluster = "exclude-cluster" 45 | flagFail = "fail" 46 | flagIncludeAll = "include-all" 47 | flagLabelSelector = "selector" 48 | flagNoStream = "no-stream" 49 | ) 50 | 51 | func main() { 52 | rootCmd, err := parseCliArgs() 53 | if err != nil { 54 | fmt.Printf("%v", err) 55 | } 56 | 57 | err = rootCmd.Execute() 58 | if err != nil { 59 | fmt.Printf("%v", err) 60 | } 61 | 62 | os.Exit(toExitCode(rootCmd.Annotations[statusAnnotation])) 63 | } 64 | 65 | func toExitCode(codeStr string) int { 66 | code, err := strconv.Atoi(codeStr) 67 | if err != nil { 68 | return statusFail 69 | } 70 | 71 | return code 72 | } 73 | 74 | func parseCliArgs() (*cobra.Command, error) { 75 | flags := args{Config: gitopszombiesv1.Config{ 76 | TypeMeta: metav1.TypeMeta{}, 77 | ExcludeClusters: nil, 78 | ExcludeResources: nil, 79 | Fail: false, 80 | IncludeAll: false, 81 | LabelSelector: "", 82 | NoStream: false, 83 | }} 84 | kubeconfigArgs := genericclioptions.NewConfigFlags(false) 85 | printFlags := k8sget.NewGetPrintFlags() 86 | cfgFile := path.Join(homedir.HomeDir(), ".gitops-zombies.yaml") 87 | 88 | rootCmd := &cobra.Command{ 89 | Use: "gitops-zombies", 90 | SilenceUsage: true, 91 | SilenceErrors: true, 92 | Short: "Find kubernetes resources which are not managed by GitOps", 93 | Long: `Finds all kubernetes resources from all installed apis on a kubernetes cluste and evaluates whether they are managed by a flux kustomization or a helmrelease.`, 94 | RunE: func(cmd *cobra.Command, args []string) error { 95 | cmd.Annotations = make(map[string]string) 96 | cmd.Annotations[statusAnnotation] = strconv.Itoa(statusFail) 97 | 98 | if flags.version { 99 | fmt.Printf(`{"version":"%s","sha":"%s","date":"%s"}`+"\n", version, commit, date) 100 | cmd.Annotations[statusAnnotation] = strconv.Itoa(statusOK) 101 | return nil 102 | } 103 | 104 | conf, err := loadConfig(cfgFile) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | mergeConfigAndFlags(conf, flags.Config, cmd) 110 | 111 | status, err := run(conf, kubeconfigArgs, printFlags) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | cmd.Annotations[statusAnnotation] = strconv.Itoa(status) 117 | return nil 118 | }, 119 | } 120 | 121 | apiServer := "" 122 | kubeconfigArgs.APIServer = &apiServer 123 | kubeconfigArgs.AddFlags(rootCmd.PersistentFlags()) 124 | 125 | rest.SetDefaultWarningHandler(rest.NewWarningWriter(io.Discard, rest.WarningWriterOptions{})) 126 | set := &flag.FlagSet{} 127 | klog.InitFlags(set) 128 | rootCmd.PersistentFlags().AddGoFlagSet(set) 129 | 130 | err := rootCmd.RegisterFlagCompletionFunc("context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 131 | return contextsCompletionFunc(kubeconfigArgs, toComplete) 132 | }) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | rootCmd.Flags().StringVarP(&cfgFile, "config", "", cfgFile, "Config file") 138 | rootCmd.Flags().StringVarP(printFlags.OutputFormat, "output", "o", *printFlags.OutputFormat, fmt.Sprintf(`Output format. One of: (%s). See custom columns [https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [https://kubernetes.io/docs/reference/kubectl/jsonpath/].`, strings.Join(printFlags.AllowedFormats(), ", "))) 139 | rootCmd.Flags().BoolVarP(&flags.version, "version", "", flags.version, "Print version and exit") 140 | rootCmd.Flags().BoolVarP(&flags.IncludeAll, flagIncludeAll, "a", false, "Includes resources which are considered dynamic resources") 141 | rootCmd.Flags().StringVarP(&flags.LabelSelector, flagLabelSelector, "l", "", "Label selector (Is used for all apis)") 142 | rootCmd.Flags().BoolVarP(&flags.NoStream, flagNoStream, "", false, "Display discovered resources at the end instead of live") 143 | rootCmd.Flags().BoolVarP(&flags.Fail, flagFail, "", false, "Exit with an exit code > 0 if zombies are detected") 144 | rootCmd.Flags().StringSliceVarP(&flags.ExcludeClusters, flagExcludeCluster, "", []string{}, "Exclude cluster from zombie detection (default none)") 145 | 146 | rootCmd.DisableAutoGenTag = true 147 | rootCmd.SetOut(os.Stdout) 148 | return rootCmd, nil 149 | } 150 | 151 | func loadConfig(configPath string) (*gitopszombiesv1.Config, error) { 152 | if _, err := os.Stat(configPath); err != nil { 153 | klog.V(1).Infof("Can't find config file at %s", configPath) 154 | return &gitopszombiesv1.Config{}, nil 155 | } 156 | 157 | json, err := os.ReadFile(configPath) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | err = gitopszombiesv1.AddToScheme(scheme.Scheme) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | obj, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), json) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | var cfg gitopszombiesv1.Config 173 | switch o := obj.(type) { 174 | case *gitopszombiesv1.Config: 175 | cfg = *o 176 | default: 177 | err = errors.New("unsupported config") 178 | return nil, err 179 | } 180 | 181 | return &cfg, nil 182 | } 183 | 184 | func mergeConfigAndFlags(conf *gitopszombiesv1.Config, flags gitopszombiesv1.Config, cmd *cobra.Command) { 185 | // cmd line overrides config 186 | if cmd.Flags().Changed(flagExcludeCluster) { 187 | conf.ExcludeClusters = flags.ExcludeClusters 188 | } 189 | 190 | if cmd.Flags().Changed(flagFail) { 191 | conf.Fail = flags.Fail 192 | } 193 | 194 | if cmd.Flags().Changed(flagIncludeAll) { 195 | conf.IncludeAll = flags.IncludeAll 196 | } 197 | 198 | if cmd.Flags().Changed(flagLabelSelector) { 199 | conf.LabelSelector = flags.LabelSelector 200 | } 201 | 202 | if cmd.Flags().Changed(flagNoStream) { 203 | conf.NoStream = flags.NoStream 204 | } 205 | } 206 | 207 | func run(conf *gitopszombiesv1.Config, kubeconfigArgs *genericclioptions.ConfigFlags, printFlags *k8sget.PrintFlags) (int, error) { 208 | // default processing 209 | detect, err := detector.New(conf, kubeconfigArgs, printFlags) 210 | if err != nil { 211 | return statusFail, err 212 | } 213 | resourceCount, allZombies, err := detect.DetectZombies() 214 | if err != nil { 215 | return statusFail, err 216 | } 217 | 218 | if conf.NoStream { 219 | err = detect.PrintZombies(allZombies) 220 | if err != nil { 221 | return statusFail, err 222 | } 223 | } 224 | 225 | var totalZombies int 226 | for _, zombies := range allZombies { 227 | totalZombies += len(zombies) 228 | } 229 | 230 | if conf.NoStream && printFlags.OutputFormat != nil && *printFlags.OutputFormat == "" { 231 | fmt.Printf("\nSummary: %d resources found, %d zombies detected\n", resourceCount, totalZombies) 232 | } 233 | 234 | if conf.Fail && totalZombies > 0 { 235 | return statusZombiesDetected, nil 236 | } 237 | 238 | return statusOK, nil 239 | } 240 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/raffis/gitops-zombies 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fluxcd/helm-controller/api v0.32.1 7 | github.com/fluxcd/kustomize-controller/api v0.35.1 8 | github.com/spf13/cobra v1.7.0 9 | github.com/stretchr/testify v1.8.2 10 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 11 | gotest.tools/v3 v3.4.0 12 | k8s.io/api v0.26.3 13 | k8s.io/apimachinery v0.26.3 14 | k8s.io/cli-runtime v0.26.3 15 | k8s.io/client-go v0.26.3 16 | k8s.io/klog/v2 v2.90.1 17 | k8s.io/kubectl v0.26.3 18 | sigs.k8s.io/cli-utils v0.34.0 19 | ) 20 | 21 | require ( 22 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 23 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 24 | github.com/chai2010/gettext-go v1.0.2 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 27 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 28 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 29 | github.com/fluxcd/pkg/apis/kustomize v1.0.0 // indirect 30 | github.com/fluxcd/pkg/apis/meta v1.0.0 // indirect 31 | github.com/fvbommel/sortorder v1.0.1 // indirect 32 | github.com/go-errors/errors v1.0.1 // indirect 33 | github.com/go-logr/logr v1.2.3 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 35 | github.com/go-openapi/jsonreference v0.20.0 // indirect 36 | github.com/go-openapi/swag v0.22.3 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/golang/protobuf v1.5.2 // indirect 39 | github.com/google/btree v1.0.1 // indirect 40 | github.com/google/gnostic v0.6.9 // indirect 41 | github.com/google/go-cmp v0.5.9 // indirect 42 | github.com/google/gofuzz v1.2.0 // indirect 43 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 44 | github.com/google/uuid v1.3.0 // indirect 45 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 46 | github.com/imdario/mergo v0.3.13 // indirect 47 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/kr/pretty v0.3.0 // indirect 51 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 52 | github.com/mailru/easyjson v0.7.7 // indirect 53 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 54 | github.com/moby/spdystream v0.2.0 // indirect 55 | github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.2 // indirect 58 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 59 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 60 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 61 | github.com/pkg/errors v0.9.1 // indirect 62 | github.com/pmezard/go-difflib v1.0.0 // indirect 63 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 64 | github.com/spf13/pflag v1.0.5 // indirect 65 | github.com/xlab/treeprint v1.1.0 // indirect 66 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 67 | golang.org/x/net v0.7.0 // indirect 68 | golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect 69 | golang.org/x/sys v0.5.0 // indirect 70 | golang.org/x/term v0.5.0 // indirect 71 | golang.org/x/text v0.7.0 // indirect 72 | golang.org/x/time v0.3.0 // indirect 73 | google.golang.org/appengine v1.6.7 // indirect 74 | google.golang.org/protobuf v1.28.1 // indirect 75 | gopkg.in/inf.v0 v0.9.1 // indirect 76 | gopkg.in/yaml.v2 v2.4.0 // indirect 77 | gopkg.in/yaml.v3 v3.0.1 // indirect 78 | k8s.io/apiextensions-apiserver v0.26.3 // indirect 79 | k8s.io/component-base v0.26.3 // indirect 80 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 81 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect 82 | sigs.k8s.io/controller-runtime v0.14.6 // indirect 83 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 84 | sigs.k8s.io/kustomize/api v0.12.1 // indirect 85 | sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect 86 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 87 | sigs.k8s.io/yaml v1.3.0 // indirect 88 | ) 89 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 7 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 8 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 9 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 12 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 13 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 14 | github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= 15 | github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 16 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 17 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 18 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 21 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 22 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 24 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 25 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 26 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 27 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 29 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 31 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= 32 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 33 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 34 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 35 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 36 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 37 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 38 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 39 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 40 | github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= 41 | github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 42 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= 43 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 44 | github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= 45 | github.com/fluxcd/helm-controller/api v0.32.1 h1:b2q0V+cXnqvW24Zy4zY+5Jfn1D3sqBIBTNhbqsD+r9Q= 46 | github.com/fluxcd/helm-controller/api v0.32.1/go.mod h1:xzQgNoaPOg77zFUqvnaX0Fn3lPA3iGDLoz8q4wiEyLA= 47 | github.com/fluxcd/kustomize-controller/api v0.35.1 h1:l7AndDJXVLZcCHmEIRXU9ksWInlP6SjFtHQH1SC7++c= 48 | github.com/fluxcd/kustomize-controller/api v0.35.1/go.mod h1:hrxVOUss0om4mg+ykMYtH4CgLuM2RReSPf0hG9e0b18= 49 | github.com/fluxcd/pkg/apis/kustomize v1.0.0 h1:5T2b/mRZiGWtP7fvSU8gZOApIc06H6SdLX3MlsE6LRo= 50 | github.com/fluxcd/pkg/apis/kustomize v1.0.0/go.mod h1:XaDYlKxrf9D2zZWcZ0BnSIqGtcm8mdNtJGzZWYjCnQo= 51 | github.com/fluxcd/pkg/apis/meta v1.0.0 h1:i9IGHd/VNEZELX7mepkiYFbJxs2J5znaB4cN9z2nPm8= 52 | github.com/fluxcd/pkg/apis/meta v1.0.0/go.mod h1:04ZdpZYm1x+aL93K4daNHW1UX6E8K7Gyf5za9OhrE+U= 53 | github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= 54 | github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 55 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 56 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 57 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 58 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 59 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 60 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 61 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 62 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 63 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 64 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 65 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 66 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 67 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 68 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 69 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 70 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 71 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 72 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 73 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 74 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 76 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 77 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 78 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 79 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 80 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 81 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 82 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 83 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 84 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 85 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 86 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 87 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 88 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 89 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 90 | github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= 91 | github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= 92 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 93 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 95 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 99 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 100 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 101 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 102 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 103 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 104 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 105 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 106 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 107 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 108 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 109 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= 110 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 111 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 112 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 113 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 114 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 115 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 116 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 117 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 118 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 119 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 120 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 121 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 122 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 123 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 124 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 125 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 126 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 127 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 128 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 129 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 130 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 131 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 132 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 133 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 134 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 135 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 136 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 137 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 138 | github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= 139 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 140 | github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= 141 | github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= 142 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 143 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 144 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 145 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 146 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 147 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 148 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 149 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 150 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 151 | github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= 152 | github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= 153 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 154 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 155 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 156 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 157 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 158 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 159 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 160 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 161 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 162 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 163 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 164 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 165 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 166 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 167 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 168 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 169 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 170 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 171 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 172 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 173 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 174 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 175 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 176 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 177 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 178 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 179 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 180 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 181 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 182 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 183 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 184 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 185 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 186 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 187 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 188 | github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= 189 | github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 190 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 191 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 192 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 193 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= 194 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= 195 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 196 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 197 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 198 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 199 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 200 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 201 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 202 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 203 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 204 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 205 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 206 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 207 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 208 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 209 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 211 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 213 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 214 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 216 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 217 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 218 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 219 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 220 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 221 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 222 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 223 | golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= 224 | golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= 225 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 228 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 229 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 233 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 244 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 246 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 247 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 248 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 249 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 250 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 251 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 252 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 253 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 254 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 255 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 256 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 257 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 258 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 259 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 260 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 261 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 262 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 263 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 264 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 265 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 266 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 267 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 268 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 269 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 270 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 271 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 272 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 273 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 274 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 275 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 276 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 277 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 278 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 279 | google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 280 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 281 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 282 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 283 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 284 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 285 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 286 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 287 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 288 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 289 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 290 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 291 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 292 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 293 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 294 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 295 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 296 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 297 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 298 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 299 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 300 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 301 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 302 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 303 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 304 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 305 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 306 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 307 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 308 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 309 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 310 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 311 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 312 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 313 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 314 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 315 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 316 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 317 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 318 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 319 | gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 320 | gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= 321 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 322 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 323 | k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= 324 | k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= 325 | k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE= 326 | k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ= 327 | k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= 328 | k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= 329 | k8s.io/cli-runtime v0.26.3 h1:3ULe0oI28xmgeLMVXIstB+ZL5CTGvWSMVMLeHxitIuc= 330 | k8s.io/cli-runtime v0.26.3/go.mod h1:5YEhXLV4kLt/OSy9yQwtSSNZU2Z7aTEYta1A+Jg4VC4= 331 | k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s= 332 | k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ= 333 | k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g= 334 | k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E= 335 | k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= 336 | k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 337 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= 338 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= 339 | k8s.io/kubectl v0.26.3 h1:bZ5SgFyeEXw6XTc1Qji0iNdtqAC76lmeIIQULg2wNXM= 340 | k8s.io/kubectl v0.26.3/go.mod h1:02+gv7Qn4dupzN3fi/9OvqqdW+uG/4Zi56vc4Zmsp1g= 341 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= 342 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 343 | sigs.k8s.io/cli-utils v0.34.0 h1:zCUitt54f0/MYj/ajVFnG6XSXMhpZ72O/3RewIchW8w= 344 | sigs.k8s.io/cli-utils v0.34.0/go.mod h1:EXyMwPMu9OL+LRnj0JEMsGG/fRvbgFadcVlSnE8RhFs= 345 | sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= 346 | sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= 347 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 348 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 349 | sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= 350 | sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= 351 | sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= 352 | sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= 353 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 354 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 355 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 356 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 357 | -------------------------------------------------------------------------------- /hack/code-gen.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-buster 2 | 3 | ARG USER=$USER 4 | ARG UID=1000 5 | ARG GID=1000 6 | RUN useradd -m ${USER} --uid=${UID} && echo "${USER}:" chpasswd 7 | USER ${UID}:${GID} 8 | 9 | ARG KUBE_VERSION 10 | 11 | RUN go install k8s.io/code-generator@$KUBE_VERSION; exit 0 12 | RUN go install k8s.io/apimachinery@$KUBE_VERSION; exit 0 13 | RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.11.1; exit 0 14 | 15 | RUN mkdir -p $GOPATH/src/k8s.io/code-generator,apimachinery} 16 | RUN cp -R $GOPATH/pkg/mod/k8s.io/code-generator@$KUBE_VERSION $GOPATH/src/k8s.io/code-generator 17 | RUN cp -R $GOPATH/pkg/mod/k8s.io/apimachinery@$KUBE_VERSION $GOPATH/src/k8s.io/apimachinery 18 | RUN chmod +x $GOPATH/src/k8s.io/code-generator/generate-groups.sh 19 | 20 | WORKDIR $GOPATH/src/k8s.io/code-generator 21 | -------------------------------------------------------------------------------- /hack/code-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | set -e -o pipefail 4 | 5 | PROJECT_MODULE="github.com/raffis/gitops-zombies" 6 | IMAGE_NAME="kubernetes-codegen:latest" 7 | 8 | echo "Building codegen Docker image..." 9 | docker build --build-arg KUBE_VERSION=v0.26.1 --build-arg USER=$USER -f "./hack/code-gen.Dockerfile" \ 10 | -t "${IMAGE_NAME}" \ 11 | "." 12 | 13 | cmd="/go/src/k8s.io/code-generator/generate-groups.sh deepcopy $PROJECT_MODULE/pkg/client $PROJECT_MODULE/pkg/apis gitopszombies:v1 --go-header-file /go/src/k8s.io/code-generator/hack/boilerplate.go.txt" 14 | echo "Generating clientSet code ..." 15 | echo $(pwd) 16 | docker run --rm \ 17 | -v "$(pwd):/go/src/${PROJECT_MODULE}" \ 18 | -w "/go/src/${PROJECT_MODULE}" \ 19 | "${IMAGE_NAME}" $cmd 20 | -------------------------------------------------------------------------------- /pkg/apis/gitopszombies/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | // +groupName=gitopszombies 4 | 5 | package v1 6 | -------------------------------------------------------------------------------- /pkg/apis/gitopszombies/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | // SchemeGroupVersion is group version used to register these objects. 10 | var SchemeGroupVersion = schema.GroupVersion{ 11 | Group: "gitopszombies", 12 | Version: "v1", 13 | } 14 | 15 | var ( 16 | schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 17 | // AddToScheme applies the SchemeBuilder functions to a specified scheme. 18 | AddToScheme = schemeBuilder.AddToScheme 19 | ) 20 | 21 | // Resource takes an unqualified resource and returns a Group qualified GroupResource. 22 | func Resource(resource string) schema.GroupResource { 23 | sch := schema.GroupVersion{ 24 | Group: "gitopszombies", 25 | Version: "v1", 26 | } 27 | return sch.WithResource(resource).GroupResource() 28 | } 29 | 30 | // Adds the list of known types to the given scheme. 31 | func addKnownTypes(scheme *runtime.Scheme) error { 32 | scheme.AddKnownTypes( 33 | SchemeGroupVersion, 34 | &Config{}, 35 | ) 36 | 37 | metav1.AddToGroupVersion( 38 | scheme, 39 | SchemeGroupVersion, 40 | ) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/apis/gitopszombies/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:nonNamespaced 9 | 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | 12 | // Config defines the config for gitops-zombies. 13 | type Config struct { 14 | metav1.TypeMeta `json:",inline"` 15 | 16 | ExcludeClusters []string `json:"excludeClusters,omitempty"` 17 | ExcludeResources []ExcludeResources `json:"excludeResources,omitempty"` 18 | Fail bool `json:"fail,omitempty"` 19 | IncludeAll bool `json:"includeAll,omitempty"` 20 | LabelSelector string `json:"selector,omitempty"` 21 | NoStream bool `json:"noStream,omitempty"` 22 | } 23 | 24 | // ExcludeResources configures filters to exclude resources from zombies list. 25 | type ExcludeResources struct { 26 | Cluster string `json:"cluster,omitempty"` 27 | Annotations map[string]string `json:"annotations,omitempty"` 28 | Labels map[string]string `json:"labels,omitempty"` 29 | Name string `json:"name,omitempty"` 30 | Namespace string `json:"namespace,omitempty"` 31 | metav1.TypeMeta `json:",inline"` 32 | } 33 | -------------------------------------------------------------------------------- /pkg/apis/gitopszombies/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by deepcopy-gen. DO NOT EDIT. 21 | 22 | package v1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *Config) DeepCopyInto(out *Config) { 30 | *out = *in 31 | out.TypeMeta = in.TypeMeta 32 | if in.ExcludeClusters != nil { 33 | in, out := &in.ExcludeClusters, &out.ExcludeClusters 34 | *out = make([]string, len(*in)) 35 | copy(*out, *in) 36 | } 37 | if in.ExcludeResources != nil { 38 | in, out := &in.ExcludeResources, &out.ExcludeResources 39 | *out = make([]ExcludeResources, len(*in)) 40 | for i := range *in { 41 | (*in)[i].DeepCopyInto(&(*out)[i]) 42 | } 43 | } 44 | return 45 | } 46 | 47 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. 48 | func (in *Config) DeepCopy() *Config { 49 | if in == nil { 50 | return nil 51 | } 52 | out := new(Config) 53 | in.DeepCopyInto(out) 54 | return out 55 | } 56 | 57 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 58 | func (in *Config) DeepCopyObject() runtime.Object { 59 | if c := in.DeepCopy(); c != nil { 60 | return c 61 | } 62 | return nil 63 | } 64 | 65 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 66 | func (in *ExcludeResources) DeepCopyInto(out *ExcludeResources) { 67 | *out = *in 68 | if in.Annotations != nil { 69 | in, out := &in.Annotations, &out.Annotations 70 | *out = make(map[string]string, len(*in)) 71 | for key, val := range *in { 72 | (*out)[key] = val 73 | } 74 | } 75 | if in.Labels != nil { 76 | in, out := &in.Labels, &out.Labels 77 | *out = make(map[string]string, len(*in)) 78 | for key, val := range *in { 79 | (*out)[key] = val 80 | } 81 | } 82 | out.TypeMeta = in.TypeMeta 83 | return 84 | } 85 | 86 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExcludeResources. 87 | func (in *ExcludeResources) DeepCopy() *ExcludeResources { 88 | if in == nil { 89 | return nil 90 | } 91 | out := new(ExcludeResources) 92 | in.DeepCopyInto(out) 93 | return out 94 | } 95 | -------------------------------------------------------------------------------- /pkg/collector/resource.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "regexp" 6 | 7 | helmapi "github.com/fluxcd/helm-controller/api/v2beta1" 8 | ksapi "github.com/fluxcd/kustomize-controller/api/v1beta2" 9 | v1 "github.com/raffis/gitops-zombies/pkg/apis/gitopszombies/v1" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/klog/v2" 12 | "sigs.k8s.io/cli-utils/pkg/object" 13 | ) 14 | 15 | const ( 16 | fluxHelmNameLabel = "helm.toolkit.fluxcd.io/name" 17 | fluxHelmNamespaceLabel = "helm.toolkit.fluxcd.io/namespace" 18 | fluxKustomizeNameLabel = "kustomize.toolkit.fluxcd.io/name" 19 | fluxKustomizeNamespaceLabel = "kustomize.toolkit.fluxcd.io/namespace" 20 | ) 21 | 22 | // FilterFunc is a function that filters resources. 23 | type FilterFunc func(res unstructured.Unstructured, logger klog.Logger) bool 24 | 25 | // Interface represents collector interface. 26 | type Interface interface { 27 | Discover(ctx context.Context, list *unstructured.UnstructuredList, ch chan unstructured.Unstructured) error 28 | } 29 | 30 | type discovery struct { 31 | filters []FilterFunc 32 | logger klog.Logger 33 | } 34 | 35 | // NewDiscovery returns a new discovery instance. 36 | func NewDiscovery(logger klog.Logger, filters ...FilterFunc) Interface { 37 | return &discovery{ 38 | logger: logger, 39 | filters: filters, 40 | } 41 | } 42 | 43 | // Discover validates discovered resources against all filters and adds it to consumer channel. 44 | func (d *discovery) Discover(_ context.Context, list *unstructured.UnstructuredList, ch chan unstructured.Unstructured) error { 45 | RESOURCES: 46 | for _, res := range list.Items { 47 | d.logger.V(1).Info("validate resource", "name", res.GetName(), "namespace", res.GetNamespace(), "apiVersion", res.GetAPIVersion()) 48 | 49 | for _, filter := range d.filters { 50 | if filter(res, d.logger) { 51 | continue RESOURCES 52 | } 53 | } 54 | 55 | ch <- res 56 | } 57 | 58 | return nil 59 | } 60 | 61 | // IgnoreOwnedResource returns a FilterFunc which filters resources owner by parents ones. 62 | func IgnoreOwnedResource() FilterFunc { 63 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 64 | if refs := res.GetOwnerReferences(); len(refs) > 0 { 65 | logger.V(1).Info("ignore resource owned by parent", "name", res.GetName(), "namespace", res.GetNamespace(), "apiVersion", res.GetAPIVersion()) 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | } 72 | 73 | // IgnoreServiceAccountSecret returns a FilterFunc which filters secrets linked to a service account. 74 | func IgnoreServiceAccountSecret() FilterFunc { 75 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 76 | if res.GetKind() == "Secret" && res.GetAPIVersion() == "v1" { 77 | if _, ok := res.GetAnnotations()["kubernetes.io/service-account.name"]; ok { 78 | return true 79 | } 80 | } 81 | 82 | return false 83 | } 84 | } 85 | 86 | // IgnoreHelmSecret returns a FilterFunc which filters secrets owned by helm. 87 | func IgnoreHelmSecret() FilterFunc { 88 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 89 | if res.GetKind() == "Secret" && res.GetAPIVersion() == "v1" { 90 | if v, ok := res.GetLabels()["owner"]; ok && v == "helm" { 91 | return true 92 | } 93 | } 94 | 95 | return false 96 | } 97 | } 98 | 99 | // IgnoreIfHelmReleaseFound returns a FilterFunc which filters resources part of an helm release. 100 | func IgnoreIfHelmReleaseFound(helmReleases []helmapi.HelmRelease) FilterFunc { 101 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 102 | labels := res.GetLabels() 103 | if helmName, ok := labels[fluxHelmNameLabel]; ok { 104 | if helmNamespace, ok := labels[fluxHelmNamespaceLabel]; ok { 105 | if hasResource(helmReleases, helmName, helmNamespace) { 106 | return true 107 | } 108 | 109 | logger.V(1).Info("helmrelease not found from resource", "helmReleaseName", helmName, "helmReleaseNamespace", helmNamespace, "name", res.GetName(), "namespace", res.GetNamespace(), "apiVersion", res.GetAPIVersion()) 110 | } 111 | } 112 | 113 | return false 114 | } 115 | } 116 | 117 | // IgnoreIfKustomizationFound returns a FilterFunc which filters resources part of a flux kustomization. 118 | func IgnoreIfKustomizationFound(kustomizations []ksapi.Kustomization) FilterFunc { 119 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 120 | labels := res.GetLabels() 121 | ksName, okKsName := labels[fluxKustomizeNameLabel] 122 | ksNamespace, okKsNamespace := labels[fluxKustomizeNamespaceLabel] 123 | if !okKsName || !okKsNamespace { 124 | return false 125 | } 126 | 127 | if ks := findKustomization(kustomizations, ksName, ksNamespace); ks != nil { 128 | obj := object.ObjMetadata{ 129 | Namespace: res.GetNamespace(), 130 | Name: res.GetName(), 131 | GroupKind: res.GroupVersionKind().GroupKind(), 132 | } 133 | id := obj.String() 134 | 135 | logger.V(1).Info("lookup kustomization inventory", "kustomizationName", ksName, "kustomizationNamespace", ksNamespace, "resourceId", id) 136 | 137 | if ks.Status.Inventory != nil { 138 | for _, entry := range ks.Status.Inventory.Entries { 139 | if entry.ID == id { 140 | return true 141 | } 142 | } 143 | } 144 | 145 | logger.V(1).Info("resource is not part of the kustomization inventory", "name", res.GetName(), "namespace", res.GetNamespace(), "apiVersion", res.GetAPIVersion(), "kustomizationName", ksName, "kustomizationNamespace", ksNamespace) 146 | return false 147 | } 148 | logger.V(1).Info("kustomization not found from resource", "resource", res.GetName(), "namespace", res.GetNamespace(), "apiVersion", res.GetAPIVersion(), "kustomizationName", ksName, "kustomizationNamespace", ksNamespace) 149 | return false 150 | } 151 | } 152 | 153 | // IgnoreRuleExclusions returns a FilterFunc which excludes resources part of configuration exclusions. 154 | func IgnoreRuleExclusions(cluster string, exclusions []v1.ExcludeResources) FilterFunc { 155 | return func(res unstructured.Unstructured, logger klog.Logger) bool { 156 | for _, exclusion := range exclusions { 157 | if !matchesCluster(cluster, exclusion.Cluster) { 158 | continue 159 | } 160 | 161 | if !resourceMatchesGetAPIVersionAndKind(res, exclusion.APIVersion, exclusion.Kind) { 162 | continue 163 | } 164 | 165 | if !resourceMatchesNamespace(res, exclusion.Namespace) { 166 | continue 167 | } 168 | 169 | if !resourceMatchesMetadata(res.GetAnnotations(), exclusion.Annotations) { 170 | continue 171 | } 172 | 173 | if !resourceMatchesMetadata(res.GetLabels(), exclusion.Labels) { 174 | continue 175 | } 176 | 177 | if resourceMatchesName(res, exclusion.Name) { 178 | return true 179 | } 180 | } 181 | return false 182 | } 183 | } 184 | 185 | func matchesCluster(cluster, clusterExclude string) bool { 186 | if clusterExclude != "" { 187 | match, err := regexp.MatchString(`^`+clusterExclude+`$`, cluster) 188 | if err != nil { 189 | klog.Error(err) 190 | } 191 | 192 | return match 193 | } 194 | 195 | return true 196 | } 197 | 198 | func resourceMatchesGetAPIVersionAndKind(res unstructured.Unstructured, apiVersion, kind string) bool { 199 | // match all api versions 200 | resVer := res.GetAPIVersion() 201 | if apiVersion != "" && resVer != apiVersion { 202 | return false 203 | } 204 | 205 | if kind != "" && res.GetKind() != kind { 206 | return false 207 | } 208 | 209 | return true 210 | } 211 | 212 | func resourceMatchesNamespace(res unstructured.Unstructured, namespace string) bool { 213 | if namespace != "" { 214 | match, err := regexp.MatchString(`^`+namespace+`$`, res.GetNamespace()) 215 | if err != nil { 216 | klog.Error(err) 217 | } 218 | if !match { 219 | return false 220 | } 221 | } 222 | 223 | return true 224 | } 225 | 226 | func resourceMatchesMetadata(resMetadata, metadata map[string]string) bool { 227 | for key, val := range metadata { 228 | v, ok := resMetadata[key] 229 | if !ok { 230 | return false 231 | } 232 | 233 | match, err := regexp.MatchString(`^`+val+`$`, v) 234 | if err != nil { 235 | klog.Error(err) 236 | } 237 | if !match { 238 | return false 239 | } 240 | } 241 | 242 | return true 243 | } 244 | 245 | func resourceMatchesName(res unstructured.Unstructured, name string) bool { 246 | if name != "" { 247 | match, err := regexp.MatchString(`^`+name+`$`, res.GetName()) 248 | if err != nil { 249 | klog.Error(err) 250 | } 251 | 252 | return match 253 | } 254 | 255 | return true 256 | } 257 | 258 | func hasResource(pool []helmapi.HelmRelease, name, namespace string) bool { 259 | for _, res := range pool { 260 | if res.GetName() == name && res.GetNamespace() == namespace { 261 | return true 262 | } 263 | } 264 | 265 | return false 266 | } 267 | 268 | func findKustomization(pool []ksapi.Kustomization, name, namespace string) *ksapi.Kustomization { 269 | for _, res := range pool { 270 | if res.GetName() == name && res.GetNamespace() == namespace { 271 | return &res 272 | } 273 | } 274 | 275 | return nil 276 | } 277 | -------------------------------------------------------------------------------- /pkg/collector/resource_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | helmapi "github.com/fluxcd/helm-controller/api/v2beta1" 8 | ksapi "github.com/fluxcd/kustomize-controller/api/v1beta2" 9 | gitopszombiesv1 "github.com/raffis/gitops-zombies/pkg/apis/gitopszombies/v1" 10 | "github.com/stretchr/testify/require" 11 | "gotest.tools/v3/assert" 12 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "k8s.io/klog/v2" 16 | ) 17 | 18 | type NullLogger struct{} 19 | 20 | func (l NullLogger) Debugf(_ string, _ ...interface{}) { 21 | } 22 | 23 | func (l NullLogger) Failuref(_ string, _ ...interface{}) { 24 | } 25 | 26 | type test struct { 27 | name string 28 | filters func() []FilterFunc 29 | list func() *unstructured.UnstructuredList 30 | expectedPass int 31 | } 32 | 33 | func getExclusionListResourceSet() *unstructured.UnstructuredList { 34 | list := &unstructured.UnstructuredList{} 35 | 36 | res1 := unstructured.Unstructured{} 37 | res1.SetName("velero-capi-backup-1") 38 | res1.SetNamespace("velero") 39 | res1.SetGroupVersionKind(schema.GroupVersionKind{ 40 | Group: "velero.io", 41 | Version: "v1", 42 | Kind: "Backup", 43 | }) 44 | res1.SetAnnotations(map[string]string{"test-annotation": "velero-capi-backup-1"}) 45 | 46 | res2 := unstructured.Unstructured{} 47 | res2.SetName("velero-capi-backup-2") 48 | res2.SetNamespace("velero") 49 | res2.SetGroupVersionKind(schema.GroupVersionKind{ 50 | Group: "velero.io", 51 | Version: "v2", 52 | Kind: "Backup", 53 | }) 54 | res2.SetLabels(map[string]string{"test-label": "velero-capi-backup-2"}) 55 | 56 | res3 := unstructured.Unstructured{} 57 | res3.SetName("velero-capi-backup-3") 58 | res3.SetNamespace("velero2") 59 | res3.SetGroupVersionKind(schema.GroupVersionKind{ 60 | Group: "velero.io", 61 | Version: "v1", 62 | Kind: "Backuped", 63 | }) 64 | 65 | list.Items = append(list.Items, res1, res2, res3) 66 | 67 | return list 68 | } 69 | 70 | func TestDiscovery(t *testing.T) { 71 | tests := []test{ 72 | { 73 | name: "A resource which has owner references is skipped", 74 | filters: func() []FilterFunc { 75 | return []FilterFunc{IgnoreOwnedResource()} 76 | }, 77 | list: func() *unstructured.UnstructuredList { 78 | list := &unstructured.UnstructuredList{} 79 | expected := unstructured.Unstructured{} 80 | expected.SetName("resource-without-owner") 81 | 82 | notExpected := unstructured.Unstructured{} 83 | notExpected.SetName("resource-with-owner") 84 | notExpected.SetOwnerReferences([]v1.OwnerReference{ 85 | { 86 | Name: "owner", 87 | }, 88 | }) 89 | 90 | list.Items = append(list.Items, expected, notExpected) 91 | return list 92 | }, 93 | expectedPass: 1, 94 | }, 95 | { 96 | name: "A secret which belongs to a service account is ignored", 97 | filters: func() []FilterFunc { 98 | return []FilterFunc{IgnoreServiceAccountSecret()} 99 | }, 100 | list: func() *unstructured.UnstructuredList { 101 | list := &unstructured.UnstructuredList{} 102 | expected := unstructured.Unstructured{} 103 | expected.SetName("secret") 104 | expected.SetAPIVersion("v1") 105 | expected.SetKind("Secret") 106 | 107 | notExpected := unstructured.Unstructured{} 108 | notExpected.SetName("service-account-secret") 109 | notExpected.SetAPIVersion("v1") 110 | notExpected.SetKind("Secret") 111 | notExpected.SetAnnotations(map[string]string{ 112 | "kubernetes.io/service-account.name": "sa", 113 | }) 114 | 115 | list.Items = append(list.Items, expected, notExpected) 116 | return list 117 | }, 118 | expectedPass: 1, 119 | }, 120 | { 121 | name: "A secret which is labeled as a helm owner is ignored", 122 | filters: func() []FilterFunc { 123 | return []FilterFunc{IgnoreHelmSecret()} 124 | }, 125 | list: func() *unstructured.UnstructuredList { 126 | list := &unstructured.UnstructuredList{} 127 | expected := unstructured.Unstructured{} 128 | expected.SetName("secret") 129 | expected.SetAPIVersion("v1") 130 | expected.SetKind("Secret") 131 | 132 | notExpected := unstructured.Unstructured{} 133 | notExpected.SetName("service-account-secret") 134 | notExpected.SetAPIVersion("v1") 135 | notExpected.SetKind("Secret") 136 | notExpected.SetLabels(map[string]string{ 137 | "owner": "helm", 138 | }) 139 | 140 | list.Items = append(list.Items, expected, notExpected) 141 | return list 142 | }, 143 | expectedPass: 1, 144 | }, 145 | { 146 | name: "A resource which is part of a helmrelease is ignored", 147 | filters: func() []FilterFunc { 148 | helmReleases := []helmapi.HelmRelease{} 149 | hr := helmapi.HelmRelease{} 150 | hr.SetName("release") 151 | hr.SetNamespace("test") 152 | 153 | helmReleases = append(helmReleases, hr) 154 | 155 | return []FilterFunc{IgnoreIfHelmReleaseFound(helmReleases)} 156 | }, 157 | list: func() *unstructured.UnstructuredList { 158 | list := &unstructured.UnstructuredList{} 159 | expected := unstructured.Unstructured{} 160 | expected.SetName("resource") 161 | 162 | alsoExpected := unstructured.Unstructured{} 163 | alsoExpected.SetName("service-account-secret") 164 | alsoExpected.SetLabels(map[string]string{ 165 | fluxHelmNameLabel: "release", 166 | fluxHelmNamespaceLabel: "not-existing", 167 | }) 168 | 169 | notExpected := unstructured.Unstructured{} 170 | notExpected.SetName("service-account-secret") 171 | notExpected.SetLabels(map[string]string{ 172 | fluxHelmNameLabel: "release", 173 | fluxHelmNamespaceLabel: "test", 174 | }) 175 | 176 | list.Items = append(list.Items, expected, alsoExpected, notExpected) 177 | return list 178 | }, 179 | expectedPass: 2, 180 | }, 181 | { 182 | name: "A resource which is part of a kustomization but without a matching inventory entry is not ignored", 183 | filters: func() []FilterFunc { 184 | kustomizations := &ksapi.KustomizationList{} 185 | ks := ksapi.Kustomization{} 186 | ks.SetName("release") 187 | ks.SetNamespace("test") 188 | 189 | kustomizations.Items = append(kustomizations.Items, ks) 190 | 191 | return []FilterFunc{IgnoreIfKustomizationFound(kustomizations.Items)} 192 | }, 193 | list: func() *unstructured.UnstructuredList { 194 | list := &unstructured.UnstructuredList{} 195 | expected := unstructured.Unstructured{} 196 | expected.SetName("resource") 197 | 198 | alsoExpected := unstructured.Unstructured{} 199 | alsoExpected.SetName("service-account-secret") 200 | alsoExpected.SetLabels(map[string]string{ 201 | fluxKustomizeNameLabel: "release", 202 | fluxKustomizeNamespaceLabel: "test", 203 | }) 204 | 205 | list.Items = append(list.Items, expected, alsoExpected) 206 | return list 207 | }, 208 | expectedPass: 2, 209 | }, 210 | { 211 | name: "A resource which is part of a kustomization and has a valid matching inventory entry is ignored", 212 | filters: func() []FilterFunc { 213 | kustomizations := &ksapi.KustomizationList{} 214 | ks := ksapi.Kustomization{} 215 | ks.SetName("release") 216 | ks.SetNamespace("test") 217 | ks.Status.Inventory = &ksapi.ResourceInventory{ 218 | Entries: []ksapi.ResourceRef{ 219 | { 220 | ID: "test_cluster-role__test_rbac.authorization.k8s.io_ClusterRole", 221 | }, 222 | }, 223 | } 224 | 225 | kustomizations.Items = append(kustomizations.Items, ks) 226 | 227 | return []FilterFunc{IgnoreIfKustomizationFound(kustomizations.Items)} 228 | }, 229 | list: func() *unstructured.UnstructuredList { 230 | list := &unstructured.UnstructuredList{} 231 | expected := unstructured.Unstructured{} 232 | expected.SetName("resource") 233 | 234 | alsoExpected := unstructured.Unstructured{} 235 | alsoExpected.SetName("service-account-secret") 236 | alsoExpected.SetLabels(map[string]string{ 237 | fluxKustomizeNameLabel: "release", 238 | fluxKustomizeNamespaceLabel: "test", 239 | }) 240 | 241 | notExpected := unstructured.Unstructured{} 242 | notExpected.SetGroupVersionKind(schema.GroupVersionKind{ 243 | Group: "rbac.authorization.k8s.io", 244 | Version: "v1", 245 | Kind: "ClusterRole", 246 | }) 247 | notExpected.SetNamespace("test") 248 | notExpected.SetName("cluster-role:test") 249 | notExpected.SetLabels(map[string]string{ 250 | fluxKustomizeNameLabel: "release", 251 | fluxKustomizeNamespaceLabel: "test", 252 | }) 253 | 254 | list.Items = append(list.Items, expected, alsoExpected, notExpected) 255 | return list 256 | }, 257 | expectedPass: 2, 258 | }, 259 | { 260 | name: "A resource which is part of a kustomization but the kustomization was not found", 261 | filters: func() []FilterFunc { 262 | kustomizations := &ksapi.KustomizationList{} 263 | ks := ksapi.Kustomization{} 264 | ks.SetName("release") 265 | ks.SetNamespace("test") 266 | ks.Status.Inventory = &ksapi.ResourceInventory{ 267 | Entries: []ksapi.ResourceRef{ 268 | { 269 | ID: "test_service-account-secret__Secret", 270 | }, 271 | }, 272 | } 273 | 274 | kustomizations.Items = append(kustomizations.Items, ks) 275 | 276 | return []FilterFunc{IgnoreIfKustomizationFound(kustomizations.Items)} 277 | }, 278 | list: func() *unstructured.UnstructuredList { 279 | list := &unstructured.UnstructuredList{} 280 | expected := unstructured.Unstructured{} 281 | expected.SetName("service-account-secret") 282 | expected.SetLabels(map[string]string{ 283 | fluxKustomizeNameLabel: "does-not-exists", 284 | fluxKustomizeNamespaceLabel: "does-not-exists", 285 | }) 286 | 287 | notExpected := unstructured.Unstructured{} 288 | notExpected.SetGroupVersionKind(schema.GroupVersionKind{ 289 | Group: "", 290 | Version: "v1", 291 | Kind: "Secret", 292 | }) 293 | notExpected.SetNamespace("test") 294 | notExpected.SetName("service-account-secret") 295 | notExpected.SetLabels(map[string]string{ 296 | fluxKustomizeNameLabel: "release", 297 | fluxKustomizeNamespaceLabel: "test", 298 | }) 299 | 300 | list.Items = append(list.Items, expected, notExpected) 301 | return list 302 | }, 303 | expectedPass: 1, 304 | }, 305 | { 306 | name: "Resources excluded from conf: match all", 307 | filters: func() []FilterFunc { 308 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 309 | {}, 310 | })} 311 | }, 312 | list: getExclusionListResourceSet, 313 | expectedPass: 0, 314 | }, 315 | { 316 | name: "Resources excluded from conf: match restricted by cluster", 317 | filters: func() []FilterFunc { 318 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 319 | { 320 | Cluster: "test", 321 | }, 322 | })} 323 | }, 324 | list: getExclusionListResourceSet, 325 | expectedPass: 0, 326 | }, 327 | { 328 | name: "Resources excluded from conf: match restricted by cluster (regexp)", 329 | filters: func() []FilterFunc { 330 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 331 | { 332 | Cluster: "t.*", 333 | }, 334 | })} 335 | }, 336 | list: getExclusionListResourceSet, 337 | expectedPass: 0, 338 | }, 339 | { 340 | name: "Resources excluded from conf: match restricted by apiVersion", 341 | filters: func() []FilterFunc { 342 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 343 | { 344 | TypeMeta: v1.TypeMeta{APIVersion: "velero.io/v1"}, 345 | }, 346 | })} 347 | }, 348 | list: getExclusionListResourceSet, 349 | expectedPass: 1, 350 | }, 351 | { 352 | name: "Resources excluded from conf: match restricted by apiVersion and kind", 353 | filters: func() []FilterFunc { 354 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 355 | { 356 | TypeMeta: v1.TypeMeta{APIVersion: "velero.io/v1", Kind: "Backup"}, 357 | }, 358 | })} 359 | }, 360 | list: getExclusionListResourceSet, 361 | expectedPass: 2, 362 | }, 363 | { 364 | name: "Resources excluded from conf: match restricted by namespace", 365 | filters: func() []FilterFunc { 366 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 367 | { 368 | Namespace: "velero", 369 | }, 370 | })} 371 | }, 372 | list: getExclusionListResourceSet, 373 | expectedPass: 1, 374 | }, 375 | { 376 | name: "Resources excluded from conf: match restricted by namespace (regexp)", 377 | filters: func() []FilterFunc { 378 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 379 | { 380 | Namespace: "v.*", 381 | }, 382 | })} 383 | }, 384 | list: getExclusionListResourceSet, 385 | expectedPass: 0, 386 | }, 387 | { 388 | name: "Resources excluded from conf: match restricted by annotation", 389 | filters: func() []FilterFunc { 390 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 391 | { 392 | Annotations: map[string]string{"test-annotation": "velero-capi-backup-1"}, 393 | }, 394 | })} 395 | }, 396 | list: getExclusionListResourceSet, 397 | expectedPass: 2, 398 | }, 399 | { 400 | name: "Resources excluded from conf: match restricted by annotation (regexp)", 401 | filters: func() []FilterFunc { 402 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 403 | { 404 | Annotations: map[string]string{"test-annotation": "v.*"}, 405 | }, 406 | })} 407 | }, 408 | list: getExclusionListResourceSet, 409 | expectedPass: 2, 410 | }, 411 | { 412 | name: "Resources excluded from conf: match restricted by label", 413 | filters: func() []FilterFunc { 414 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 415 | { 416 | Labels: map[string]string{"test-label": "velero-capi-backup-2"}, 417 | }, 418 | })} 419 | }, 420 | list: getExclusionListResourceSet, 421 | expectedPass: 2, 422 | }, 423 | { 424 | name: "Resources excluded from conf: match restricted by label (regexp)", 425 | filters: func() []FilterFunc { 426 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 427 | { 428 | Labels: map[string]string{"test-label": "v.*"}, 429 | }, 430 | })} 431 | }, 432 | list: getExclusionListResourceSet, 433 | expectedPass: 2, 434 | }, 435 | { 436 | name: "Resources excluded from conf: match restricted by name", 437 | filters: func() []FilterFunc { 438 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 439 | { 440 | Name: "velero-capi-backup-1", 441 | }, 442 | })} 443 | }, 444 | list: getExclusionListResourceSet, 445 | expectedPass: 2, 446 | }, 447 | { 448 | name: "Resources excluded from conf: match restricted by name (regexp)", 449 | filters: func() []FilterFunc { 450 | return []FilterFunc{IgnoreRuleExclusions("test", []gitopszombiesv1.ExcludeResources{ 451 | { 452 | Name: "velero-capi-backup-(1|2)", 453 | }, 454 | })} 455 | }, 456 | list: getExclusionListResourceSet, 457 | expectedPass: 1, 458 | }, 459 | } 460 | 461 | for _, test := range tests { 462 | t.Run(test.name, func(t *testing.T) { 463 | ch := make(chan unstructured.Unstructured, test.expectedPass+1) 464 | discovery := NewDiscovery(klog.NewKlogr(), test.filters()...) 465 | err := discovery.Discover(context.TODO(), test.list(), ch) 466 | require.NoError(t, err) 467 | assert.Equal(t, test.expectedPass, len(ch)) 468 | }) 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /pkg/detector/blacklist.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime/schema" 5 | ) 6 | 7 | // backlist are resources which are ignored from validation. 8 | func getBlacklist() []schema.GroupVersionResource { 9 | return []schema.GroupVersionResource{ 10 | { 11 | Version: "v1", 12 | Resource: "events", 13 | }, 14 | { 15 | Version: "v1", 16 | Resource: "endpoints", 17 | }, 18 | { 19 | Version: "v1", 20 | Resource: "componentstatuses", 21 | }, 22 | { 23 | Version: "v1", 24 | Resource: "persistentvolumeclaims", 25 | }, 26 | { 27 | Version: "v1", 28 | Resource: "persistentvolumes", 29 | }, 30 | { 31 | Version: "v1", 32 | Group: "storage.k8s.io", 33 | Resource: "volumeattachments", 34 | }, 35 | { 36 | Version: "v1", 37 | Resource: "nodes", 38 | }, 39 | { 40 | Version: "v1beta1", 41 | Group: "events.k8s.io", 42 | Resource: "events", 43 | }, 44 | { 45 | Version: "v1", 46 | Group: "events.k8s.io", 47 | Resource: "events", 48 | }, 49 | { 50 | Version: "v1beta1", 51 | Group: "metrics.k8s.io", 52 | Resource: "pods", 53 | }, 54 | { 55 | Version: "v1beta1", 56 | Group: "metrics.k8s.io", 57 | Resource: "nodes", 58 | }, 59 | { 60 | Version: "v1", 61 | Group: "coordination.k8s.io", 62 | Resource: "leases", 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/detector/clients.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | ksapi "github.com/fluxcd/kustomize-controller/api/v1beta2" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/serializer" 10 | "k8s.io/apimachinery/pkg/util/json" 11 | "k8s.io/cli-runtime/pkg/genericclioptions" 12 | "k8s.io/client-go/discovery" 13 | "k8s.io/client-go/dynamic" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/clientcmd" 16 | ) 17 | 18 | func getDiscoveryClient(kubeconfigArgs *genericclioptions.ConfigFlags) (*discovery.DiscoveryClient, error) { 19 | cfg, err := kubeconfigArgs.ToRESTConfig() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | cfg.WarningHandler = rest.NoWarnings{} 25 | 26 | client, err := discovery.NewDiscoveryClientForConfig(cfg) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return client, nil 32 | } 33 | 34 | func getDynClient(kubeconfigArgs *genericclioptions.ConfigFlags) (dynamic.Interface, error) { 35 | cfg, err := kubeconfigArgs.ToRESTConfig() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | client, err := dynamic.NewForConfig(cfg) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return client, nil 46 | } 47 | 48 | func getRestClient(kubeconfigArgs *genericclioptions.ConfigFlags) (*rest.RESTClient, error) { 49 | cfg, err := kubeconfigArgs.ToRESTConfig() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | cfg.GroupVersion = &ksapi.GroupVersion 55 | scheme := runtime.NewScheme() 56 | err = ksapi.AddToScheme(scheme) 57 | if err != nil { 58 | return nil, err 59 | } 60 | codecs := serializer.NewCodecFactory(scheme) 61 | cfg.NegotiatedSerializer = codecs.WithoutConversion() 62 | cfg.APIPath = "/apis" 63 | 64 | client, err := rest.RESTClientFor(cfg) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return client, nil 70 | } 71 | 72 | func getClusterClientsFromConfig(ctx context.Context, gitopsClient dynamic.Interface, namespace string, specStr interface{}) (string, clusterClients, error) { 73 | spec := ksapi.KustomizationSpec{} 74 | b, err := json.Marshal(specStr) 75 | if err != nil { 76 | return "", clusterClients{}, err 77 | } 78 | 79 | err = json.Unmarshal(b, &spec) 80 | if err != nil { 81 | return "", clusterClients{}, err 82 | } 83 | 84 | secret, err := loadKubeconfigSecret(ctx, gitopsClient, namespace, spec.KubeConfig.SecretRef.Name) 85 | if err != nil { 86 | return "", clusterClients{}, err 87 | } 88 | 89 | var kubeConfig []byte 90 | switch { 91 | case spec.KubeConfig.SecretRef.Key != "": 92 | key := spec.KubeConfig.SecretRef.Key 93 | kubeConfig = secret.Data[key] 94 | if kubeConfig == nil { 95 | return "", clusterClients{}, fmt.Errorf("KubeConfig secret '%s' does not contain a '%s' key with a kubeconfig", spec.KubeConfig.SecretRef.Name, key) 96 | } 97 | case secret.Data["value"] != nil: 98 | kubeConfig = secret.Data["value"] 99 | case secret.Data["value.yaml"] != nil: 100 | kubeConfig = secret.Data["value.yaml"] 101 | default: 102 | return "", clusterClients{}, fmt.Errorf("KubeConfig secret '%s' does not contain a 'value' nor 'value.yaml' key with a kubeconfig", spec.KubeConfig.SecretRef.Name) 103 | } 104 | restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeConfig) 105 | if err != nil { 106 | return "", clusterClients{}, err 107 | } 108 | 109 | dynClient, err := dynamic.NewForConfig(restConfig) 110 | if err != nil { 111 | return "", clusterClients{}, err 112 | } 113 | 114 | restConfig.WarningHandler = rest.NoWarnings{} 115 | 116 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) 117 | if err != nil { 118 | return "", clusterClients{}, err 119 | } 120 | 121 | cfg, err := clientcmd.Load(kubeConfig) 122 | if err != nil { 123 | return "", clusterClients{}, err 124 | } 125 | 126 | return cfg.Contexts[cfg.CurrentContext].Cluster, clusterClients{dynamic: dynClient, discovery: discoveryClient}, nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/detector/detector.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | 10 | helmapi "github.com/fluxcd/helm-controller/api/v2beta1" 11 | ksapi "github.com/fluxcd/kustomize-controller/api/v1beta2" 12 | gitopszombiesv1 "github.com/raffis/gitops-zombies/pkg/apis/gitopszombies/v1" 13 | "github.com/raffis/gitops-zombies/pkg/collector" 14 | "golang.org/x/exp/slices" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/runtime/schema" 19 | "k8s.io/cli-runtime/pkg/genericclioptions" 20 | "k8s.io/client-go/discovery" 21 | "k8s.io/client-go/dynamic" 22 | "k8s.io/client-go/rest" 23 | "k8s.io/klog/v2" 24 | k8sget "k8s.io/kubectl/pkg/cmd/get" 25 | ) 26 | 27 | const ( 28 | fluxClusterName = "self" 29 | defaultLabelSelector = "kubernetes.io/bootstrapping!=rbac-defaults,kube-aggregator.kubernetes.io/automanaged!=onstart,kube-aggregator.kubernetes.io/automanaged!=true" 30 | ) 31 | 32 | type clusterDetectionResult struct { 33 | cluster string 34 | resourceCount int 35 | zombies []unstructured.Unstructured 36 | } 37 | 38 | type clusterClients struct { 39 | dynamic dynamic.Interface 40 | discovery *discovery.DiscoveryClient 41 | } 42 | 43 | // Detector owns detector materials. 44 | type Detector struct { 45 | gitopsDynClient dynamic.Interface 46 | clusterDiscoveryClient *discovery.DiscoveryClient 47 | clusterDynClient dynamic.Interface 48 | gitopsRestClient *rest.RESTClient 49 | kubeconfigArgs *genericclioptions.ConfigFlags 50 | printFlags *k8sget.PrintFlags 51 | conf *gitopszombiesv1.Config 52 | } 53 | 54 | // New creates a new detection object. 55 | func New(conf *gitopszombiesv1.Config, kubeconfigArgs *genericclioptions.ConfigFlags, printFlags *k8sget.PrintFlags) (*Detector, error) { 56 | gitopsDynClient, err := getDynClient(kubeconfigArgs) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | clusterDiscoveryClient, err := getDiscoveryClient(kubeconfigArgs) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | clusterDynClient, err := getDynClient(kubeconfigArgs) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | gitopsRestClient, err := getRestClient(kubeconfigArgs) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return &Detector{ 77 | gitopsDynClient: gitopsDynClient, 78 | clusterDiscoveryClient: clusterDiscoveryClient, 79 | clusterDynClient: clusterDynClient, 80 | gitopsRestClient: gitopsRestClient, 81 | conf: conf, 82 | kubeconfigArgs: kubeconfigArgs, 83 | printFlags: printFlags, 84 | }, nil 85 | } 86 | 87 | // DetectZombies detects all workload not managed by gitops. 88 | func (d *Detector) DetectZombies() (resourceCount int, zombies map[string][]unstructured.Unstructured, err error) { 89 | zombies = make(map[string][]unstructured.Unstructured) 90 | ch := make(chan clusterDetectionResult) 91 | 92 | helmReleases, kustomizations, clustersConfigs, err := d.listGitopsResources() 93 | if err != nil { 94 | return 0, nil, err 95 | } 96 | 97 | var wg sync.WaitGroup 98 | clustersConfigs[fluxClusterName] = clusterClients{dynamic: d.clusterDynClient, discovery: d.clusterDiscoveryClient} 99 | 100 | for cluster := range clustersConfigs { 101 | if d.conf.ExcludeClusters != nil && slices.Contains(d.conf.ExcludeClusters, cluster) { 102 | klog.Infof("[%s] excluding from zombie detection", cluster) 103 | continue 104 | } 105 | 106 | wg.Add(1) 107 | go func(cluster string) { 108 | defer wg.Done() 109 | 110 | clusterResourceCount, clusterZombies, err := d.detectZombiesOnCluster(cluster, helmReleases, kustomizations, clustersConfigs[cluster].dynamic, clustersConfigs[cluster].discovery) 111 | if err != nil { 112 | klog.Errorf("[%s] could not detect zombies on: %w", cluster, err) 113 | } 114 | ch <- clusterDetectionResult{ 115 | cluster: cluster, 116 | resourceCount: clusterResourceCount, 117 | zombies: clusterZombies, 118 | } 119 | }(cluster) 120 | } 121 | 122 | go func() { 123 | wg.Wait() 124 | close(ch) 125 | }() 126 | 127 | for res := range ch { 128 | resourceCount += res.resourceCount 129 | zombies[res.cluster] = res.zombies 130 | } 131 | 132 | return resourceCount, zombies, nil 133 | } 134 | 135 | // PrintZombies prints all workload not managed by gitops. 136 | func (d *Detector) PrintZombies(allZombies map[string][]unstructured.Unstructured) error { 137 | p, err := d.printFlags.ToPrinter() 138 | if err != nil { 139 | return err 140 | } 141 | 142 | for clusterName, zombies := range allZombies { 143 | for _, zombie := range zombies { 144 | if *d.printFlags.OutputFormat == "" { 145 | ok := zombie.GetObjectKind().GroupVersionKind() 146 | fmt.Printf("[%s] %s: %s.%s\n", clusterName, ok.String(), zombie.GetName(), zombie.GetNamespace()) 147 | } else { 148 | z := zombie 149 | if err := p.PrintObj(&z, os.Stdout); err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func (d *Detector) detectZombiesOnCluster(clusterName string, helmReleases []helmapi.HelmRelease, kustomizations []ksapi.Kustomization, clusterDynClient dynamic.Interface, clusterDiscoveryClient *discovery.DiscoveryClient) (int, []unstructured.Unstructured, error) { 160 | var ( 161 | resourceCount int 162 | zombies []unstructured.Unstructured 163 | ) 164 | 165 | discover := collector.NewDiscovery( 166 | klog.NewKlogr().WithValues("cluster", clusterName), 167 | collector.IgnoreOwnedResource(), 168 | collector.IgnoreServiceAccountSecret(), 169 | collector.IgnoreHelmSecret(), 170 | collector.IgnoreIfHelmReleaseFound(helmReleases), 171 | collector.IgnoreIfKustomizationFound(kustomizations), 172 | collector.IgnoreRuleExclusions(clusterName, d.conf.ExcludeResources), 173 | ) 174 | 175 | var list []*metav1.APIResourceList 176 | klog.V(1).Infof("[%s] discover all api groups and resources", clusterName) 177 | list, err := listServerGroupsAndResources(clusterDiscoveryClient) 178 | if err != nil { 179 | return 0, nil, err 180 | } 181 | for _, g := range list { 182 | klog.V(1).Infof("[%s] found group %v with the following resources", clusterName, g.GroupVersion) 183 | for _, r := range g.APIResources { 184 | var namespaceStr string 185 | if r.Namespaced { 186 | namespaceStr = " (namespaced)" 187 | } 188 | klog.V(1).Infof("[%s] |_ %v%v verbs: %v", clusterName, r.Kind, namespaceStr, r.Verbs) 189 | } 190 | } 191 | 192 | ch := make(chan unstructured.Unstructured) 193 | var wgProducer, wgConsumer sync.WaitGroup 194 | for _, group := range list { 195 | klog.V(1).Infof("[%s] discover resource group %#v", clusterName, group.GroupVersion) 196 | gv, err := schema.ParseGroupVersion(group.GroupVersion) 197 | if err != nil { 198 | return 0, nil, err 199 | } 200 | 201 | for _, resource := range group.APIResources { 202 | klog.V(1).Infof("[%s] discover resource %#v.%#v.%#v", clusterName, resource.Name, resource.Group, resource.Version) 203 | 204 | gvr, err := d.validateResource(*d.kubeconfigArgs.Namespace, gv, resource) 205 | if err != nil { 206 | klog.V(1).Infof("[%s] %v", clusterName, err.Error()) 207 | continue 208 | } 209 | 210 | resAPI := clusterDynClient.Resource(*gvr).Namespace(*d.kubeconfigArgs.Namespace) 211 | 212 | wgProducer.Add(1) 213 | 214 | go func(resAPI dynamic.ResourceInterface) { 215 | defer wgProducer.Done() 216 | 217 | count, err := handleResource(context.TODO(), discover, resAPI, ch, d.getLabelSelector()) 218 | if err != nil { 219 | klog.V(1).Infof("[%s] could not handle resource: %w", clusterName, err) 220 | } 221 | resourceCount += count 222 | }(resAPI) 223 | } 224 | } 225 | 226 | wgConsumer.Add(1) 227 | go func() { 228 | defer wgConsumer.Done() 229 | for res := range ch { 230 | if d.conf.NoStream { 231 | zombies = append(zombies, res) 232 | } else { 233 | _ = d.PrintZombies(map[string][]unstructured.Unstructured{clusterName: {res}}) 234 | } 235 | } 236 | }() 237 | 238 | wgProducer.Wait() 239 | close(ch) 240 | wgConsumer.Wait() 241 | 242 | return resourceCount, zombies, nil 243 | } 244 | 245 | func (d *Detector) listGitopsResources() ([]helmapi.HelmRelease, []ksapi.Kustomization, map[string]clusterClients, error) { 246 | klog.V(1).Infof("discover all helmreleases") 247 | helmReleases, err := listHelmReleases(context.TODO(), d.gitopsDynClient, d.getLabelSelector()) 248 | if err != nil { 249 | return nil, nil, nil, fmt.Errorf("failed to get helmreleases: %w", err) 250 | } 251 | for _, h := range helmReleases { 252 | klog.V(1).Infof(" |_ %s.%s", h.GetName(), h.GetNamespace()) 253 | } 254 | 255 | klog.V(1).Infof("discover all kustomizations") 256 | kustomizations, err := listKustomizations(context.TODO(), d.gitopsRestClient) 257 | if err != nil { 258 | return nil, nil, nil, fmt.Errorf("failed to get kustomizations: %w", err) 259 | } 260 | 261 | for _, k := range kustomizations { 262 | klog.V(1).Infof(" |_ %s.%s", k.GetName(), k.GetNamespace()) 263 | } 264 | 265 | klog.V(1).Infof("discover all managed clustersClients") 266 | clustersClients, err := d.getClustersClientsFromKustomizationsAndHelmReleases(context.TODO(), d.gitopsDynClient, kustomizations, helmReleases) 267 | if err != nil { 268 | return nil, nil, nil, fmt.Errorf("failed to get managed clustersClients: %w", err) 269 | } 270 | 271 | for clusterName := range clustersClients { 272 | klog.V(1).Infof(" |_ %s", clusterName) 273 | } 274 | 275 | return helmReleases, kustomizations, clustersClients, nil 276 | } 277 | 278 | func (d *Detector) getClustersClientsFromKustomizationsAndHelmReleases(ctx context.Context, gitopsClient dynamic.Interface, kustomizations []ksapi.Kustomization, helmReleases []helmapi.HelmRelease) (map[string]clusterClients, error) { 279 | resourcesWithSecrets := map[string]*unstructured.Unstructured{} 280 | clients := make(map[string]clusterClients) 281 | 282 | for _, ks := range kustomizations { 283 | ks := ks 284 | if ks.Spec.KubeConfig != nil { 285 | key := fmt.Sprintf("%s/%s", ks.Namespace, ks.Spec.KubeConfig.SecretRef.Name) 286 | if _, ok := resourcesWithSecrets[key]; !ok { 287 | ksu, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&ks) 288 | if err != nil { 289 | return nil, err 290 | } 291 | resourcesWithSecrets[key] = &unstructured.Unstructured{Object: ksu} 292 | } 293 | } 294 | } 295 | 296 | for _, hr := range helmReleases { 297 | hr := hr 298 | if hr.Spec.KubeConfig != nil { 299 | key := fmt.Sprintf("%s/%s", hr.Namespace, hr.Spec.KubeConfig.SecretRef.Name) 300 | if _, ok := resourcesWithSecrets[key]; !ok { 301 | hru, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&hr) 302 | if err != nil { 303 | return nil, err 304 | } 305 | resourcesWithSecrets[key] = &unstructured.Unstructured{Object: hru} 306 | } 307 | } 308 | } 309 | 310 | for _, r := range resourcesWithSecrets { 311 | clusterName, clusterClts, err := getClusterClientsFromConfig(ctx, gitopsClient, r.GetNamespace(), r.Object["spec"]) 312 | if err != nil { 313 | return nil, err 314 | } 315 | 316 | clients[clusterName] = clusterClts 317 | } 318 | 319 | return clients, nil 320 | } 321 | 322 | func (d *Detector) getLabelSelector() string { 323 | selector := "" 324 | if !d.conf.IncludeAll { 325 | selector = defaultLabelSelector 326 | } 327 | 328 | if d.conf.LabelSelector != "" { 329 | selector = strings.Join(append(strings.Split(selector, ","), strings.Split(d.conf.LabelSelector, ",")...), ",") 330 | } 331 | 332 | return selector 333 | } 334 | 335 | func (d *Detector) validateResource(ns string, gv schema.GroupVersion, resource metav1.APIResource) (*schema.GroupVersionResource, error) { 336 | if ns != "" && !resource.Namespaced { 337 | return nil, fmt.Errorf("skipping cluster scoped resource %#v.%#v.%#v, namespaced scope was requested", resource.Name, resource.Group, resource.Version) 338 | } 339 | 340 | gvr := schema.GroupVersionResource{ 341 | Group: gv.Group, 342 | Version: gv.Version, 343 | Resource: resource.Name, 344 | } 345 | 346 | if !d.conf.IncludeAll { 347 | for _, listed := range getBlacklist() { 348 | if listed == gvr { 349 | return nil, fmt.Errorf("skipping blacklisted api resource %v/%v.%v", gvr.Group, gvr.Version, gvr.Resource) 350 | } 351 | } 352 | } 353 | 354 | if !slices.Contains(resource.Verbs, "list") { 355 | return nil, fmt.Errorf("skipping resource %v/%v.%v: unable to list", gvr.Group, gvr.Version, gvr.Resource) 356 | } 357 | 358 | return &gvr, nil 359 | } 360 | 361 | func handleResource(ctx context.Context, discover collector.Interface, resAPI dynamic.ResourceInterface, ch chan unstructured.Unstructured, labelSelector string) (int, error) { 362 | list, err := resAPI.List(ctx, metav1.ListOptions{ 363 | LabelSelector: labelSelector, 364 | }) 365 | if err != nil { 366 | return 0, err 367 | } 368 | 369 | return len(list.Items), discover.Discover(ctx, list, ch) 370 | } 371 | -------------------------------------------------------------------------------- /pkg/detector/gitops_resources.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "context" 5 | 6 | helmapi "github.com/fluxcd/helm-controller/api/v2beta1" 7 | ksapi "github.com/fluxcd/kustomize-controller/api/v1beta2" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/client-go/dynamic" 13 | "k8s.io/client-go/rest" 14 | ) 15 | 16 | func listResources(ctx context.Context, resAPI dynamic.ResourceInterface, labelSelector string) (items []unstructured.Unstructured, err error) { 17 | list, err := resAPI.List(ctx, metav1.ListOptions{ 18 | LabelSelector: labelSelector, 19 | }) 20 | if err != nil { 21 | return items, err 22 | } 23 | 24 | return list.Items, err 25 | } 26 | 27 | func listHelmReleases(ctx context.Context, gitopsClient dynamic.Interface, labelSelector string) ([]helmapi.HelmRelease, error) { 28 | helmReleases := []helmapi.HelmRelease{} 29 | list, err := listResources(ctx, 30 | gitopsClient.Resource(schema.GroupVersionResource{ 31 | Group: "helm.toolkit.fluxcd.io", 32 | Version: "v2beta1", 33 | Resource: "helmreleases", 34 | }), labelSelector) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | for _, element := range list { 40 | c := helmapi.HelmRelease{} 41 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(element.UnstructuredContent(), &c) 42 | if err != nil { 43 | return nil, err 44 | } 45 | helmReleases = append(helmReleases, c) 46 | } 47 | 48 | return helmReleases, nil 49 | } 50 | 51 | func listKustomizations(ctx context.Context, client *rest.RESTClient) ([]ksapi.Kustomization, error) { 52 | ks := &ksapi.KustomizationList{} 53 | 54 | r := client. 55 | Get(). 56 | Resource("kustomizations"). 57 | Do(ctx) 58 | 59 | err := r.Into(ks) 60 | if err != nil { 61 | return []ksapi.Kustomization{}, err 62 | } 63 | 64 | return ks.Items, err 65 | } 66 | -------------------------------------------------------------------------------- /pkg/detector/resources.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "context" 5 | 6 | v1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/client-go/discovery" 11 | "k8s.io/client-go/dynamic" 12 | ) 13 | 14 | func listServerGroupsAndResources(clusterDiscoveryClient *discovery.DiscoveryClient) ([]*metav1.APIResourceList, error) { 15 | _, list, err := clusterDiscoveryClient.ServerGroupsAndResources() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return list, err 21 | } 22 | 23 | func loadKubeconfigSecret(ctx context.Context, gitopsClient dynamic.Interface, namespace, name string) (*v1.Secret, error) { 24 | var secret v1.Secret 25 | element, err := gitopsClient.Resource(schema.GroupVersionResource{ 26 | Group: "", 27 | Version: "v1", 28 | Resource: "secrets", 29 | }).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = runtime.DefaultUnstructuredConverter.FromUnstructured(element.UnstructuredContent(), &secret) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &secret, nil 40 | } 41 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "github>whitesource/merge-confidence:beta", ":semanticCommitTypeAll(chore)"], 3 | "prHourlyLimit": 50, 4 | "prConcurrentLimit": 10, 5 | "osvVulnerabilityAlerts": true, 6 | "vulnerabilityAlerts": { 7 | "labels": [ 8 | "security" 9 | ] 10 | }, 11 | "stabilityDays": 3, 12 | "packageRules": [ 13 | { 14 | "matchPaths": ["**"], 15 | "labels": ["dependencies", "{{manager}}"] 16 | }, 17 | { 18 | "semanticCommitScope": "deps-dev", 19 | "matchManagers": ["github-actions"] 20 | } 21 | ], 22 | "postUpdateOptions": [ 23 | "gomodUpdateImportPaths", 24 | "gomodTidy" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------