From 11bd001494f7b08567d1fe7c11dacc67347eb13c Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Wed, 13 Apr 2022 21:00:32 +0200 Subject: [PATCH] MVP --- .github/workflows/go.yml | 60 ++++++++++++++++ .gitignore | 3 +- .golangci.yml | 147 +++++++++++++++++++++++++++++++++++++++ .goreleaser.yaml | 45 ++++++++++++ Tiltfile | 28 ++++++++ deployments/Dockerfile | 7 ++ go.mod | 18 +++-- go.sum | 33 +++++---- main.go | 96 ++++++++++++++++++++++--- reaper/reaper.go | 117 +++++++++++++++++++++++++++++-- reaper/types.go | 17 ++++- scripts/build.sh | 3 + testdata/deployment.yaml | 61 ++++++++++++++++ testdata/kind.yaml | 12 ++++ testdata/target_pod.yaml | 24 +++++++ 15 files changed, 634 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/go.yml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 Tiltfile create mode 100644 deployments/Dockerfile create mode 100755 scripts/build.sh create mode 100644 testdata/deployment.yaml create mode 100644 testdata/kind.yaml create mode 100644 testdata/target_pod.yaml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..36b5728 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,60 @@ +name: Go + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '^1.18' + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + with: + lfs: true + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + # skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b986336..1e4ad75 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ -.idea/ \ No newline at end of file +.idea/ +dist/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7ad2a7b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,147 @@ +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + gci: + local-prefixes: github.com/baez90/kreaper + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + enabled-tags: + - diagnostic + - opinionated + - performance + disabled-checks: + - ifElseChain + - octalLiteral + - wrapperFunc + # see https://github.com/golangci/golangci-lint/issues/2649 + - hugeParam + - rangeValCopy + # settings: + # hugeParam: + # sizeThreshold: 200 + + gocyclo: + min-complexity: 15 + goimports: + local-prefixes: github.com/baez90/kreaper + golint: + min-confidence: 0 + gomnd: + settings: + mnd: + # don't include the "operation" and "assign" + checks: + - argument + - case + - condition + - return + gomoddirectives: + replace-allow-list: + # pin versions + - k8s.io/api + - k8s.io/apiextensions-apiserver + - k8s.io/apimachinery + - k8s.io/client-go + - k8s.io/component-base + govet: + check-shadowing: true + enable-all: true + disable: + - fieldalignment + # see https://github.com/golangci/golangci-lint/issues/2649 + - nilness + - unusedwrite + importas: + no-unaliased: true + alias: + - pkg: (k8s.io/api|k8s.io/apimachinery/pkg/apis)/([A-z0-9]+)/([A-z0-9]+) + alias: $2$3 + lll: + line-length: 140 + misspell: + locale: US + nolintlint: + allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) + allow-unused: false # report any unused nolint directives + require-explanation: false # don't require an explanation for nolint directives + require-specific: true + +linters: + disable-all: true + enable: + - contextcheck + - deadcode + - dogsled + - dupl + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - exportloopref + - funlen + - gocognit + - goconst + # - gocritic + - gocyclo + - godox + - gofumpt + - goimports + - gomoddirectives + - gomnd + - gosec + - gosimple + - govet + - ifshort + - importas + - ineffassign + # - ireturn - enable later + - lll + - misspell + - nakedret + - nestif + - nilnil + - noctx + - nolintlint + - paralleltest + - prealloc + - predeclared + - promlinter + - staticcheck + - structcheck + - stylecheck + - tenv + - testpackage + - thelper + - typecheck + - unconvert + - unparam + - varcheck + - whitespace + # - unused + - wastedassign + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - dupl + - funlen + - gocognit + - gomnd + - govet + - path: magefiles/ + linters: + - deadcode + +run: + skip-files: + - ".*.mock.\\.go$" + modules-download-mode: readonly + timeout: 5m diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..0b32332 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,45 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + - go mod tidy +builds: + - id: kreaper + binary: kreaper + flags: + - -trimpath + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin +archives: + - builds: + - kreaper + replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +dockers: + - ids: + - kreaper + image_templates: + - ghcr.io/baez90/kreaper:latest + - ghcr.io/baez90/kreaper:{{ .Tag }} + - ghcr.io/baez90/kreaper:{{ .Major }} + - ghcr.io/baez90/kreaper:{{ .ShortCommit}} + dockerfile: deployments/Dockerfile diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 0000000..e6d1f9e --- /dev/null +++ b/Tiltfile @@ -0,0 +1,28 @@ +# -*- mode: Python -*- + +local_resource( + 'build', + 'CGO_ENABLED=0 go build -trimpath -ldflags "-w -s" -installsuffix cgo -o dist/kreaper main.go', + deps=['main.go', './reaper'], +) + +debug_dockerfile = """ +FROM docker.io/alpine:3.15 + +COPY kreaper /usr/local/bin/ + +ENTRYPOINT ["/usr/local/bin/kreaper"] +""" + +custom_build( + 'kreaper', + 'docker build -f deployments/Dockerfile -t $EXPECTED_REF --build-arg BASE="docker.io/alpine:3.15" ./dist/', + entrypoint='/usr/local/bin/kreaper', + deps=['./dist/kreaper'], + live_update=[ + sync('./dist/kreaper', '/usr/local/bin/kreaper'), + ] +) + +k8s_yaml(['testdata/target_pod.yaml', 'testdata/deployment.yaml']) +k8s_resource('kreaper', resource_deps=['build']) \ No newline at end of file diff --git a/deployments/Dockerfile b/deployments/Dockerfile new file mode 100644 index 0000000..ad0f951 --- /dev/null +++ b/deployments/Dockerfile @@ -0,0 +1,7 @@ +ARG BASE="gcr.io/distroless/static:nonroot" + +FROM $BASE + +COPY kreaper /usr/local/bin/ + +ENTRYPOINT ["/usr/local/bin/kreaper"] \ No newline at end of file diff --git a/go.mod b/go.mod index 59a7f80..25c0107 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/baez90/kreaper go 1.18 require ( + go.uber.org/zap v1.21.0 k8s.io/api v0.23.5 + k8s.io/apimachinery v0.23.5 k8s.io/client-go v0.23.5 sigs.k8s.io/controller-runtime v0.11.2 ) @@ -17,10 +19,14 @@ require ( github.com/google/go-cmp v0.5.5 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.8.0 // indirect golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect @@ -32,8 +38,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/api v0.23.5 // indirect - k8s.io/apimachinery v0.23.5 // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect @@ -43,9 +47,9 @@ require ( ) replace ( - k8s.io/api => k8s.io/api v0.23.1 - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.1 - k8s.io/apimachinery => k8s.io/apimachinery v0.23.1 - k8s.io/client-go => k8s.io/client-go v0.23.1 - k8s.io/component-base => k8s.io/component-base v0.23.1 + k8s.io/api => k8s.io/api v0.23.5 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.5 + k8s.io/apimachinery => k8s.io/apimachinery v0.23.5 + k8s.io/client-go => k8s.io/client-go v0.23.5 + k8s.io/component-base => k8s.io/component-base v0.23.5 ) diff --git a/go.sum b/go.sum index d19bd07..1896dd5 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -175,6 +177,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -215,6 +218,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -248,9 +252,16 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -606,13 +617,13 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.1 h1:ncu/qfBfUoClqwkTGbeRqqOqBCRoUAflMuOaOD7J0c8= -k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= -k8s.io/apiextensions-apiserver v0.23.1 h1:xxE0q1vLOVZiWORu1KwNRQFsGWtImueOrqSl13sS5EU= -k8s.io/apimachinery v0.23.1 h1:sfBjlDFwj2onG0Ijx5C+SrAoeUscPrmghm7wHP+uXlo= -k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= -k8s.io/client-go v0.23.1 h1:Ma4Fhf/p07Nmj9yAB1H7UwbFHEBrSPg8lviR24U2GiQ= -k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= +k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= @@ -621,7 +632,6 @@ k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -632,7 +642,6 @@ sigs.k8s.io/controller-runtime v0.11.2/go.mod h1:P6QCzrEjLaZGqHsfd+os7JQ+WFZhvB8 sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/main.go b/main.go index bbe8d81..9fac1f7 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,15 @@ package main import ( "context" "flag" + "log" + "os" + "os/signal" "path/filepath" + "strconv" "time" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" @@ -14,36 +20,89 @@ import ( "github.com/baez90/kreaper/reaper" ) -type ReaperTarget string +const defaultKreaperLifetime = 5 * time.Minute var ( kubeconfig string - target reaper.Target - lifetime time.Duration + dryRun bool + logLevel *zapcore.Level + kreaper = reaper.Kreaper{ + Target: lookupEnvOr[reaper.Target]("KREAPER_TARGET", "", reaper.ParseTarget), + } ) func main() { prepareFlags() + setupLogging() + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + + if err := run(ctx); err != nil { + cancel() + os.Exit(1) + } +} + +func run(ctx context.Context) error { + logger := zap.L() + restCfg, err := loadRestConfig() if err != nil { - panic(err) + logger.Error("Failed to get cluster config", zap.Error(err)) + return err } - k8sClient, err := client.NewWithWatch(restCfg, client.Options{}) - labels := client.MatchingLabels{ - "from": "value", + if kreaper.Client, err = client.NewWithWatch(restCfg, client.Options{}); err != nil { + logger.Error("failed to prepare Kubernetes API client", zap.Error(err)) + return err } - k8sClient.Watch(context.Background(), nil, labels) + + return kreaper.Kill(ctx) +} + +func setupLogging() { + cfg := zap.NewProductionConfig() + cfg.Level = zap.NewAtomicLevelAt(*logLevel) + logger, err := cfg.Build() + if err != nil { + log.Fatalf("Failed to setup logging: %v", err) + } + + zap.ReplaceGlobals(logger) } func prepareFlags() { - flag.Var(&target, "target", "Target that should be monitored") - flag.DurationVar(&lifetime, "lifetime", 5*time.Minute, "Lifetime after which all matching targets will be deleted") + logLevel = zap.LevelFlag("log-level", zapcore.InfoLevel, "Log level to use") + flag.Var(&kreaper.Target, "target", "Target that should be monitored") + + flag.BoolVar( + &dryRun, + "dry-run", + lookupEnvOr("KREAPER_DRY_RUN", false, strconv.ParseBool), + "Don't actually delete anything but only list all found pods matching the target - env variable: KREAPER_DRY_RUN", + ) + + flag.DurationVar( + &kreaper.Lifetime, + "lifetime", + lookupEnvOr("KREAPER_LIFETIME", defaultKreaperLifetime, time.ParseDuration), + "Lifetime after which all matching targets will be deleted - env variable: KREAPER_LIFETIME", + ) + + flag.StringVar( + &kreaper.TargetNamespace, + "target-namespace", + lookupEnvOr("KREAPER_TARGET_NAMESPACE", "default", identity[string]), + "Set target namespace in which kreaper will look for pods - env variable: KREAPER_TARGET_NAMESPACE", + ) + if home := homedir.HomeDir(); home != "" { flag.StringVar(&kubeconfig, "kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") } else { flag.StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file") } + + flag.Parse() } func loadRestConfig() (cfg *rest.Config, err error) { @@ -53,3 +112,20 @@ func loadRestConfig() (cfg *rest.Config, err error) { return clientcmd.BuildConfigFromFlags("", kubeconfig) } + +func lookupEnvOr[T any](envKey string, fallback T, parse func(envVal string) (T, error)) T { + envVal := os.Getenv(envKey) + if envVal == "" { + return fallback + } + + if parsed, err := parse(envVal); err != nil { + return fallback + } else { + return parsed + } +} + +func identity[T any](in T) (T, error) { + return in, nil +} diff --git a/reaper/reaper.go b/reaper/reaper.go index 1157102..4303cf6 100644 --- a/reaper/reaper.go +++ b/reaper/reaper.go @@ -1,8 +1,117 @@ package reaper -import "time" +import ( + "context" + "time" -type Reaper struct { - Lifetime time.Duration - Target Target + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/watch" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Kreaper struct { + labelSelector labels.Selector + Client client.WithWatch + Lifetime time.Duration + Target Target + TargetNamespace string +} + +func (k Kreaper) Kill(ctx context.Context) (err error) { + var ( + logger = zap.L() + podList corev1.PodList + ) + + if k.labelSelector, err = k.Target.Selector(); err != nil { + return err + } + + opts := []client.ListOption{ + client.MatchingLabelsSelector{Selector: k.labelSelector}, + client.InNamespace(k.TargetNamespace), + } + + if err = k.Client.List(ctx, &podList, opts...); err != nil { + logger.Error("failed to list", zap.Error(err)) + return err + } + + if len(podList.Items) < 1 { + logger.Warn("No pod targets found") + return nil + } + + for i := range podList.Items { + logger.Info("Found pod", zap.String("pod_name", podList.Items[i].Name)) + } + + watcher, err := k.Client.Watch(ctx, &podList, opts...) + if err != nil { + logger.Error("failed to setup watch", zap.Error(err)) + return err + } + + defer watcher.Stop() + done, err := k.startPodWatcher(ctx, podList, opts) + + select { + case <-time.After(k.Lifetime): + logger.Info("Reached end of lifetime force delete all matching pods") + if err := k.forceDeleteAll(ctx); err != nil { + logger.Error("Failed to delete all pods", zap.Error(err)) + return err + } + return nil + case <-done: + logger.Info("All pods deleted") + return nil + } +} + +func (k *Kreaper) startPodWatcher(ctx context.Context, podList corev1.PodList, opts []client.ListOption) (<-chan struct{}, error) { + logger := zap.L() + watcher, err := k.Client.Watch(ctx, &podList, opts...) + if err != nil { + logger.Error("failed to setup watch", zap.Error(err)) + return nil, err + } + + done := make(chan struct{}) + + go func() { + defer watcher.Stop() + + for ev := range watcher.ResultChan() { + if ev.Type != watch.Deleted { + continue + } + + if err := k.Client.List(ctx, &podList, opts...); err != nil { + continue + } + + if len(podList.Items) == 0 { + close(done) + break + } + } + }() + + return done, nil +} + +func (k *Kreaper) forceDeleteAll(ctx context.Context) error { + return k.Client.DeleteAllOf( + ctx, + new(corev1.Pod), + client.InNamespace(k.TargetNamespace), + client.PropagationPolicy(metav1.DeletePropagationForeground), + client.MatchingLabelsSelector{ + Selector: k.labelSelector, + }, + ) } diff --git a/reaper/types.go b/reaper/types.go index 01c2180..86d0ea0 100644 --- a/reaper/types.go +++ b/reaper/types.go @@ -6,15 +6,24 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" ) var ErrNotAVAlidTarget = errors.New("not a valid target") var _ flag.Value = (*Target)(nil) +func ParseTarget(val string) (Target, error) { + t := Target(val) + if _, err := t.Selector(); err != nil { + return "", err + } + return t, nil +} + type Target string -func (t Target) Selector() (*metav1.LabelSelector, error) { +func (t Target) Selector() (labels.Selector, error) { s := string(t) if s == "" { return nil, ErrNotAVAlidTarget @@ -25,11 +34,13 @@ func (t Target) Selector() (*metav1.LabelSelector, error) { return nil, ErrNotAVAlidTarget } - return &metav1.LabelSelector{ + sel := &metav1.LabelSelector{ MatchLabels: map[string]string{ key: val, }, - }, nil + } + + return metav1.LabelSelectorAsSelector(sel) } func (t Target) String() string { diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..4e034aa --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +CGO_ENABLED=0 go build -trimpath -ldflags "-w -s" -installsuffix cgo -o dist/kreaper main.go \ No newline at end of file diff --git a/testdata/deployment.yaml b/testdata/deployment.yaml new file mode 100644 index 0000000..eddc52f --- /dev/null +++ b/testdata/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kreaper-debug +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kreaper-debug +rules: + - verbs: + - get + - list + - watch + - deletecollection + apiGroups: + - "" + resources: + - pods +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kreaper-debug +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kreaper-debug +subjects: + - kind: ServiceAccount + name: kreaper-debug +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: kreaper +spec: + backoffLimit: 3 + completions: 1 + ttlSecondsAfterFinished: 30 + template: + metadata: + labels: + app.kubernetes.io/name: kreaper + spec: + restartPolicy: OnFailure + serviceAccountName: kreaper-debug + containers: + - name: kreaper + image: kreaper + args: + - -log-level=debug + env: + - name: KREAPER_TARGET + value: org.testcontainers.golang/sessionID=ee8dcc4d-72b5-4e77-8244-37abe525f948 + - name: KREAPER_LIFETIME + value: "30s" + - name: KREAPER_TARGET_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace \ No newline at end of file diff --git a/testdata/kind.yaml b/testdata/kind.yaml new file mode 100644 index 0000000..657939c --- /dev/null +++ b/testdata/kind.yaml @@ -0,0 +1,12 @@ +# Creates a kind cluster with Kind's custom cluster config +# https://pkg.go.dev/sigs.k8s.io/kind/pkg/apis/config/v1alpha4#Cluster +# Creates a cluster with 2 nodes. +apiVersion: ctlptl.dev/v1alpha1 +kind: Cluster +product: kind +registry: ctlptl-registry +kindV1Alpha4Cluster: + name: kind + nodes: + - role: control-plane + - role: worker diff --git a/testdata/target_pod.yaml b/testdata/target_pod.yaml new file mode 100644 index 0000000..663aff7 --- /dev/null +++ b/testdata/target_pod.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: kreaper-target + labels: + org.testcontainers.golang/sessionID: ee8dcc4d-72b5-4e77-8244-37abe525f948 + app.kubernetes.io/created-by: testcontainers-go + app.kubernetes.io/managed-by: testcontainers-go +spec: + terminationGracePeriodSeconds: 1 + containers: + - name: busybox + image: docker.io/busybox:latest + command: + - /bin/sh + - -c + - "sleep 15 && touch /tmp/healthy && sleep 7200" + startupProbe: + exec: + command: + - cat + - /tmp/healthy + periodSeconds: 3 + failureThreshold: 10 \ No newline at end of file