├── .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 | 
4 | [](https://github.com/raffis/gitops-zombies/actions/workflows/release.yaml)
5 | [](https://goreportcard.com/report/github.com/raffis/gitops-zombies)
6 | [](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 |

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 |
--------------------------------------------------------------------------------