diff --git a/.gitea/actions/go-cache-key/action.yaml b/.gitea/actions/go-cache-key/action.yaml new file mode 100644 index 0000000..929e530 --- /dev/null +++ b/.gitea/actions/go-cache-key/action.yaml @@ -0,0 +1,17 @@ +name: Go Cache Key +description: Create a cache key for Go dependencies + +outputs: + hash: + description: The cache key for Go dependencies + value: ${{ steps.hash-go.outputs.hash }} + +runs: + using: composite + steps: + - name: Create cache key + shell: bash + id: hash-go + run: | + echo "hash=$(sha256sum go.mod go.sum | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + echo "hash=$hash" >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index a910bf1..f74412e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -8,30 +8,76 @@ env: RUNNER_TOOL_CACHE: /toolcache jobs: - test: - name: test + install-dependencies: + name: install dependencies runs-on: - ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + - name: Install dependencies + uses: .gitea/workflows/install-go-dependencies + + build-check: + name: build check + runs-on: + - ubuntu-latest + needs: install-dependencies + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run build script + uses: .gitea/workflows/run-go-script with: - go-version-file: go.mod - check-latest: true - - name: Create cache key - id: hash-go - run: echo "hash=$(sha256sum go.mod go.sum | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" - - name: cache go - id: cache-go - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + script: build + + check-format: + name: check format + runs-on: + - ubuntu-latest + needs: build-check + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run check format script + uses: .gitea/workflows/run-go-script with: - path: | - /go_path - /go_cache - key: go_path-${{ steps.hash-go.outputs.hash }} - restore-keys: |- - go_cache-${{ steps.hash-go.outputs.hash }} - - name: build - run: make build + script: check-format + + check-lint: + name: check lint + runs-on: + - ubuntu-latest + needs: build-check + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run check lint script + uses: .gitea/workflows/run-go-script + with: + script: check-lint + + test: + name: test + runs-on: + - ubuntu-latest + needs: build-check + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run test script + uses: .gitea/workflows/run-go-script + with: + script: test + + image-check: + name: image check + runs-on: + - ubuntu-latest + - linux_amd64 + needs: build-check + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Build image + run: make build-image diff --git a/.gitea/workflows/install-go-dependencies.yaml b/.gitea/workflows/install-go-dependencies.yaml new file mode 100644 index 0000000..90ddd67 --- /dev/null +++ b/.gitea/workflows/install-go-dependencies.yaml @@ -0,0 +1,28 @@ +name: Install Go Dependencies + +on: + workflow_call: + +jobs: + install-dependencies: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0.4.0 + with: + go-version-file: go.mod + check-latest: true + - name: Create cache key + uses: .gitea/actions/go-cache-key@main + - name: cache go + id: cache-go + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + /go_path + /go_cache + key: go_path-${{ steps.hash-go.outputs.hash }} + restore-keys: |- + go_cache-${{ steps.hash-go.outputs.hash }} diff --git a/.gitea/workflows/run-go-script.yaml b/.gitea/workflows/run-go-script.yaml new file mode 100644 index 0000000..53b7172 --- /dev/null +++ b/.gitea/workflows/run-go-script.yaml @@ -0,0 +1,35 @@ +name: Run Go Script + +on: + workflow_call: + inputs: + script: + description: The script to run + required: true + type: string + +jobs: + run-script: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + check-latest: true + - name: Create cache key + uses: .gitea/actions/go-cache-key@main + - name: Install dependencies from Cache + id: cache-go + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + /go_path + /go_cache + key: go_path-${{ steps.hash-go.outputs.hash }} + restore-keys: |- + go_cache-${{ steps.hash-go.outputs.hash }} + - name: Run script + run: make ${{ inputs.script }} diff --git a/.gitignore b/.gitignore index 5f56073..f72610b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +lcov.info # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/Makefile b/Makefile index 0c34b95..788ed27 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,39 @@ ifneq (,$(wildcard ./.env)) include .env export endif -.PHONY: build run codegen +.PHONY: build run codegen build-image test test-unit test-coverage lint format check-format build: go build -o main +build-image: + docker build -t authentik-kubernetes-operator:latest . + run: make build ./main --kubeconfig=/home/tbehrendt/.kube/config codegen: ./scripts/codegen.sh + +test: test-unit test-coverage + +test-unit: + go test . -coverprofile=coverage.out + +test-coverage: + go tool gcov2lcov -infile coverage.out > lcov.info + +lint: + go vet ./... + +format: + gofmt -w . + +check-format: + @OUTPUT=$$(gofmt -l .); \ + if [ -n "$$OUTPUT" ]; then \ + echo "Formatter failed for:"; \ + echo "$$OUTPUT"; \ + exit 1; \ + fi diff --git a/controller.go b/controller.go index a1dbd81..2330bf3 100644 --- a/controller.go +++ b/controller.go @@ -210,7 +210,7 @@ func (c *Controller) reconcileDelete(ctx context.Context, pp *v1alpha1.ProxyProv r, err := c.authentik.ProvidersApi.ProvidersProxyDestroy(ctx, int32(pk)).Execute() if err != nil { // This handles an edge-case, where when the ProxyProvider on Authentik has already been deleted, but the finalizer is still present. We just remove the finalizer and return. - if r.StatusCode != http.StatusNotFound { + if r != nil && r.StatusCode != http.StatusNotFound { return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyDestroy`: %w with response %v", err, r) } } @@ -226,13 +226,14 @@ func (c *Controller) reconcileUpdate(ctx context.Context, pp *v1alpha1.ProxyProv return fmt.Errorf("error parsing PK: %v", err) } _, r, err := c.authentik.ProvidersApi.ProvidersAllRetrieve(ctx, int32(pk)).Execute() - if err != nil && r.StatusCode != http.StatusNotFound { - + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + // This handles an edge-case, where when the PorxyProvider on Authentik has been deleted, e.g. by mistake. We just remove the PK and return. + // During the next reconciliation, the ProxyProvider will be re-created. + pp.Status.PK = "" + return c.updateProxyProviderStatus(ctx, pp) + } return fmt.Errorf("error retrieving existing ProxyProvider: %v with response %v", err, r) - } else if r.StatusCode == http.StatusNotFound { - // This handles an edge-case, where when the PorxyProvider on Authentik has been deleted, e.g. by mistake. We just remove the PK and return. - // During the next reconciliation, the ProxyProvider will be re-created. - pp.Status.PK = "" } proxyProviderRequest := &authentikapi.PatchedProxyProviderRequest{ diff --git a/controller_test.go b/controller_test.go new file mode 100644 index 0000000..8d3c13b --- /dev/null +++ b/controller_test.go @@ -0,0 +1,385 @@ +// AI generated tests and not yet reviewed. +package main + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strings" + "testing" + + v1alpha1 "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/apis/proxyprovider/v1alpha1" + operatorfake "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/clientset/versioned/fake" + operatorinformers "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/informers/externalversions" + authentikapi "goauthentik.io/api/v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" +) + +func TestController_syncHandler_create(t *testing.T) { + const wantPK = 42 + + server := newAuthentikTestServer(t, authentikTestHandlers{ + proxyCreate: func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK}) + }, + }) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, testProxyProvider(), server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-pp"}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + + got := getProxyProvider(t, ctrl, "default", "test-pp") + if got.Status.PK != "42" { + t.Fatalf("status.pk = %q, want 42", got.Status.PK) + } +} + +func TestController_syncHandler_ensureFinalizers(t *testing.T) { + pp := testProxyProvider() + pp.Status.PK = "42" + + server := newAuthentikTestServer(t, authentikTestHandlers{}) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + + got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) + if !slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { + t.Fatalf("finalizers = %v, want %q", got.Finalizers, DeleteAuthentikProxyProviderFinalizer) + } +} + +func TestController_syncHandler_update(t *testing.T) { + pp := testProxyProvider() + pp.Status.PK = "42" + pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} + + server := newAuthentikTestServer(t, authentikTestHandlers{ + allRetrieve: func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusOK, map[string]any{"pk": 42}) + }, + proxyPartialUpdate: func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusOK, map[string]any{"pk": 42}) + }, + }) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + + got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) + if got.Status.PK != "42" { + t.Fatalf("status.pk = %q, want 42", got.Status.PK) + } +} + +func TestController_syncHandler_update_providerNotFound(t *testing.T) { + pp := testProxyProvider() + pp.Status.PK = "42" + pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} + + server := newAuthentikTestServer(t, authentikTestHandlers{ + allRetrieve: func(w http.ResponseWriter, _ *http.Request) { + http.NotFound(w, nil) + }, + }) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + + got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) + if got.Status.PK != "" { + t.Fatalf("status.pk = %q, want empty after provider not found", got.Status.PK) + } +} + +func TestController_syncHandler_delete(t *testing.T) { + now := metav1.Now() + pp := testProxyProvider() + pp.Status.PK = "42" + pp.DeletionTimestamp = &now + pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} + + var destroyCalled bool + server := newAuthentikTestServer(t, authentikTestHandlers{ + proxyDestroy: func(w http.ResponseWriter, r *http.Request) { + destroyCalled = true + if r.Method != http.MethodDelete { + t.Errorf("destroy method = %s, want DELETE", r.Method) + } + w.WriteHeader(http.StatusNoContent) + }, + }) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + if !destroyCalled { + t.Fatal("expected Authentik destroy call") + } + + got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) + if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { + t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers) + } +} + +func TestController_syncHandler_delete_providerAlreadyGone(t *testing.T) { + now := metav1.Now() + pp := testProxyProvider() + pp.Status.PK = "42" + pp.DeletionTimestamp = &now + pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} + + server := newAuthentikTestServer(t, authentikTestHandlers{ + proxyDestroy: func(w http.ResponseWriter, _ *http.Request) { + http.NotFound(w, nil) + }, + }) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err != nil { + t.Fatalf("syncHandler() error = %v", err) + } + + got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) + if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { + t.Fatalf("finalizers = %v, want finalizer removed after 404", got.Finalizers) + } +} + +func TestController_syncHandler_notFound(t *testing.T) { + server := newAuthentikTestServer(t, authentikTestHandlers{}) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, nil, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "missing"}) + if err != nil { + t.Fatalf("syncHandler() error = %v, want nil for missing object", err) + } +} + +func TestController_syncHandler_invalidPK(t *testing.T) { + pp := testProxyProvider() + pp.Status.PK = "not-a-number" + pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} + + server := newAuthentikTestServer(t, authentikTestHandlers{}) + t.Cleanup(server.Close) + + ctrl, ctx, cancel := newTestController(t, pp, server.URL) + t.Cleanup(cancel) + + err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) + if err == nil { + t.Fatal("syncHandler() error = nil, want parse error") + } + if !strings.Contains(err.Error(), "error parsing PK") { + t.Fatalf("syncHandler() error = %v, want PK parse error", err) + } +} + +func TestController_enqueueProxyProvider(t *testing.T) { + server := newAuthentikTestServer(t, authentikTestHandlers{}) + t.Cleanup(server.Close) + + ctrl, _, cancel := newTestController(t, testProxyProvider(), server.URL) + t.Cleanup(cancel) + + ctrl.enqueueProxyProvider(testProxyProvider()) + + if ctrl.workqueue.Len() != 1 { + t.Fatalf("workqueue length = %d, want 1", ctrl.workqueue.Len()) + } +} + +// --- test helpers --- + +func testProxyProvider() *v1alpha1.ProxyProvider { + return &v1alpha1.ProxyProvider{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "ProxyProvider", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pp", + Namespace: "default", + }, + Spec: v1alpha1.ProxyProviderSpec{ + Name: "my-app", + AuthorizationFlow: "flow-auth", + InvalidationFlow: "flow-invalidate", + ExternalHost: "https://app.example.com", + }, + } +} + +func newTestController(t *testing.T, pp *v1alpha1.ProxyProvider, authentikURL string) (*Controller, context.Context, context.CancelFunc) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + ctrl, _, stop := newTestControllerWithContext(t, ctx, pp, authentikURL) + return ctrl, ctx, func() { + cancel() + stop() + } +} + +func newTestControllerWithContext(t *testing.T, ctx context.Context, pp *v1alpha1.ProxyProvider, authentikURL string) (*Controller, context.Context, func()) { + t.Helper() + + authentikClient := newAuthentikAPIClientForTest(t, authentikURL) + + var objects []runtime.Object + if pp != nil { + objects = append(objects, pp) + } + proxyClient := operatorfake.NewSimpleClientset(objects...) + + informerFactory := operatorinformers.NewSharedInformerFactory(proxyClient, 0) + proxyInformer := informerFactory.Proxyprovider().V1alpha1().ProxyProviders() + + ctrl := NewController(ctx, fake.NewClientset(), proxyClient, authentikClient, proxyInformer) + + informerFactory.Start(ctx.Done()) + for informerType, synced := range informerFactory.WaitForCacheSync(ctx.Done()) { + if !synced { + t.Fatalf("informer %v failed to sync", informerType) + } + } + + return ctrl, ctx, func() {} +} + +func newAuthentikAPIClientForTest(t *testing.T, serverURL string) *authentikapi.APIClient { + t.Helper() + + u, err := url.Parse(serverURL) + if err != nil { + t.Fatalf("parse server URL: %v", err) + } + + cfg := authentikapi.NewConfiguration() + cfg.Scheme = u.Scheme + cfg.Host = u.Host + + return authentikapi.NewAPIClient(cfg) +} + +type authentikTestHandlers struct { + proxyCreate http.HandlerFunc + proxyDestroy http.HandlerFunc + proxyPartialUpdate http.HandlerFunc + allRetrieve http.HandlerFunc +} + +func newAuthentikTestServer(t *testing.T, handlers authentikTestHandlers) *httptest.Server { + t.Helper() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + switch { + case path == "/api/v3/providers/proxy/" && r.Method == http.MethodPost: + if handlers.proxyCreate != nil { + handlers.proxyCreate(w, r) + return + } + http.NotFound(w, r) + + case strings.HasPrefix(path, "/api/v3/providers/proxy/") && strings.HasSuffix(path, "/"): + idPath := strings.TrimPrefix(path, "/api/v3/providers/proxy/") + if idPath == "" { + http.NotFound(w, r) + return + } + switch r.Method { + case http.MethodDelete: + if handlers.proxyDestroy != nil { + handlers.proxyDestroy(w, r) + return + } + http.NotFound(w, r) + case http.MethodPatch: + if handlers.proxyPartialUpdate != nil { + handlers.proxyPartialUpdate(w, r) + return + } + http.NotFound(w, r) + default: + http.Error(w, "unexpected method on proxy instance", http.StatusMethodNotAllowed) + } + + case strings.HasPrefix(path, "/api/v3/providers/all/") && strings.HasSuffix(path, "/"): + if r.Method == http.MethodGet && handlers.allRetrieve != nil { + handlers.allRetrieve(w, r) + return + } + http.NotFound(w, r) + + default: + http.NotFound(w, r) + } + }) + + return httptest.NewServer(handler) +} + +func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(body); err != nil { + t.Fatalf("write JSON response: %v", err) + } +} + +func getProxyProvider(t *testing.T, ctrl *Controller, namespace, name string) *v1alpha1.ProxyProvider { + t.Helper() + + got, err := ctrl.proxyProviderClientset.ProxyproviderV1alpha1().ProxyProviders(namespace).Get( + context.Background(), name, metav1.GetOptions{}, + ) + if err != nil { + t.Fatalf("get ProxyProvider: %v", err) + } + return got +} diff --git a/go.mod b/go.mod index b6e9adc..0f731a7 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jandelgado/gcov2lcov v1.1.1 // 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.3-0.20250322232337-35a7c28c31ee // indirect @@ -60,3 +61,5 @@ require ( ) replace k8s.io/code-generator => ./code-generator + +tool github.com/jandelgado/gcov2lcov diff --git a/go.sum b/go.sum index 8e707e6..c63418c 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jandelgado/gcov2lcov v1.1.1 h1:CHUNoAglvb34DqmMoZchnzDbA3yjpzT8EoUvVqcAY+s= +github.com/jandelgado/gcov2lcov v1.1.1/go.mod h1:tMVUlMVtS1po2SB8UkADWhOT5Y5Q13XOce2AYU69JuI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -71,9 +73,16 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -107,6 +116,7 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.0.0-20260509204538-0dfb117cc6ec h1:xf12Yh3ltN4fnNyP0CyyM0TwNVnZDfLJjV3+bf9fPFY=