diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 36b5728..e5c517f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,6 +27,15 @@ jobs: with: lfs: true + - name: Install gotestsum + run: go install gotest.tools/gotestsum@latest + + - name: Create out directory + run: mkdir -p ./out + + - name: Run tests + run: gotestsum -- -coverprofile=out/cover.txt -shuffle=on -race -covermode=atomic ./... + - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: diff --git a/.gitignore b/.gitignore index 1e4ad75..2860173 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ .idea/ dist/ +out/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3b4b643 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-beta.5 + hooks: + - id: go-mod-tidy-repo + args: + - -go=1.18 + - id: go-fumpt + args: + - -w + - id: go-imports + args: + - -local=gitlab.com/inetmock/inetmock + - -w + - id: golangci-lint-repo-mod + args: + - --fast + - --fix diff --git a/go.mod b/go.mod index 25c0107..51e68c3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/baez90/kreaper go 1.18 require ( + github.com/maxatome/go-testdeep v1.11.0 go.uber.org/zap v1.21.0 k8s.io/api v0.23.5 k8s.io/apimachinery v0.23.5 diff --git a/go.sum b/go.sum index 1896dd5..b3b0344 100644 --- a/go.sum +++ b/go.sum @@ -194,6 +194,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= +github.com/maxatome/go-testdeep v1.11.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/main.go b/main.go index 5b7d9d7..99b416c 100644 --- a/main.go +++ b/main.go @@ -96,12 +96,18 @@ func prepareFlags() { "Set target namespace in which kreaper will look for pods - env variable: KREAPER_TARGET_NAMESPACE", ) + var kubeconfigFallback string if home := homedir.HomeDir(); home != "" { - flag.StringVar(&kubeconfig, "kubeconfig", lookupEnvOr("KUBECONFIG", filepath.Join(home, ".kube", "config"), identity[string]), "(optional) absolute path to the kubeconfig file") - } else { - flag.StringVar(&kubeconfig, "kubeconfig", lookupEnvOr("KUBECONFIG", "", identity[string]), "absolute path to the kubeconfig file") + kubeconfigFallback = filepath.Join(home, ".kube", "config") } + flag.StringVar( + &kubeconfig, + "kubeconfig", + lookupEnvOr("KUBECONFIG", kubeconfigFallback, identity[string]), + "absolute path to the kubeconfig file", + ) + flag.Parse() } diff --git a/reaper/reaper_test.go b/reaper/reaper_test.go new file mode 100644 index 0000000..35caefe --- /dev/null +++ b/reaper/reaper_test.go @@ -0,0 +1,165 @@ +package reaper_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/maxatome/go-testdeep/td" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/baez90/kreaper/reaper" +) + +func TestKreaper_Kill(t *testing.T) { + const defaultNamespace = "default" + t.Parallel() + type fields struct { + initialState corev1.PodList + lifetime time.Duration + target reaper.Target + } + tests := []struct { + name string + fields fields + modifier func(tb testing.TB, k8sClient client.Client) + wantErr error + wantRemaining td.TestDeep + }{ + { + name: "Empty initial state", + fields: fields{ + lifetime: 10 * time.Second, + target: reaper.Target("app.kubernetes.io/name=ee8dcc4d"), + }, + wantRemaining: td.Empty(), + }, + { + name: "Single pod to delete", + fields: fields{ + target: reaper.Target("app.kubernetes.io/name=ee8dcc4d"), + lifetime: 100 * time.Millisecond, + initialState: corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "demo-asdf234", + Namespace: defaultNamespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "ee8dcc4d", + }, + }, + }, + }, + }, + }, + wantRemaining: td.Empty(), + }, + { + name: "Single pod to delete - delete preemptively", + fields: fields{ + target: reaper.Target("app.kubernetes.io/name=ee8dcc4d"), + lifetime: 100 * time.Millisecond, + initialState: corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "demo-asdf234", + Namespace: defaultNamespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "ee8dcc4d", + }, + }, + }, + }, + }, + }, + modifier: func(tb testing.TB, k8sClient client.Client) { + tb.Helper() + td.CmpNoError(tb, k8sClient.DeleteAllOf(context.Background(), new(corev1.Pod))) + }, + wantRemaining: td.Empty(), + }, + { + name: "Single pod to delete - one should remain", + fields: fields{ + target: reaper.Target("app.kubernetes.io/name=ee8dcc4d"), + lifetime: 100 * time.Millisecond, + initialState: corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "demo-asdf234", + Namespace: defaultNamespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "ee8dcc4d", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "demo-lkjklsdf9234", + Namespace: defaultNamespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "ef903e61", + }, + }, + }, + }, + }, + }, + wantRemaining: td.Len(1), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + k8sClient := fake.NewClientBuilder(). + WithLists(&tt.fields.initialState). + Build() + + k := reaper.Kreaper{ + Client: k8sClient, + Lifetime: tt.fields.lifetime, + Target: tt.fields.target, + TargetNamespace: defaultNamespace, + } + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + errs := make(chan error, 1) + + go func(ctx context.Context, errs chan<- error) { + defer close(errs) + errs <- k.Kill(ctx) + if err := k.Kill(ctx); !errors.Is(err, tt.wantErr) { + t.Errorf("Kill() error = %v, wantErr %v", err, tt.wantErr) + } + }(ctx, errs) + + if tt.modifier != nil { + tt.modifier(t, k8sClient) + } + + for err := range errs { + if !errors.Is(err, tt.wantErr) { + t.Errorf("Kill() error = %v, wantErr %v", err, tt.wantErr) + return + } + } + + var remainingPods corev1.PodList + if err := k8sClient.List(ctx, &remainingPods); err != nil { + t.Fatalf("Failed to list remaining pods err = %v", err) + } + + td.Cmp(t, remainingPods.Items, tt.wantRemaining) + }) + } +}