From 2298fa3ab61f7e45b4d3e8980abc8075befadb86 Mon Sep 17 00:00:00 2001 From: justinsb Date: Fri, 25 Apr 2025 11:57:58 -0400 Subject: [PATCH 1/2] Update golden test framework * Add support for envtest (instead of mock-kubeapiserver) * Create shared code for capturing requests / normalization. * Use a more conventional env var WRITE_GOLDEN_OUTPUT * Support object rewriting --- dev/test | 3 + docs/addon/walkthrough/tests.md | 4 +- .../controllers/guestbook_controller_test.go | 44 ++- .../controllers/tests/patches-stable.in.yaml | 1 + .../controllers/tests/simple-stable.in.yaml | 1 + examples/guestbook-operator/go.mod | 3 +- go.work.sum | 7 +- ktest/go.mod | 32 +- ktest/go.sum | 88 ++++- ktest/httprecorder/http_recorder.go | 2 - ktest/httprecorder/kube_normalize.go | 145 ++++++++ ktest/httprecorder/request_log.go | 78 +++- ktest/testharness/golden.go | 6 +- ktest/testharness/kubeapiserver.go | 228 ++++++++++++ mockkubeapiserver/tests/kubeapiserver_test.go | 3 +- pkg/patterns/addon/patch.go | 3 +- .../declarative/pkg/applier/applylib_test.go | 95 +---- .../declarative/pkg/applier/exec_test.go | 4 + pkg/test/golden/validator.go | 341 ++++++++++++------ .../simpletest/controller_test.go | 1 + 20 files changed, 863 insertions(+), 226 deletions(-) create mode 100644 ktest/httprecorder/kube_normalize.go create mode 100644 ktest/testharness/kubeapiserver.go diff --git a/dev/test b/dev/test index e110cea8..28e8335b 100755 --- a/dev/test +++ b/dev/test @@ -27,6 +27,9 @@ cd "${REPO_ROOT}" set -x +# Download the kubebuilder assets for envtest +export KUBEBUILDER_ASSETS=$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest use -p path) + pushd mockkubeapiserver CGO_ENABLED=0 go test -count=1 -v ./... popd diff --git a/docs/addon/walkthrough/tests.md b/docs/addon/walkthrough/tests.md index d1215d7c..4f42f570 100644 --- a/docs/addon/walkthrough/tests.md +++ b/docs/addon/walkthrough/tests.md @@ -33,7 +33,7 @@ This means that the output is materialized and checked in to the repo; this proves to be very handy for understanding the impact of a change. There's also a helpful "cheat" function, which rewrite the output when you run -the tests locally - set the HACK_AUTOFIX_EXPECTED_OUTPUT env var to a non-empty +the tests locally - set the WRITE_GOLDEN_OUTPUT env var to a non-empty string. This is useful when you have a big set of changes; it's just as easy to review the changes yourself in the diff and there's not a ton of value in typing them out. @@ -87,7 +87,7 @@ the env-var cheat code. ```bash cd pkg/controller/{{operator}}/tests touch tests/simple.out.yaml - HACK_AUTOFIX_EXPECTED_OUTPUT=1 go test ./... + WRITE_GOLDEN_OUTPUT=1 go test ./... ``` 1. Verify the output is reproducible diff --git a/examples/guestbook-operator/controllers/guestbook_controller_test.go b/examples/guestbook-operator/controllers/guestbook_controller_test.go index 7ca1eb15..ab34fe1a 100644 --- a/examples/guestbook-operator/controllers/guestbook_controller_test.go +++ b/examples/guestbook-operator/controllers/guestbook_controller_test.go @@ -17,21 +17,51 @@ limitations under the License. package controllers import ( + "path/filepath" "testing" + "k8s.io/klog/v2/klogr" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + api "sigs.k8s.io/kubebuilder-declarative-pattern/examples/guestbook-operator/api/v1alpha1" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/test/golden" ) func TestGuestbook(t *testing.T) { - v := golden.NewValidator(t, api.SchemeBuilder) - dr := &GuestbookReconciler{ - Client: v.Client(), + log.SetLogger(klogr.New()) + + env := &envtest.Environment{ + CRDInstallOptions: envtest.CRDInstallOptions{ + ErrorIfPathMissing: true, + Paths: []string{ + filepath.Join("..", "config", "crd", "bases"), + }, + }, } - err := dr.setupReconciler(v.Manager()) - if err != nil { - t.Fatalf("creating reconciler: %v", err) + opt := golden.ValidatorOptions{ + EnvtestEnvironment: env, + ManagerOptions: manager.Options{ + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + }, } + opt.WithSchema(api.AddToScheme) + + v := golden.NewValidator(t, opt) - v.Validate(dr.Reconciler) + v.Validate(func(mgr manager.Manager) (*declarative.Reconciler, error) { + gr := &GuestbookReconciler{ + Client: mgr.GetClient(), + } + err := gr.setupReconciler(mgr) + if err != nil { + return nil, err + } + return &gr.Reconciler, nil + }) } diff --git a/examples/guestbook-operator/controllers/tests/patches-stable.in.yaml b/examples/guestbook-operator/controllers/tests/patches-stable.in.yaml index c6813972..1d643cd0 100644 --- a/examples/guestbook-operator/controllers/tests/patches-stable.in.yaml +++ b/examples/guestbook-operator/controllers/tests/patches-stable.in.yaml @@ -2,6 +2,7 @@ apiVersion: addons.example.org/v1alpha1 kind: Guestbook metadata: name: guestbook-sample + namespace: default spec: channel: stable patches: diff --git a/examples/guestbook-operator/controllers/tests/simple-stable.in.yaml b/examples/guestbook-operator/controllers/tests/simple-stable.in.yaml index f4a17035..60ccf10b 100644 --- a/examples/guestbook-operator/controllers/tests/simple-stable.in.yaml +++ b/examples/guestbook-operator/controllers/tests/simple-stable.in.yaml @@ -2,5 +2,6 @@ apiVersion: addons.example.org/v1alpha1 kind: Guestbook metadata: name: guestbook-sample + namespace: default spec: channel: stable \ No newline at end of file diff --git a/examples/guestbook-operator/go.mod b/examples/guestbook-operator/go.mod index e3a3c87d..7021d719 100644 --- a/examples/guestbook-operator/go.mod +++ b/examples/guestbook-operator/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-logr/logr v1.4.2 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 + k8s.io/klog/v2 v2.130.1 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210922163802-cac4a6cf1977 ) @@ -102,13 +103,13 @@ require ( k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/cli-runtime v0.29.1 // indirect k8s.io/component-base v0.32.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/kubectl v0.29.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/cli-utils v0.33.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kubebuilder-declarative-pattern/applylib v0.0.0-20230420203711-4abaa68e1923 // indirect + sigs.k8s.io/kubebuilder-declarative-pattern/ktest v0.0.0-20240909164454-57a043fb3ad5 // indirect sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20230303024857-d1f76c15e05b // indirect sigs.k8s.io/kustomize/api v0.14.0 // indirect sigs.k8s.io/kustomize/kstatus v0.0.2-0.20200509233124-065f70705d4d // indirect diff --git a/go.work.sum b/go.work.sum index 917f8dcd..edc0a07d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -329,6 +329,7 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -368,6 +369,7 @@ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= @@ -391,7 +393,6 @@ github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7y github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= @@ -404,7 +405,6 @@ github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65 github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= @@ -488,6 +488,7 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -565,6 +566,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -646,6 +648,7 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk= sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca h1:6dsH6AYQWbyZmtttJNe8Gq1cXOeS1BdV3eW37zHilAQ= diff --git a/ktest/go.mod b/ktest/go.mod index 91d3e5e6..5f990656 100644 --- a/ktest/go.mod +++ b/ktest/go.mod @@ -7,25 +7,51 @@ toolchain go1.24.1 require ( github.com/google/go-cmp v0.6.0 k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20221021151406-9bd3fb842119 sigs.k8s.io/yaml v1.4.0 ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect diff --git a/ktest/go.sum b/ktest/go.sum index f653b73f..be8b56c8 100644 --- a/ktest/go.sum +++ b/ktest/go.sum @@ -1,41 +1,85 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -48,6 +92,10 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -59,39 +107,67 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20221021151406-9bd3fb842119 h1:PoxuR/rQXKsCp7ZJqv4sxCstjsYdn1L5pzlcoIiai/0= +sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver v0.0.0-20221021151406-9bd3fb842119/go.mod h1:lnUjbSD3YT/lBWOZEIfhJT/uj1Ic+IRWMnT+iwsXM3Y= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/ktest/httprecorder/http_recorder.go b/ktest/httprecorder/http_recorder.go index cc68db7a..e926f1f4 100644 --- a/ktest/httprecorder/http_recorder.go +++ b/ktest/httprecorder/http_recorder.go @@ -54,8 +54,6 @@ func (m *HTTPRecorder) RoundTrip(request *http.Request) (*http.Response, error) switch strings.ToLower(k) { case "authorization": entry.Response.Header[k] = []string{"(redacted)"} - case "date": - entry.Response.Header[k] = []string{"(removed)"} default: entry.Response.Header[k] = values } diff --git a/ktest/httprecorder/kube_normalize.go b/ktest/httprecorder/kube_normalize.go new file mode 100644 index 00000000..97dab3fc --- /dev/null +++ b/ktest/httprecorder/kube_normalize.go @@ -0,0 +1,145 @@ +package httprecorder + +import ( + "net/url" + "sort" + "strconv" + "strings" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/rest" +) + +// placeholderTime is a placeholder time that is used to replace the timestamps, so that we can golden test. +var placeholderTime = metav1.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC) + +// NormalizeKubeRequestLog normalizes a kube request log for golden testing, +// it replaces ephemeral values with placeholder values, +// sorts requests into a predictable order, +// and removes non-deterministic headers. +func NormalizeKubeRequestLog(t *testing.T, requestLog *RequestLog, restConfig *rest.Config) { + requestLog.RewriteEntries(t, func(entry *LogEntry) { + u, err := url.Parse(entry.Request.URL) + if err != nil { + t.Errorf("error parsing url %q: %v", entry.Request.URL, err) + return + } + u.Host = "kube-apiserver" + u.Scheme = "https" + entry.Request.URL = u.String() + }) + + // Replace URLs that include a hash value - this is used by API discovery, but introduces effectively random values into our golden tests + requestLog.RewriteEntries(t, func(entry *LogEntry) { + u, err := url.Parse(entry.Request.URL) + if err != nil { + t.Errorf("error parsing url %q: %v", entry.Request.URL, err) + return + } + q := u.Query() + if q.Get("hash") != "" { + q.Set("hash", "some-hash") + u.RawQuery = q.Encode() + entry.Request.URL = u.String() + } + }) + + // Remove very-long well-known response bodies (e.g. API discovery) + requestLog.RewriteEntries(t, func(entry *LogEntry) { + replaceResponseBody := "" + s := entry.Request.URL + // Remove querystring + s, _, _ = strings.Cut(s, "?") + switch s { + case "https://kube-apiserver/openapi/v3", + "https://kube-apiserver/openapi/v3/api/v1", + "https://kube-apiserver/api/v1", + "https://kube-apiserver/api", + "https://kube-apiserver/apis": + // Remove massive API discovery documents + replaceResponseBody = "// discovery response removed for length" + } + if replaceResponseBody != "" { + entry.Response.Body = replaceResponseBody + } + }) + + requestLog.RemoveUserAgent() + + requestLog.RemoveHeader("X-Kubernetes-Pf-Flowschema-Uid") + requestLog.RemoveHeader("X-Kubernetes-Pf-Prioritylevel-Uid") + requestLog.RemoveHeader("Audit-Id") + requestLog.RemoveHeader("Kubectl-Session") + + requestLog.RemoveHeader("Content-Length") + requestLog.RemoveHeader("Date") + + requestLog.ReplaceHeader("ETag", "(removed)") + requestLog.ReplaceHeader("Expires", "(removed)") + requestLog.ReplaceHeader("Last-Modified", "(removed)") + + requestLog.RewriteBodies(t, func(body map[string]any) { + u := unstructured.Unstructured{Object: body} + uid := u.GetUID() + if uid != "" { + u.SetUID("fake-uid") + } + + replaceIfPresent(t, body, placeholderTime.Format(time.RFC3339), "metadata", "creationTimestamp") + + if managedFields := u.GetManagedFields(); managedFields != nil { + for i := range managedFields { + managedFields[i].Time = &placeholderTime + } + u.SetManagedFields(managedFields) + } + }) + + // Rewrite the resource version to a predictable value + // We don't want to use a fixed value, because we want to verify our behaviour here. + { + resourceVersions := sets.New[int]() + requestLog.RewriteBodies(t, func(body map[string]any) { + u := unstructured.Unstructured{Object: body} + rv := u.GetResourceVersion() + if rv != "" { + // These aren't guaranteed to be ints, but in practice they are, and this is test code. + rvInt, err := strconv.Atoi(rv) + if err != nil { + t.Errorf("error converting resource version %q to int: %v", rv, err) + return + } + resourceVersions.Insert(rvInt) + } + }) + resourceVersionsList := resourceVersions.UnsortedList() + sort.Ints(resourceVersionsList) + rewriteResourceVersion := map[string]string{} + for i, rv := range resourceVersionsList { + rewriteResourceVersion[strconv.Itoa(rv)] = strconv.Itoa(1000 + i) + } + requestLog.RewriteBodies(t, func(body map[string]any) { + u := unstructured.Unstructured{Object: body} + rv := u.GetResourceVersion() + u.SetResourceVersion(rewriteResourceVersion[rv]) + }) + } + + requestLog.SortGETs() +} + +func replaceIfPresent(t *testing.T, body map[string]any, replace string, path ...string) { + u := unstructured.Unstructured{Object: body} + _, found, err := unstructured.NestedFieldNoCopy(u.Object, path...) + if err != nil { + t.Errorf("error getting nested field %v: %v", path, err) + return + } + if found { + unstructured.SetNestedField(u.Object, replace, path...) + } +} diff --git a/ktest/httprecorder/request_log.go b/ktest/httprecorder/request_log.go index ac08b939..5fcb47c6 100644 --- a/ktest/httprecorder/request_log.go +++ b/ktest/httprecorder/request_log.go @@ -70,7 +70,7 @@ func writeBody(w io.StringWriter, body string, pretty bool) { } if pretty { - var obj interface{} + var obj any if err := json.Unmarshal([]byte(body), &obj); err == nil { b, err := json.MarshalIndent(obj, "", " ") if err == nil { @@ -186,8 +186,28 @@ func (l *RequestLog) RemoveHeader(k string) { defer l.mutex.Unlock() for i := range l.entries { - r := &l.entries[i].Request - r.Header.Del(k) + request := &l.entries[i].Request + request.Header.Del(k) + + response := &l.entries[i].Response + response.Header.Del(k) + } +} + +func (l *RequestLog) ReplaceHeader(k string, v string) { + l.mutex.Lock() + defer l.mutex.Unlock() + + for i := range l.entries { + request := &l.entries[i].Request + if len(request.Header.Values(k)) != 0 { + request.Header.Set(k, v) + } + + response := &l.entries[i].Response + if len(response.Header.Values(k)) != 0 { + response.Header.Set(k, v) + } } } @@ -260,3 +280,55 @@ func (l *RequestLog) RegexReplaceURL(t *testing.T, find string, replace string) request.URL = u } } + +// RewriteBodies rewrites the bodies of the requests and responses. +// The function fn is called with the body as a map[string]any. +func (l *RequestLog) RewriteEntries(t *testing.T, fn func(entry *LogEntry)) { + l.mutex.Lock() + defer l.mutex.Unlock() + + for i := range l.entries { + entry := l.entries[i] + fn(entry) + } +} + +// RewriteBodies rewrites the bodies of the requests and responses. +// The function fn is called with the body as a map[string]any. +func (l *RequestLog) RewriteBodies(t *testing.T, fn func(body map[string]any)) { + l.RewriteEntries(t, func(entry *LogEntry) { + entry.Request.Body = rewriteBody(t, entry.Request.Body, fn) + entry.Response.Body = rewriteBody(t, entry.Response.Body, fn) + }) +} + +// rewriteBody rewrites the body of a request or response, assuming it is a JSON object. +func rewriteBody(t *testing.T, body string, fn func(body map[string]any)) string { + if body == "" { + return body + } + + // Ignore values we replaced + if strings.HasPrefix(body, "// ") { + return body + } + + var obj any + if err := json.Unmarshal([]byte(body), &obj); err != nil { + t.Errorf("failed to unmarshal body: %v", err) + return body + } + + m, ok := obj.(map[string]any) + if !ok { + return body + } + fn(m) + + b, err := json.MarshalIndent(obj, "", " ") + if err != nil { + t.Errorf("failed to marshal body: %v", err) + return body + } + return string(b) +} diff --git a/ktest/testharness/golden.go b/ktest/testharness/golden.go index 44ec757b..f74b6aee 100644 --- a/ktest/testharness/golden.go +++ b/ktest/testharness/golden.go @@ -39,8 +39,12 @@ func RunGoldenTests(t *testing.T, basedir string, fn func(h *Harness, dir string } } +func ShouldWriteGoldenOutput() bool { + return os.Getenv("WRITE_GOLDEN_OUTPUT") != "" +} + func (h *Harness) CompareGoldenFile(p string, got string) { - if os.Getenv("WRITE_GOLDEN_OUTPUT") != "" { + if ShouldWriteGoldenOutput() { // Short-circuit when the output is correct b, err := os.ReadFile(p) if err == nil && bytes.Equal(b, []byte(got)) { diff --git a/ktest/testharness/kubeapiserver.go b/ktest/testharness/kubeapiserver.go new file mode 100644 index 00000000..69c4e8d6 --- /dev/null +++ b/ktest/testharness/kubeapiserver.go @@ -0,0 +1,228 @@ +package testharness + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/httprecorder" + "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver" + "sigs.k8s.io/yaml" +) + +type TestKubeAPIServer struct { + t *testing.T + ctx context.Context + restConfig *rest.Config + client client.Client +} + +func NewTestKubeAPIServer(t *testing.T, ctx context.Context, env *envtest.Environment) *TestKubeAPIServer { + s := &TestKubeAPIServer{t: t, ctx: ctx} + + if env != nil { + restConfig, err := env.Start() + if err != nil { + t.Fatalf("failed to start envtest kube-apiserver: %v", err) + } + s.restConfig = restConfig + t.Cleanup(func() { + if err := env.Stop(); err != nil { + t.Errorf("error stopping envtest: %v", err) + } + }) + } else { + k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") + if err != nil { + t.Fatalf("error building mock kube-apiserver: %v", err) + } + + t.Cleanup(func() { + if err := k8s.Stop(); err != nil { + t.Errorf("error stopping mockkubeapiserver: %v", err) + } + }) + + k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, "namespaces", meta.RESTScopeRoot) + + addr, err := k8s.StartServing() + if err != nil { + t.Errorf("error starting mock kube-apiserver: %v", err) + } + klog.Infof("mock kubeapiserver listening on %v", addr) + + clientAddr := addr.String() + clientAddr = strings.ReplaceAll(clientAddr, "[::]", "127.0.0.1") + restConfig := &rest.Config{ + Host: "http://" + clientAddr, + } + s.restConfig = restConfig + } + + client, err := client.New(s.restConfig, client.Options{}) + if err != nil { + t.Fatalf("error creating k8s client: %v", err) + } + s.client = client + + return s +} + +func (s *TestKubeAPIServer) RESTConfig() *rest.Config { + c := *s.restConfig + c.TLSClientConfig = *c.TLSClientConfig.DeepCopy() + return &c +} + +func (s *TestKubeAPIServer) Client() client.Client { + return s.client +} + +// AddProxyAndRecordToLog starts a proxy server that records requests and responses to the log. +// It changes the rest.Config to point to the proxy server. +func (s *TestKubeAPIServer) AddProxyAndRecordToLog(log *httprecorder.RequestLog) { + proxy := &proxy{ + t: s.t, + log: log, + upstream: s.restConfig, + } + s.restConfig = proxy.Start() + + s.t.Cleanup(proxy.Stop) +} + +// AddObject pre-creates an object +func (s *TestKubeAPIServer) AddObject(obj *unstructured.Unstructured) error { + t := s.t + t.Logf("precreating %s object %s/%s", obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()) + + return s.client.Create(s.ctx, obj) +} + +// AddObjectsFromManifest pre-creates the objects in the manifest +func (s *TestKubeAPIServer) AddObjectsFromManifest(y string) error { + for _, obj := range strings.Split(y, "\n---\n") { + u := &unstructured.Unstructured{} + if err := yaml.Unmarshal([]byte(obj), &u.Object); err != nil { + return fmt.Errorf("failed to unmarshal object %q: %w", obj, err) + } + if err := s.AddObject(u); err != nil { + return err + } + } + return nil +} + +// proxy is a simple http server that proxies requests to the upstream kube-apiserver +// and records the request and response into the log. +// It is used by AddProxyAndRecordToLog +type proxy struct { + t *testing.T + log *httprecorder.RequestLog + upstream *rest.Config + + listener net.Listener + httpServer *http.Server + upstreamBaseURL *url.URL + + httpRecorder *httprecorder.HTTPRecorder +} + +// Start starts the proxy server +func (p *proxy) Start() *rest.Config { + t := p.t + // Run a simple http server that proxies requests to the upstream server + httpServer := http.Server{ + Handler: p, + } + + upstreamBaseURL, err := url.Parse(p.upstream.Host) + if err != nil { + t.Fatalf("parsing upstream host: %v", err) + } + p.upstreamBaseURL = upstreamBaseURL + + upstreamHTTPClient, err := rest.HTTPClientFor(p.upstream) + if err != nil { + t.Fatalf("creating upstream HTTP client: %v", err) + } + upstreamTransport := upstreamHTTPClient.Transport + if upstreamTransport == nil { + upstreamTransport = http.DefaultTransport + } + // By reusing httprecorder.NewRecorder, we share the code to record the request and response. + p.httpRecorder = httprecorder.NewRecorder(upstreamTransport, p.log) + + p.httpServer = &httpServer + + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("starting proxy: %v", err) + } + p.listener = listener + go httpServer.Serve(listener) + + return &rest.Config{ + Host: listener.Addr().String(), + } +} + +// ServeHTTP is the method that implements the proxy server; +// we proxy requests to the upstream kube-apiserver and record the request and response into the log. +func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log := log.FromContext(r.Context()) + + upstreamURL := p.upstreamBaseURL.JoinPath(r.URL.Path) + upstreamURL.RawQuery = r.URL.RawQuery + + u, err := url.Parse(upstreamURL.String()) + if err != nil { + p.t.Fatalf("parsing upstream URL: %v", err) + } + + upstreamReq := &http.Request{ + Method: r.Method, + URL: u, + Header: r.Header, + Body: r.Body, + } + + log.Info("proxying kube-apiserver request", "method", upstreamReq.Method, "url", upstreamURL.String()) + + resp, err := p.httpRecorder.RoundTrip(upstreamReq) + if err != nil { + p.t.Fatalf("proxying request: %v", err) + } + defer resp.Body.Close() + + // Forward the response headers + for k, vv := range resp.Header { + w.Header()[k] = vv + } + w.WriteHeader(resp.StatusCode) + + // Forward the response body + if resp.Body != nil { + if _, err := io.Copy(w, resp.Body); err != nil { + p.t.Fatalf("error copying response body: %v", err) + } + } +} + +// Stop stops the proxy server +func (p *proxy) Stop() { + p.listener.Close() +} diff --git a/mockkubeapiserver/tests/kubeapiserver_test.go b/mockkubeapiserver/tests/kubeapiserver_test.go index a49934cc..2c39d7ab 100644 --- a/mockkubeapiserver/tests/kubeapiserver_test.go +++ b/mockkubeapiserver/tests/kubeapiserver_test.go @@ -32,7 +32,7 @@ func TestGoldenTests(t *testing.T) { addr, err := k8s.StartServing() if err != nil { - t.Errorf("error starting mock kube-apiserver: %v", err) + t.Fatalf("error starting mock kube-apiserver: %v", err) } klog.Infof("mock kubeapiserver will listen on %v", addr) @@ -86,6 +86,7 @@ func TestGoldenTests(t *testing.T) { t.Logf("replacing old url prefix %q", "http://"+restConfig.Host) requestLog.ReplaceURLPrefix("http://"+restConfig.Host, "http://kube-apiserver") requestLog.RemoveUserAgent() + requestLog.ReplaceHeader("Date", "(removed)") requestLog.SortGETs() requests := requestLog.FormatHTTP(true) diff --git a/pkg/patterns/addon/patch.go b/pkg/patterns/addon/patch.go index be1cd6d9..05f7ddbe 100644 --- a/pkg/patterns/addon/patch.go +++ b/pkg/patterns/addon/patch.go @@ -58,7 +58,8 @@ func ApplyPatches(ctx context.Context, object declarative.DeclarativeObject, obj patch := &unstructured.Unstructured{} if err := decoder.Decode(patch); err != nil { - return fmt.Errorf("error parsing json into unstructured object: %v", err) + log.Info("invalid patch", "patch", string(p.Raw)) + return fmt.Errorf("error parsing patch json into unstructured object: %v", err) } log.WithValues("patch", patch).V(1).Info("parsed patch") diff --git a/pkg/patterns/declarative/pkg/applier/applylib_test.go b/pkg/patterns/declarative/pkg/applier/applylib_test.go index 07e5863e..7d16f8ee 100644 --- a/pkg/patterns/declarative/pkg/applier/applylib_test.go +++ b/pkg/patterns/declarative/pkg/applier/applylib_test.go @@ -1,30 +1,22 @@ package applier import ( - "bytes" "context" - "io" "net/http" "path/filepath" "testing" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/kubebuilder-declarative-pattern/applylib/applyset" "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/httprecorder" "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/testharness" - "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/restmapper" ) -func fakeParent() runtime.Object { +func fakeParent() *unstructured.Unstructured { parent := &unstructured.Unstructured{} parent.SetKind("ConfigMap") parent.SetAPIVersion("v1") @@ -42,40 +34,23 @@ func TestApplySetApplier(t *testing.T) { func runApplierGoldenTests(t *testing.T, testDir string, interceptHTTPServer bool, applier Applier) { testharness.RunGoldenTests(t, testDir, func(h *testharness.Harness, testdir string) { ctx := context.Background() + t := h.T - k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") - if err != nil { - t.Fatalf("error building mock kube-apiserver: %v", err) - } - - k8s.RegisterType(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, "namespaces", meta.RESTScopeRoot) + env := &envtest.Environment{} + k8s := testharness.NewTestKubeAPIServer(t, ctx, env) - defer func() { - if err := k8s.Stop(); err != nil { - t.Fatalf("error closing mock kube-apiserver: %v", err) - } - }() - - addr, err := k8s.StartServing() - if err != nil { - t.Errorf("error starting mock kube-apiserver: %v", err) + var apiserverRequestLog httprecorder.RequestLog + if interceptHTTPServer { + k8s.AddProxyAndRecordToLog(&apiserverRequestLog) } - klog.Infof("mock kubeapiserver will listen on %v", addr) + restConfig := k8s.RESTConfig() var requestLog httprecorder.RequestLog wrapTransport := func(rt http.RoundTripper) http.RoundTripper { return httprecorder.NewRecorder(rt, &requestLog) } - restConfig := &rest.Config{ - Host: addr.String(), - WrapTransport: wrapTransport, - } - - var apiserverRequestLog httprecorder.RequestLog - if interceptHTTPServer { - k8s.AddHook(&logKubeRequestsHook{log: &apiserverRequestLog}) - } + restConfig.WrapTransport = wrapTransport if h.FileExists(filepath.Join(testdir, "before.yaml")) { before := string(h.MustReadFile(filepath.Join(testdir, "before.yaml"))) @@ -96,67 +71,33 @@ func runApplierGoldenTests(t *testing.T, testDir string, interceptHTTPServer boo } parent := fakeParent() - gvk := parent.GetObjectKind().GroupVersionKind() - restmapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + parentGVK := parent.GroupVersionKind() + parentRESTMapping, err := restMapper.RESTMapping(parentGVK.GroupKind(), parentGVK.Version) if err != nil { - t.Errorf("error getting restmapping for parent %v", err) + t.Fatalf("error getting restmapping for parent %v", err) } + k8s.AddObject(parent) - client := fake.NewClientBuilder().WithRuntimeObjects(parent).Build() + client := k8s.Client() options := ApplierOptions{ Objects: objects.GetItems(), RESTConfig: restConfig, RESTMapper: restMapper, - ParentRef: applyset.NewParentRef(parent, "test", "default", restmapping), + ParentRef: applyset.NewParentRef(parent, parent.GetName(), parent.GetNamespace(), parentRESTMapping), Client: client, } if err := applier.Apply(ctx, options); err != nil { t.Fatalf("error from applier.Apply: %v", err) } - t.Logf("replacing old url prefix %q", "http://"+restConfig.Host) - requestLog.ReplaceURLPrefix("http://"+restConfig.Host, "http://kube-apiserver") - requestLog.RemoveUserAgent() - requestLog.SortGETs() - + httprecorder.NormalizeKubeRequestLog(t, &requestLog, restConfig) requests := requestLog.FormatHTTP(false) h.CompareGoldenFile(filepath.Join(testdir, "expected.yaml"), requests) if interceptHTTPServer { - t.Logf("replacing old url prefix %q", "http://"+restConfig.Host) - apiserverRequestLog.ReplaceURLPrefix("http://"+restConfig.Host, "http://kube-apiserver") - apiserverRequestLog.RemoveUserAgent() - apiserverRequestLog.SortGETs() - apiserverRequestLog.RemoveHeader("Kubectl-Session") + httprecorder.NormalizeKubeRequestLog(t, &apiserverRequestLog, restConfig) apiserverRequests := apiserverRequestLog.FormatHTTP(false) h.CompareGoldenFile(filepath.Join(testdir, "expected-apiserver.yaml"), apiserverRequests) } }) } - -// logKubeRequestsHook is a hook to record mock-kubeapiserver requests to a RequestLog -type logKubeRequestsHook struct { - log *httprecorder.RequestLog -} - -var _ mockkubeapiserver.BeforeHTTPOperation = &logKubeRequestsHook{} - -func (h *logKubeRequestsHook) BeforeHTTPOperation(op *mockkubeapiserver.HTTPOperation) { - req := op.Request - entry := &httprecorder.LogEntry{} - entry.Request = httprecorder.Request{ - Method: req.Method, - URL: req.URL.String(), - Header: req.Header, - } - - if req.Body != nil { - requestBody, err := io.ReadAll(req.Body) - if err != nil { - panic("failed to read request body") - } - entry.Request.Body = string(requestBody) - req.Body = io.NopCloser(bytes.NewReader(requestBody)) - } - h.log.AddEntry(entry) -} diff --git a/pkg/patterns/declarative/pkg/applier/exec_test.go b/pkg/patterns/declarative/pkg/applier/exec_test.go index 7d97748b..9e45f25f 100644 --- a/pkg/patterns/declarative/pkg/applier/exec_test.go +++ b/pkg/patterns/declarative/pkg/applier/exec_test.go @@ -29,6 +29,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "k8s.io/klog/v2/klogr" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest" ) @@ -147,6 +149,8 @@ metadata: } func TestKubectlApplier(t *testing.T) { + log.SetLogger(klogr.New()) + kubectlPath, err := exec.LookPath("kubectl") if err != nil { t.Fatalf("failed to find kubectl on path: %v", err) diff --git a/pkg/test/golden/validator.go b/pkg/test/golden/validator.go index 6881ffa6..a7e1b0d5 100644 --- a/pkg/test/golden/validator.go +++ b/pkg/test/golden/validator.go @@ -24,17 +24,19 @@ import ( "os" "os/exec" "path/filepath" - "strings" + "testing" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/scheme" + "sigs.k8s.io/kubebuilder-declarative-pattern/ktest/testharness" "sigs.k8s.io/kubebuilder-declarative-pattern/mockkubeapiserver" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/loaders" @@ -59,59 +61,139 @@ type T interface { Helper() Logf(format string, args ...interface{}) TempDir() string + + Run(name string, testFunc func(t *testing.T)) bool +} + +type AddToSchemeFunc func(s *runtime.Scheme) error + +type ValidatorOptions struct { + EnvtestEnvironment *envtest.Environment + AddToSchemeFunctions []AddToSchemeFunc + ManagerOptions manager.Options + + // RewriteObjects allows us to replace values in objects + RewriteObjects func(o *unstructured.Unstructured) +} + +func (opt *ValidatorOptions) WithSchema(addToSchemeFuncs ...AddToSchemeFunc) *ValidatorOptions { + opt.AddToSchemeFunctions = append(opt.AddToSchemeFunctions, addToSchemeFuncs...) + return opt } -func NewValidator(t T, b *scheme.Builder) *validator { - v := &validator{T: t, scheme: runtime.NewScheme()} - if err := b.AddToScheme(v.scheme); err != nil { - t.Fatalf("error from AddToScheme: %v", err) +func NewValidator(t T, opt ValidatorOptions) *validator { + v := &validator{T: t, scheme: runtime.NewScheme(), options: opt} + for _, addToSchemeFunc := range opt.AddToSchemeFunctions { + if err := addToSchemeFunc(v.scheme); err != nil { + t.Fatalf("error from AddToScheme: %v", err) + } } v.T.Helper() addon.Init() v.findChannelsPath() - k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") - if err != nil { - t.Fatalf("error building mock kube-apiserver: %v", err) - } + return v +} - addr, err := k8s.StartServing() - if err != nil { - t.Errorf("error starting mock kube-apiserver: %v", err) +type validator struct { + T T + scheme *runtime.Scheme + TestDir string + + options ValidatorOptions +} + +type kubeInstance struct { + restConfig *rest.Config + mgr manager.Manager + client client.Client +} + +func deepCopyEnvtestEnvironment(in *envtest.Environment) *envtest.Environment { + out := &envtest.Environment{} + *out = *in + + out.CRDInstallOptions.CRDs = nil + for _, crd := range in.CRDInstallOptions.CRDs { + out.CRDInstallOptions.CRDs = append(out.CRDInstallOptions.CRDs, crd.DeepCopy()) } - v.k8s = k8s - t.Cleanup(func() { - if err := k8s.Stop(); err != nil { - t.Errorf("error stopping mock kube-apiserver: %v", err) + return out +} + +func (v *kubeInstance) startKube(t T, opt ValidatorOptions) { + useEnvtest := opt.EnvtestEnvironment != nil + if useEnvtest { + env := deepCopyEnvtestEnvironment(opt.EnvtestEnvironment) + rc, err := env.Start() + if err != nil { + t.Fatalf("failed to start envtest kube-apiserver: %v", err) } - }) + v.restConfig = rc + t.Cleanup(func() { + if err := env.Stop(); err != nil { + t.Errorf("error stopping envtest: %v", err) + } + }) + + } else { + k8s, err := mockkubeapiserver.NewMockKubeAPIServer(":0") + if err != nil { + t.Fatalf("error building mock kube-apiserver: %v", err) + } + + addr, err := k8s.StartServing() + if err != nil { + t.Errorf("error starting mock kube-apiserver: %v", err) + } + // v.k8s = k8s + + t.Cleanup(func() { + if err := k8s.Stop(); err != nil { + t.Errorf("error stopping mock kube-apiserver: %v", err) + } + }) - restConfig := &rest.Config{ - Host: addr.String(), + v.restConfig = &rest.Config{ + Host: addr.String(), + } } +} - mgr, err := manager.New(restConfig, manager.Options{ - Scheme: v.scheme, - }) +func (v *kubeInstance) startControllerRuntime(t T, scheme *runtime.Scheme, opt ValidatorOptions) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + mgrOptions := opt.ManagerOptions + mgrOptions.Scheme = scheme + mgr, err := manager.New(v.restConfig, mgrOptions) if err != nil { t.Fatalf("error building manager: %v", err) } v.client = mgr.GetClient() v.mgr = mgr - return v -} -type validator struct { - T T - scheme *runtime.Scheme - TestDir string + managerError := make(chan error) + go func() { + err := v.mgr.Start(ctx) + if err != nil { + t.Errorf("starting manager: %v", err) + } + managerError <- err + }() - k8s *mockkubeapiserver.MockKubeAPIServer + // Wait for the manager to start + if !v.mgr.GetCache().WaitForCacheSync(ctx) { + t.Fatalf("error waiting for cache sync") + } - mgr manager.Manager - client client.Client + t.Cleanup(func() { + // Cancel the context so the manager exits + cancel() + // Wait for manager to exit + <-managerError + }) } // findChannelsPath will search for a channels directory, which is helpful when running under bazel @@ -178,15 +260,9 @@ func (v *validator) findChannelsPath() { t.Logf("flagChannel = %s", loaders.FlagChannel) } -func (v *validator) Manager() manager.Manager { - return v.mgr -} - -func (v *validator) Client() client.Client { - return v.client -} +func (v *validator) Validate(reconcilerFactory func(mgr manager.Manager) (*declarative.Reconciler, error)) { + ctx := context.TODO() -func (v *validator) Validate(r declarative.Reconciler) { t := v.T t.Helper() @@ -195,124 +271,130 @@ func (v *validator) Validate(r declarative.Reconciler) { metadataAccessor := meta.NewAccessor() - basedir := "tests" + basedir := "testdata/golden" if v.TestDir != "" { basedir = v.TestDir } - files, err := os.ReadDir(basedir) - if err != nil { - t.Fatalf("error reading dir %s: %v", basedir, err) - } - - ctx := context.TODO() - - for _, f := range files { - p := filepath.Join(basedir, f.Name()) - t.Logf("Filepath: %s", p) - if f.IsDir() { - // TODO: support fs trees? - t.Errorf("unexpected directory in tests directory: %s", p) - continue + var testNames []string + if err := filepath.WalkDir(basedir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err } - - if strings.HasSuffix(p, "~") { - // Ignore editor temp files (for sanity) - t.Logf("ignoring editor temp file %s", p) - continue + if d.IsDir() { + return nil } - - // Ignore any files that are not input - we want to find .side_in and .out files - // that correspond to given .in file - if !strings.HasSuffix(p, ".in.yaml") { - continue + if d.Name() == "input.yaml" { + testName, err := filepath.Rel(basedir, filepath.Dir(path)) + if err != nil { + t.Fatalf("getting relative path for %q: %v", path, err) + } + testNames = append(testNames, testName) } + return nil + }); err != nil { + t.Fatalf("error listing tests in %q: %v", basedir, err) + } + + runTest := func(testDir string, t T) { + kube := &kubeInstance{} + kube.startKube(t, v.options) + kube.startControllerRuntime(t, v.scheme, v.options) objectsToCleanup := []client.Object{} // Check if there is a file containing side inputs for this test - sideInputPath := strings.Replace(p, ".in.yaml", ".side_in.yaml", -1) - sideInputRead, err := os.ReadFile(sideInputPath) - if err != nil { - if !os.IsNotExist(err) { - t.Errorf("Could not read side input file %s: %v", sideInputPath, err) - } - } else { - objs, err := manifest.ParseObjects(ctx, string(sideInputRead)) + for _, name := range []string{"side_in.yaml", "dependencies.yaml"} { + sideInputPath := filepath.Join(testDir, name) + sideInputRead, err := os.ReadFile(sideInputPath) if err != nil { - t.Errorf("error parsing file %s: %v", p, err) - continue - } - - for _, obj := range objs.Items { - json, err := obj.JSON() - if err != nil { - t.Errorf("error converting resource to json in %s: %v", p, err) - continue + if !os.IsNotExist(err) { + t.Errorf("Could not read side input file %s: %v", sideInputPath, err) } - decoded, _, err := serializer.Decode(json, nil, nil) + } else { + objs, err := manifest.ParseObjects(ctx, string(sideInputRead)) if err != nil { - t.Errorf("error parsing resource in %s: %v", p, err) - continue + t.Fatalf("error parsing file %s: %v", sideInputPath, err) } - obj := decoded.(client.Object) - if err := v.client.Create(ctx, obj); err != nil { - t.Errorf("error creating resource in %s: %v", p, err) + + for _, obj := range objs.Items { + json, err := obj.JSON() + if err != nil { + t.Errorf("error converting resource to json in %s: %v", sideInputPath, err) + continue + } + decoded, _, err := serializer.Decode(json, nil, nil) + if err != nil { + t.Errorf("error parsing resource in %s: %v", sideInputPath, err) + continue + } + obj := decoded.(client.Object) + if err := kube.client.Create(ctx, obj); err != nil { + t.Errorf("error creating resource in %s: %v", sideInputPath, err) + } + t.Logf("created object %v %v/%v", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()) + objectsToCleanup = append(objectsToCleanup, obj) } - objectsToCleanup = append(objectsToCleanup, obj) } } - b, err := os.ReadFile(p) + inputPath := filepath.Join(testDir, "input.yaml") + b, err := os.ReadFile(inputPath) if err != nil { - t.Errorf("error reading file %s: %v", p, err) - continue + t.Fatalf("error reading file %s: %v", inputPath, err) } objs, err := manifest.ParseObjects(ctx, string(b)) if err != nil { - t.Errorf("error parsing file %s: %v", p, err) - continue + t.Fatalf("error parsing file %s: %v", inputPath, err) } if len(objs.Items) != 1 { - t.Errorf("expected exactly one item in %s", p) - continue + t.Fatalf("expected exactly one item in %s", inputPath) } crJSON, err := objs.Items[0].JSON() if err != nil { - t.Errorf("error converting CR to json in %s: %v", p, err) - continue + t.Fatalf("error converting CR to json in %s: %v", inputPath, err) } cr, _, err := serializer.Decode(crJSON, nil, nil) if err != nil { - t.Errorf("error parsing CR in %s: %v", p, err) - continue + t.Fatalf("error parsing CR in %s: %v", inputPath, err) + } + + { + obj := cr.(client.Object) + if err := kube.client.Create(ctx, obj); err != nil { + t.Errorf("error creating resource in %s: %v", inputPath, err) + } + t.Logf("created object %v %v/%v", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName()) + objectsToCleanup = append(objectsToCleanup, obj) } namespace, err := metadataAccessor.Namespace(cr) if err != nil { - t.Errorf("error getting namespace in %s: %v", p, err) - continue + t.Fatalf("error getting namespace in %s: %v", inputPath, err) } name, err := metadataAccessor.Name(cr) if err != nil { - t.Errorf("error getting name in %s: %v", p, err) - continue + t.Fatalf("error getting name in %s: %v", inputPath, err) } nsn := types.NamespacedName{Namespace: namespace, Name: name} + r, err := reconcilerFactory(kube.mgr) + if err != nil { + t.Fatalf("building reconcile: %v", err) + } + var fs filesys.FileSystem if r.IsKustomizeOptionUsed() { fs = filesys.MakeFsInMemory() } objects, err := r.BuildDeploymentObjectsWithFs(ctx, nsn, cr.(declarative.DeclarativeObject), fs) if err != nil { - t.Errorf("error building deployment objects: %v", err) - continue + t.Fatalf("error building deployment objects: %v", err) } var actualYAML string @@ -324,6 +406,9 @@ func (v *validator) Validate(r declarative.Reconciler) { b.WriteString("---\n") } u := o.UnstructuredObject() + if v.options.RewriteObjects != nil { + v.options.RewriteObjects(u) + } if err := yamlizer.Encode(u, &b); err != nil { t.Fatalf("error encoding to yaml: %v", err) } @@ -331,41 +416,57 @@ func (v *validator) Validate(r declarative.Reconciler) { actualYAML = b.String() } - expectedPath := strings.Replace(p, ".in.yaml", ".out.yaml", -1) + expectedPath := filepath.Join(testDir, "_expected.yaml") var expectedYAML string { b, err := os.ReadFile(expectedPath) if err != nil { - t.Errorf("error reading file %s: %v", expectedPath, err) - continue + if os.IsNotExist(err) && ShouldWriteGoldenOutput(t) { + // We'll create the file below + } else { + t.Fatalf("error reading file %s: %v", expectedPath, err) + } } expectedYAML = string(b) } if actualYAML != expectedYAML { - if os.Getenv("HACK_AUTOFIX_EXPECTED_OUTPUT") != "" { - t.Logf("HACK_AUTOFIX_EXPECTED_OUTPUT is set; replacing expected output in %s", expectedPath) + if ShouldWriteGoldenOutput(t) { + t.Logf("WRITE_GOLDEN_OUTPUT is set; replacing expected output in %s", expectedPath) if err := os.WriteFile(expectedPath, []byte(actualYAML), 0644); err != nil { t.Fatalf("error writing expected output to %s: %v", expectedPath, err) } - continue - } + } else { + if err := diffFiles(t, expectedPath, actualYAML); err != nil { + t.Logf("failed to run system diff, falling back to string diff: %v", err) + t.Logf("diff: %s", diff.StringDiff(actualYAML, expectedYAML)) + } - if err := diffFiles(t, expectedPath, actualYAML); err != nil { - t.Logf("failed to run system diff, falling back to string diff: %v", err) - t.Logf("diff: %s", diff.StringDiff(actualYAML, expectedYAML)) + t.Errorf("unexpected diff between actual and expected YAML. See previous output for details.") + t.Logf(`To regenerate the output based on this result, rerun this test with WRITE_GOLDEN_OUTPUT="true"`) } - - t.Errorf("unexpected diff between actual and expected YAML. See previous output for details.") - t.Logf(`To regenerate the output based on this result, rerun this test with HACK_AUTOFIX_EXPECTED_OUTPUT="true"`) } for _, objectToCleanup := range objectsToCleanup { - if err := v.client.Delete(ctx, objectToCleanup); err != nil { + if err := kube.client.Delete(ctx, objectToCleanup); err != nil { t.Errorf("error deleting object: %v", err) } } } + + for _, testName := range testNames { + t.Run(testName, func(t *testing.T) { + runTest(filepath.Join(basedir, testName), t) + }) + } +} + +func ShouldWriteGoldenOutput(t T) bool { + if os.Getenv("HACK_AUTOFIX_EXPECTED_OUTPUT") != "" { + t.Logf("HACK_AUTOFIX_EXPECTED_OUTPUT is set, please switch to use WRITE_GOLDEN_OUTPUT. This may be an test failure in future versions.") + return true + } + return testharness.ShouldWriteGoldenOutput() } func diffFiles(t T, expectedPath, actual string) error { diff --git a/pkg/test/testreconciler/simpletest/controller_test.go b/pkg/test/testreconciler/simpletest/controller_test.go index a343266a..54dd9346 100644 --- a/pkg/test/testreconciler/simpletest/controller_test.go +++ b/pkg/test/testreconciler/simpletest/controller_test.go @@ -129,6 +129,7 @@ func testSimpleReconciler(h *testharness.Harness, testdir string, applier applie requestLog.RegexReplaceURL(h.T, "&timeoutSeconds=.*&", "&timeoutSeconds=&") h.Logf("replacing real timestamp in request and response to a fake value") requestLog.ReplaceTimestamp() + requestLog.ReplaceHeader("Date", "(removed)") requests := requestLog.FormatHTTP(false) From 7c8ce694b0219d05bfb35083a7cdb40e83863ade Mon Sep 17 00:00:00 2001 From: justinsb Date: Tue, 29 Apr 2025 17:17:49 -0400 Subject: [PATCH 2/2] autogen: update golden test output --- .../golden/patches/_expected.yaml} | 0 .../golden/patches/input.yaml} | 0 .../golden/simple/_expected.yaml} | 0 .../golden/simple/input.yaml} | 0 .../testdata/applylib/simple1/expected.yaml | 118 ++++- .../testdata/applylib/simple2/expected.yaml | 144 +++++- .../testdata/applylib/simple3/expected.yaml | 158 +++++- .../testdata/direct/simple1/expected.yaml | 184 +++++-- .../testdata/direct/simple2/expected.yaml | 230 ++++++++- .../testdata/direct/simple3/expected.yaml | 106 +++- .../kubectl/simple1/expected-apiserver.yaml | 387 +++++++------- .../testdata/kubectl/simple1/expected.yaml | 6 +- .../kubectl/simple2/expected-apiserver.yaml | 472 ++++++++++-------- .../testdata/kubectl/simple2/expected.yaml | 6 +- .../kubectl/simple3/expected-apiserver.yaml | 364 ++++++-------- .../testdata/kubectl/simple3/expected.yaml | 6 +- 16 files changed, 1423 insertions(+), 758 deletions(-) rename examples/guestbook-operator/controllers/{tests/patches-stable.out.yaml => testdata/golden/patches/_expected.yaml} (100%) rename examples/guestbook-operator/controllers/{tests/patches-stable.in.yaml => testdata/golden/patches/input.yaml} (100%) rename examples/guestbook-operator/controllers/{tests/simple-stable.out.yaml => testdata/golden/simple/_expected.yaml} (100%) rename examples/guestbook-operator/controllers/{tests/simple-stable.in.yaml => testdata/golden/simple/input.yaml} (100%) diff --git a/examples/guestbook-operator/controllers/tests/patches-stable.out.yaml b/examples/guestbook-operator/controllers/testdata/golden/patches/_expected.yaml similarity index 100% rename from examples/guestbook-operator/controllers/tests/patches-stable.out.yaml rename to examples/guestbook-operator/controllers/testdata/golden/patches/_expected.yaml diff --git a/examples/guestbook-operator/controllers/tests/patches-stable.in.yaml b/examples/guestbook-operator/controllers/testdata/golden/patches/input.yaml similarity index 100% rename from examples/guestbook-operator/controllers/tests/patches-stable.in.yaml rename to examples/guestbook-operator/controllers/testdata/golden/patches/input.yaml diff --git a/examples/guestbook-operator/controllers/tests/simple-stable.out.yaml b/examples/guestbook-operator/controllers/testdata/golden/simple/_expected.yaml similarity index 100% rename from examples/guestbook-operator/controllers/tests/simple-stable.out.yaml rename to examples/guestbook-operator/controllers/testdata/golden/simple/_expected.yaml diff --git a/examples/guestbook-operator/controllers/tests/simple-stable.in.yaml b/examples/guestbook-operator/controllers/testdata/golden/simple/input.yaml similarity index 100% rename from examples/guestbook-operator/controllers/tests/simple-stable.in.yaml rename to examples/guestbook-operator/controllers/testdata/golden/simple/input.yaml diff --git a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple1/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple1/expected.yaml index dd23dca0..40a0ddcc 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple1/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple1/expected.yaml @@ -1,43 +1,129 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -PATCH http://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns1"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns1" + } +} -200 OK +201 Created Cache-Control: no-cache, private -Content-Length: 377 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns1"},"name":"ns1","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH http://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns2"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns2" + } +} -200 OK +201 Created Cache-Control: no-cache, private -Content-Length: 377 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns2"},"name":"ns2","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/expected.yaml index ad4fe601..9a9dbad0 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple2/expected.yaml @@ -1,43 +1,159 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -PATCH http://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns1"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns1" + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 601 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns1"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:apiVersion":{},"f:kind":{},"f:metadata":{"f:labels":{"f:applyset.kubernetes.io/part-of":{}},"f:name":{}}},"manager":"kdp-test","operation":"Apply"}],"name":"ns1","resourceVersion":"3","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH http://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns2"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns2" + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 601 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns2"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:apiVersion":{},"f:kind":{},"f:metadata":{"f:labels":{"f:applyset.kubernetes.io/part-of":{}},"f:name":{}}},"manager":"kdp-test","operation":"Apply"}],"name":"ns2","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/expected.yaml index f6710898..a4a94812 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/applylib/simple3/expected.yaml @@ -1,43 +1,173 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -PATCH http://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns1"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns1" + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 770 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns1"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:apiVersion":{},"f:kind":{},"f:metadata":{"f:labels":{"f:applyset.kubernetes.io/part-of":{}},"f:name":{}}},"manager":"kdp-test","operation":"Apply"}],"name":"ns1","resourceVersion":"3","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH http://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false +PATCH https://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kdp-test&force=false Accept: application/json Content-Type: application/apply-patch+yaml -{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1"},"name":"ns2"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1" + }, + "name": "ns2" + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 770 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"applyset.kubernetes.io/part-of":"applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1","kubernetes.io/metadata.name":"ns2"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:apiVersion":{},"f:kind":{},"f:metadata":{"f:labels":{"f:applyset.kubernetes.io/part-of":{}},"f:name":{}}},"manager":"kdp-test","operation":"Apply"}],"name":"ns2","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "applyset.kubernetes.io/part-of": "applyset-XYWvxXDUlCqMdjmmY1arThcdGiF0cvBW6sAfSMWYUdE-v1", + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + "f:applyset.kubernetes.io/part-of": {} + } + } + }, + "manager": "kdp-test", + "operation": "Apply", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple1/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple1/expected.yaml index b644ff4c..e3495ce4 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple1/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple1/expected.yaml @@ -1,75 +1,191 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -GET http://kube-apiserver/api/v1/namespaces/ns1 +GET https://kube-apiserver/api/v1/namespaces/ns1 Accept: application/json 404 Not Found -Content-Length: 10 -Content-Type: text/plain; charset=utf-8 -Date: (removed) -X-Content-Type-Options: nosniff - -Not Found +Cache-Control: no-cache, private +Content-Type: application/json +{ + "apiVersion": "v1", + "code": 404, + "details": { + "kind": "namespaces", + "name": "ns1" + }, + "kind": "Status", + "message": "namespaces \"ns1\" not found", + "metadata": {}, + "reason": "NotFound", + "status": "Failure" +} --- -POST http://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Strict +POST https://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Strict Accept: application/json Content-Type: application/json -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"name":"ns1"}} - - -200 OK +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "name": "ns1" + } +} + +201 Created Cache-Control: no-cache, private -Content-Length: 455 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"ns1"},"name":"ns1","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -GET http://kube-apiserver/api/v1/namespaces/ns2 +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json 404 Not Found -Content-Length: 10 -Content-Type: text/plain; charset=utf-8 -Date: (removed) -X-Content-Type-Options: nosniff - -Not Found +Cache-Control: no-cache, private +Content-Type: application/json +{ + "apiVersion": "v1", + "code": 404, + "details": { + "kind": "namespaces", + "name": "ns2" + }, + "kind": "Status", + "message": "namespaces \"ns2\" not found", + "metadata": {}, + "reason": "NotFound", + "status": "Failure" +} --- -POST http://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Strict +POST https://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Strict Accept: application/json Content-Type: application/json -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"name":"ns2"}} - - -200 OK +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "name": "ns2" + } +} + +201 Created Cache-Control: no-cache, private -Content-Length: 455 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"kubernetes.io/metadata.name":"ns2"},"name":"ns2","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/expected.yaml index df8c9ea3..d4f9dcda 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple2/expected.yaml @@ -1,71 +1,257 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -GET http://kube-apiserver/api/v1/namespaces/ns1 +GET https://kube-apiserver/api/v1/namespaces/ns1 Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 286 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"ns1"},"name":"ns1","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH http://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kubectl-client-side-apply&fieldValidation=Strict +PATCH https://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kubectl-client-side-apply&fieldValidation=Strict Accept: application/json Content-Type: application/strategic-merge-patch+json -{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"}}} +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + } + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 677 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"ns1"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{"f:kubectl.kubernetes.io/last-applied-configuration":{}}}},"manager":"kubectl-client-side-apply","operation":"Apply"}],"name":"ns1","resourceVersion":"3","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1002", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -GET http://kube-apiserver/api/v1/namespaces/ns2 +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 286 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"kubernetes.io/metadata.name":"ns2"},"name":"ns2","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH http://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kubectl-client-side-apply&fieldValidation=Strict +PATCH https://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kubectl-client-side-apply&fieldValidation=Strict Accept: application/json Content-Type: application/strategic-merge-patch+json -{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"}}} +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + } + } +} 200 OK Cache-Control: no-cache, private -Content-Length: 677 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"kubernetes.io/metadata.name":"ns2"},"managedFields":[{"apiVersion":"v1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{"f:kubectl.kubernetes.io/last-applied-configuration":{}}}},"manager":"kubectl-client-side-apply","operation":"Apply"}],"name":"ns2","resourceVersion":"4","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1003", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/expected.yaml index cf04ac6c..c2be6d84 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/direct/simple3/expected.yaml @@ -1,39 +1,121 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length --- -GET http://kube-apiserver/api/v1/namespaces/ns1 +GET https://kube-apiserver/api/v1/namespaces/ns1 Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 455 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"creationTimestamp":"2022-01-01T00:00:00Z","labels":{"kubernetes.io/metadata.name":"ns1"},"name":"ns1","resourceVersion":"1","uid":"00000000-0000-0000-0000-000000000001"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -GET http://kube-apiserver/api/v1/namespaces/ns2 +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json 200 OK Cache-Control: no-cache, private -Content-Length: 455 Content-Type: application/json -Date: (removed) -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"creationTimestamp":"2022-01-01T00:00:01Z","labels":{"kubernetes.io/metadata.name":"ns2"},"name":"ns2","resourceVersion":"2","uid":"00000000-0000-0000-0000-000000000002"},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected-apiserver.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected-apiserver.yaml index d8d69daf..1b2f5d9b 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected-apiserver.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected-apiserver.yaml @@ -1,265 +1,232 @@ -GET /api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* Accept-Encoding: gzip +200 OK +Cache-Control: no-cache, private +Content-Type: application/json ---- - -GET /api?timeout=32s -Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /api/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis?timeout=32s +GET https://kube-apiserver/api?timeout=32s Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/admissionregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiextensions.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apps/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis/autoscaling/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/batch/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/certificates.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/coordination.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/discovery.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/events.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/flowcontrol.apiserver.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/flowcontrol.apiserver.k8s.io/v1beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/networking.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/node.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/policy/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/rbac.authorization.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/scheduling.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/storage.k8s.io/v1?timeout=32s -Accept: application/json, */* +GET https://kube-apiserver/apis?timeout=32s +Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/storage.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /api/v1/namespaces/ns1 +GET https://kube-apiserver/api/v1/namespaces/ns1 Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +404 Not Found +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "code": 404, + "details": { + "kind": "namespaces", + "name": "ns1" + }, + "kind": "Status", + "message": "namespaces \"ns1\" not found", + "metadata": {}, + "reason": "NotFound", + "status": "Failure" +} --- -POST /api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore +POST https://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore Accept: application/json Accept-Encoding: gzip -Content-Length: 234 Content-Type: application/json Kubectl-Command: kubectl apply -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"},"name":"ns1"}} - - +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "name": "ns1" + } +} ---- +201 Created +Cache-Control: no-cache, private +Content-Type: application/json -GET /api/v1/namespaces/ns2 +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} + +--- + +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +404 Not Found +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "code": 404, + "details": { + "kind": "namespaces", + "name": "ns2" + }, + "kind": "Status", + "message": "namespaces \"ns2\" not found", + "metadata": {}, + "reason": "NotFound", + "status": "Failure" +} --- -POST /api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore +POST https://kube-apiserver/api/v1/namespaces?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore Accept: application/json Accept-Encoding: gzip -Content-Length: 234 Content-Type: application/json Kubectl-Command: kubectl apply -{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"},"name":"ns2"}} +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "name": "ns2" + } +} +201 Created +Cache-Control: no-cache, private +Content-Type: application/json +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected.yaml index a3a008f5..5eea50a4 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple1/expected.yaml @@ -1,11 +1,9 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected-apiserver.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected-apiserver.yaml index 87875f18..8cf603eb 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected-apiserver.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected-apiserver.yaml @@ -1,281 +1,333 @@ -GET /api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* Accept-Encoding: gzip +200 OK +Cache-Control: no-cache, private +Content-Type: application/json ---- - -GET /api?timeout=32s -Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /api/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis?timeout=32s +GET https://kube-apiserver/api?timeout=32s Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/admissionregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiextensions.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apps/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/batch/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/certificates.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/coordination.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/discovery.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/events.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/flowcontrol.apiserver.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/flowcontrol.apiserver.k8s.io/v1beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/networking.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/node.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis/policy/v1?timeout=32s -Accept: application/json, */* +GET https://kube-apiserver/apis?timeout=32s +Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/rbac.authorization.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis/scheduling.k8s.io/v1?timeout=32s -Accept: application/json, */* +GET https://kube-apiserver/api/v1/namespaces/ns1 +Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -GET /apis/storage.k8s.io/v1?timeout=32s +GET https://kube-apiserver/openapi/v3?timeout=32s Accept: application/json, */* Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Accept-Ranges: bytes +Cache-Control: no-cache, private +Content-Type: text/plain; charset=utf-8 +Last-Modified: (removed) ---- - -GET /apis/storage.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /api/v1/namespaces/ns1 +GET https://kube-apiserver/openapi/v3/api/v1?hash=some-hash&timeout=32s Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Accept-Ranges: bytes +Cache-Control: public, immutable +Content-Type: application/json +Etag: (removed) +Expires: (removed) +Last-Modified: (removed) +Vary: Accept ---- - -GET /openapi/v3?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /openapi/v2?timeout=32s -Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -PATCH /api/v1/namespaces/ns1?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore +PATCH https://kube-apiserver/api/v1/namespaces/ns1?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore Accept: application/json Accept-Encoding: gzip -Content-Length: 183 Content-Type: application/strategic-merge-patch+json Kubectl-Command: kubectl apply -{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n"}}} - +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + } + } +} + +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1002", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -GET /api/v1/namespaces/ns2 +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} --- -PATCH /api/v1/namespaces/ns2?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore +PATCH https://kube-apiserver/api/v1/namespaces/ns2?fieldManager=kubectl-client-side-apply&fieldValidation=Ignore Accept: application/json Accept-Encoding: gzip -Content-Length: 183 Content-Type: application/strategic-merge-patch+json Kubectl-Command: kubectl apply -{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n"}}} - +{ + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + } + } +} + +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + } + } + }, + "manager": "kubectl-client-side-apply", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1003", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected.yaml index a3a008f5..5eea50a4 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple2/expected.yaml @@ -1,11 +1,9 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected-apiserver.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected-apiserver.yaml index 93373c40..6cd338e1 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected-apiserver.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected-apiserver.yaml @@ -1,257 +1,193 @@ -GET /api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* Accept-Encoding: gzip +200 OK +Cache-Control: no-cache, private +Content-Type: application/json ---- - -GET /api?timeout=32s -Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /api/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis?timeout=32s +GET https://kube-apiserver/api?timeout=32s Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/admissionregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiextensions.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apiregistration.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/apps/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/autoscaling/v2beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/batch/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/certificates.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/coordination.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/discovery.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/events.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /apis/flowcontrol.apiserver.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/flowcontrol.apiserver.k8s.io/v1beta2?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/networking.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/node.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/policy/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/rbac.authorization.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/scheduling.k8s.io/v1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - - ---- - -GET /apis/storage.k8s.io/v1?timeout=32s -Accept: application/json, */* +GET https://kube-apiserver/apis?timeout=32s +Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: public +Content-Type: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList +Etag: (removed) +Vary: Accept ---- - -GET /apis/storage.k8s.io/v1beta1?timeout=32s -Accept: application/json, */* -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /api/v1/namespaces/ns1 +GET https://kube-apiserver/api/v1/namespaces/ns1 Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply - ---- - -GET /openapi/v3?timeout=32s -Accept: application/json, */* +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns1\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns1" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns1", + "resourceVersion": "1000", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} + +--- + +GET https://kube-apiserver/openapi/v3?timeout=32s +Accept: application/json, */* +Accept-Encoding: gzip +Kubectl-Command: kubectl apply + + +200 OK +Accept-Ranges: bytes +Cache-Control: no-cache, private +Content-Type: text/plain; charset=utf-8 +Last-Modified: (removed) + +// discovery response removed for length + +--- + +GET https://kube-apiserver/openapi/v3/api/v1?hash=some-hash&timeout=32s +Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Accept-Ranges: bytes +Cache-Control: public, immutable +Content-Type: application/json +Etag: (removed) +Expires: (removed) +Last-Modified: (removed) +Vary: Accept ---- - -GET /openapi/v2?timeout=32s -Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf -Accept-Encoding: gzip -Kubectl-Command: kubectl apply - - +// discovery response removed for length --- -GET /api/v1/namespaces/ns2 +GET https://kube-apiserver/api/v1/namespaces/ns2 Accept: application/json Accept-Encoding: gzip Kubectl-Command: kubectl apply +200 OK +Cache-Control: no-cache, private +Content-Type: application/json + +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"name\":\"ns2\"}}\n" + }, + "creationTimestamp": "2025-04-01T00:00:00Z", + "labels": { + "kubernetes.io/metadata.name": "ns2" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:kubernetes.io/metadata.name": {} + } + } + }, + "manager": "applier.test", + "operation": "Update", + "time": "2025-04-01T00:00:00Z" + } + ], + "name": "ns2", + "resourceVersion": "1001", + "uid": "fake-uid" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected.yaml b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected.yaml index a3a008f5..5eea50a4 100644 --- a/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected.yaml +++ b/pkg/patterns/declarative/pkg/applier/testdata/kubectl/simple3/expected.yaml @@ -1,11 +1,9 @@ -GET http://kube-apiserver/api/v1 +GET https://kube-apiserver/api/v1 Accept: application/json, */* 200 OK Cache-Control: no-cache, private -Content-Length: 1926 Content-Type: application/json -Date: (removed) -{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"v1","resources":[{"name":"componentstatuses","singularName":"","namespaced":false,"version":"v1","kind":"ComponentStatus","verbs":null},{"name":"configmaps","singularName":"","namespaced":true,"version":"v1","kind":"ConfigMap","verbs":null},{"name":"endpoints","singularName":"","namespaced":true,"version":"v1","kind":"Endpoints","verbs":null},{"name":"events","singularName":"","namespaced":true,"version":"v1","kind":"Event","verbs":null},{"name":"limitranges","singularName":"","namespaced":true,"version":"v1","kind":"LimitRange","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"namespaces","singularName":"","namespaced":false,"version":"v1","kind":"Namespace","verbs":null},{"name":"nodes","singularName":"","namespaced":false,"version":"v1","kind":"Node","verbs":null},{"name":"persistentvolumes","singularName":"","namespaced":false,"version":"v1","kind":"PersistentVolume","verbs":null},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"version":"v1","kind":"PersistentVolumeClaim","verbs":null},{"name":"pods","singularName":"","namespaced":true,"version":"v1","kind":"Pod","verbs":null},{"name":"podtemplates","singularName":"","namespaced":true,"version":"v1","kind":"PodTemplate","verbs":null},{"name":"replicationcontrollers","singularName":"","namespaced":true,"version":"v1","kind":"ReplicationController","verbs":null},{"name":"resourcequotas","singularName":"","namespaced":true,"version":"v1","kind":"ResourceQuota","verbs":null},{"name":"secrets","singularName":"","namespaced":true,"version":"v1","kind":"Secret","verbs":null},{"name":"services","singularName":"","namespaced":true,"version":"v1","kind":"Service","verbs":null},{"name":"serviceaccounts","singularName":"","namespaced":true,"version":"v1","kind":"ServiceAccount","verbs":null}]} +// discovery response removed for length