This commit is contained in:
Peter 2022-04-13 21:00:32 +02:00
parent 248318571f
commit 11bd001494
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
15 changed files with 634 additions and 37 deletions

60
.github/workflows/go.yml vendored Normal file
View file

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

3
.gitignore vendored
View file

@ -14,4 +14,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.idea/
.idea/
dist/

147
.golangci.yml Normal file
View file

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

45
.goreleaser.yaml Normal file
View file

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

28
Tiltfile Normal file
View file

@ -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'])

7
deployments/Dockerfile Normal file
View file

@ -0,0 +1,7 @@
ARG BASE="gcr.io/distroless/static:nonroot"
FROM $BASE
COPY kreaper /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/kreaper"]

18
go.mod
View file

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

33
go.sum
View file

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

96
main.go
View file

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

View file

@ -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,
},
)
}

View file

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

3
scripts/build.sh Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
CGO_ENABLED=0 go build -trimpath -ldflags "-w -s" -installsuffix cgo -o dist/kreaper main.go

61
testdata/deployment.yaml vendored Normal file
View file

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

12
testdata/kind.yaml vendored Normal file
View file

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

24
testdata/target_pod.yaml vendored Normal file
View file

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