From 55b33076743995dcd24f6462aefa77f6d77d7024 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 21 Nov 2023 13:21:01 +0100 Subject: [PATCH 01/20] Fix go version in tests --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4a3ae5f..8934810 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: "1.19" + go-version: "1.20" - uses: actions/cache@v2 with: From bb3cd3afb95f2560843fc33938c0d848bf9c495a Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Wed, 22 Nov 2023 18:34:08 +0100 Subject: [PATCH 02/20] Generalize resource reconciliation Combining the usage of go generics, client.Object interface, runtime.Unstructured and jsonpath library from 'https://github.com/ohler55/ojg' the reconciliation logic can be completely generalized for any resource GVK. The idea is to that using the Update verb, the reconciler only cares about the properties that the user configures in the resource template or in the global configuration registry. Any other property is ignored during resource diff to avoid updates when other entities modify the resource (other controllers, webhooks, default values ...) --- Makefile | 4 +- config/global.go | 56 ++ config/global_test.go | 115 +++ go.mod | 29 +- go.sum | 530 +------------ mutators/mutators.go | 81 ++ .../mutators_test.go | 200 +++-- {resources => mutators}/rollout_triggers.go | 41 +- mutators/rollout_triggers_test.go | 270 +++++++ property/changeset.go | 52 -- reconciler/pruner.go | 89 +++ reconciler/pruner_test.go | 294 ++++++++ reconciler/reconciler.go | 164 +---- reconciler/reconciler_test.go | 137 ---- resource/create_or_update.go | 175 +++++ resource/create_or_update_test.go | 403 ++++++++++ resource/property.go | 90 +++ resource/property_test.go | 102 +++ resource/template.go | 119 +++ resource/template_test.go | 37 + resources/configmap.go | 87 --- resources/deployment.go | 168 ----- resources/external_secret.go | 87 --- resources/grafana_dashboard.go | 87 --- resources/hpa.go | 102 --- resources/pdb.go | 92 --- resources/pipeline.go | 94 --- resources/pod_monitor.go | 87 --- resources/role.go | 88 --- resources/rolebinding.go | 89 --- resources/service_account.go | 87 --- resources/services.go | 126 ---- resources/statefulset.go | 129 ---- resources/task.go | 97 --- test/api/v1alpha1/example.com_tests.yaml | 2 + test/api/v1alpha1/test_types.go | 2 + test/api/v1alpha1/zz_generated.deepcopy.go | 5 + .../external-secrets.io_externalsecrets.yaml | 695 ------------------ ...ana.integreatly.org_grafanadashboards.yaml | 165 ----- .../podmonitors.monitoring.coreos.com.yaml | 353 --------- test/suite_test.go | 11 +- test/test_controller.go | 237 +++--- test/test_controller_suite_test.go | 234 ++---- util/cmp.go | 19 + util/k8s.go | 43 ++ util/k8s_test.go | 105 ++- util/maps.go | 11 - util/util.go | 33 + 48 files changed, 2405 insertions(+), 3918 deletions(-) create mode 100644 config/global.go create mode 100644 config/global_test.go create mode 100644 mutators/mutators.go rename resources/services_test.go => mutators/mutators_test.go (63%) rename {resources => mutators}/rollout_triggers.go (50%) create mode 100644 mutators/rollout_triggers_test.go delete mode 100644 property/changeset.go create mode 100644 reconciler/pruner.go create mode 100644 reconciler/pruner_test.go delete mode 100644 reconciler/reconciler_test.go create mode 100644 resource/create_or_update.go create mode 100644 resource/create_or_update_test.go create mode 100644 resource/property.go create mode 100644 resource/property_test.go create mode 100644 resource/template.go create mode 100644 resource/template_test.go delete mode 100644 resources/configmap.go delete mode 100644 resources/deployment.go delete mode 100644 resources/external_secret.go delete mode 100644 resources/grafana_dashboard.go delete mode 100644 resources/hpa.go delete mode 100644 resources/pdb.go delete mode 100644 resources/pipeline.go delete mode 100644 resources/pod_monitor.go delete mode 100644 resources/role.go delete mode 100644 resources/rolebinding.go delete mode 100644 resources/service_account.go delete mode 100644 resources/services.go delete mode 100644 resources/statefulset.go delete mode 100644 resources/task.go delete mode 100644 test/external-apis/external-secrets.io_externalsecrets.yaml delete mode 100644 test/external-apis/grafana.integreatly.org_grafanadashboards.yaml delete mode 100644 test/external-apis/podmonitors.monitoring.coreos.com.yaml create mode 100644 util/cmp.go delete mode 100644 util/maps.go create mode 100644 util/util.go diff --git a/Makefile b/Makefile index 4dcc074..069efa8 100644 --- a/Makefile +++ b/Makefile @@ -42,10 +42,10 @@ vet: ## Run go vet against code. ##@ Test KUBEBUILDER_ASSETS = "$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" -TEST_PKG = ./test/... +TEST_PKG = ./... test: manifests generate fmt vet envtest ginkgo ## Run tests. - KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -r $(TEST_PKG) + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -v -r $(TEST_PKG) manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) crd paths="./test/..." output:crd:artifacts:config="./test/api/v1alpha1" diff --git a/config/global.go b/config/global.go new file mode 100644 index 0000000..c2d8bbc --- /dev/null +++ b/config/global.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ReconcileConfigForGVK struct { + EnsureProperties []string + IgnoreProperties []string +} + +var config = struct { + annotationsDomain string + resourcePruner bool + defaultResourceReconcileConfig map[string]ReconcileConfigForGVK +}{ + annotationsDomain: "basereconciler.3cale.net", + resourcePruner: true, + defaultResourceReconcileConfig: map[string]ReconcileConfigForGVK{ + "*": { + EnsureProperties: []string{ + "metadata.annotations", + "metadata.labels", + "spec", + }, + IgnoreProperties: []string{}, + }, + }, +} + +func GetAnnotationsDomain() string { return config.annotationsDomain } +func SetAnnotationsDomain(domain string) { config.annotationsDomain = domain } + +func EnableResourcePruner() { config.resourcePruner = true } +func DisableResourcePruner() { config.resourcePruner = false } +func IsResourcePrunerEnabled() bool { return config.resourcePruner } + +func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConfigForGVK, error) { + if cfg, ok := config.defaultResourceReconcileConfig[gvk.String()]; ok { + return cfg, nil + } else if defcfg, ok := config.defaultResourceReconcileConfig["*"]; ok { + return defcfg, nil + } else { + return ReconcileConfigForGVK{}, fmt.Errorf("no config registered for gvk %s", gvk) + } +} +func SetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind, cfg ReconcileConfigForGVK) { + if reflect.DeepEqual(gvk, schema.GroupVersionKind{}) { + config.defaultResourceReconcileConfig["*"] = cfg + } else { + config.defaultResourceReconcileConfig[gvk.String()] = cfg + } +} diff --git a/config/global_test.go b/config/global_test.go new file mode 100644 index 0000000..b047e4f --- /dev/null +++ b/config/global_test.go @@ -0,0 +1,115 @@ +package config + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestGetDefaultReconcileConfigForGVK(t *testing.T) { + type args struct { + gvk schema.GroupVersionKind + } + tests := []struct { + name string + prepare func() + args args + want ReconcileConfigForGVK + wantErr bool + }{ + { + name: "Returns the wildcard config", + prepare: func() {}, + args: args{ + gvk: schema.GroupVersionKind{}, + }, + want: ReconcileConfigForGVK{ + EnsureProperties: []string{ + "metadata.annotations", + "metadata.labels", + "spec", + }, + IgnoreProperties: []string{}, + }, + wantErr: false, + }, + { + name: "Returns config for a GVK", + prepare: func() { + config.defaultResourceReconcileConfig["apps/v1, Kind=Deployment"] = ReconcileConfigForGVK{ + EnsureProperties: []string{"a.b.c"}, + IgnoreProperties: []string{"x.y"}, + } + }, + args: args{ + gvk: schema.FromAPIVersionAndKind("apps/v1", "Deployment"), + }, + want: ReconcileConfigForGVK{ + EnsureProperties: []string{"a.b.c"}, + IgnoreProperties: []string{"x.y"}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + got, err := GetDefaultReconcileConfigForGVK(tt.args.gvk) + if (err != nil) != tt.wantErr { + t.Errorf("GetDefaultReconcileConfigForGVK() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetDefaultReconcileConfigForGVK() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSetDefaultReconcileConfigForGVK(t *testing.T) { + type args struct { + gvk schema.GroupVersionKind + cfg ReconcileConfigForGVK + } + tests := []struct { + name string + args args + check func() ReconcileConfigForGVK + }{ + { + name: "Sets the wildcard config", + args: args{ + gvk: schema.GroupVersionKind{}, + cfg: ReconcileConfigForGVK{ + EnsureProperties: []string{"a.b.c"}, + IgnoreProperties: []string{"x.y.z"}, + }, + }, + check: func() ReconcileConfigForGVK { + return config.defaultResourceReconcileConfig["*"] + }, + }, + { + name: "Sets config for the given GVK", + args: args{ + gvk: schema.FromAPIVersionAndKind("apps/v1", "StatefulSet"), + cfg: ReconcileConfigForGVK{ + EnsureProperties: []string{"a.b.c"}, + IgnoreProperties: []string{"x.y.z"}, + }, + }, + check: func() ReconcileConfigForGVK { + return config.defaultResourceReconcileConfig["apps/v1, Kind=StatefulSet"] + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + SetDefaultReconcileConfigForGVK(tt.args.gvk, tt.args.cfg) + if got := tt.check(); !reflect.DeepEqual(got, tt.args.cfg) { + t.Errorf("SetDefaultReconcileConfigForGVK() = %v, want %v", got, tt.args.cfg) + } + }) + } +} diff --git a/go.mod b/go.mod index 385d9b1..9f3140f 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,13 @@ go 1.20 require ( github.com/davecgh/go-spew v1.1.1 - github.com/external-secrets/external-secrets v0.8.1 github.com/go-logr/logr v1.2.4 - github.com/go-test/deep v1.1.0 github.com/google/go-cmp v0.5.9 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e - github.com/grafana-operator/grafana-operator/v4 v4.10.0 + github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 + github.com/ohler55/ojg v1.20.3 github.com/onsi/ginkgo/v2 v2.9.1 github.com/onsi/gomega v1.27.3 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.42.1 - github.com/tektoncd/pipeline v0.49.0 k8s.io/api v0.26.2 k8s.io/apimachinery v0.26.5 k8s.io/client-go v0.26.2 @@ -22,19 +19,12 @@ require ( ) require ( - contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect - contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect - github.com/blendle/zapdriver v1.3.1 // indirect - github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -44,13 +34,9 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-containerregistry v0.15.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect github.com/google/uuid v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -59,33 +45,25 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/openshift/api v0.0.0-20220715133027-dab5b363ebd1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/prometheus/statsd_exporter v0.21.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/stretchr/testify v1.8.3 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.8.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/api v0.121.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -94,7 +72,6 @@ require ( k8s.io/component-base v0.26.2 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect - knative.dev/pkg v0.0.0-20230221145627-8efb3485adcf // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 795916b..faf3ad4 100644 --- a/go.sum +++ b/go.sum @@ -1,96 +1,26 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI= -contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d/go.mod h1:IshRmMJBhDfFj5Y67nVhMYTTIze91RUeT73ipWKs/GY= -contrib.go.opencensus.io/exporter/prometheus v0.4.0 h1:0QfIkj9z/iVZgK31D9H9ohjjIDApI2GOPScCKwxedbs= -contrib.go.opencensus.io/exporter/prometheus v0.4.0/go.mod h1:o7cosnyfuPVK0tB8q0QmaQNhGnptITnPQB+z1+qeFB0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= -github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/v2 v2.14.0 h1:Nrob4FwVgi5L4tV9lhjzZcjYqFVyJzsA56CwPaPfv6s= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/dave/dst v0.26.2/go.mod h1:UMDJuIRPfyUCC78eFuB+SV/WI8oDeyFDvM/JR6NI3IU= -github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= -github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= -github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8= -github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -100,89 +30,38 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/external-secrets/external-secrets v0.8.1 h1:LI7lYmR04Zi2gMVdgifTtyGKfBtYrCA380ePgds2gsY= -github.com/external-secrets/external-secrets v0.8.1/go.mod h1:N5TxTxHLbCK2vVmcUAbUUorwuZiKxJqd/j8I65+44Zc= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -196,93 +75,36 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= -github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40= github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana-operator/grafana-operator/v4 v4.10.0 h1:+AVEPP/wflmx5ySdzt1mIw+q63ZYVInxQhF3XKNhJv4= -github.com/grafana-operator/grafana-operator/v4 v4.10.0/go.mod h1:k69wJcXVrqAcZBoGuh5LSqz0ak8LlVOxxqp0W3f/4V8= -github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -291,374 +113,141 @@ 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.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= +github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= +github.com/ohler55/ojg v1.20.3 h1:Z+fnElsA/GbI5oiT726qJaG4Ca9q5l7UO68Qd0PtkD4= +github.com/ohler55/ojg v1.20.3/go.mod h1:uHcD1ErbErC27Zhb5Df2jUjbseLLcmOCo6oxSr3jZxo= github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk= github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/openshift/api v0.0.0-20220715133027-dab5b363ebd1 h1:FzCXZdnkGLus4hHu7/d/utr3ELPiwNt2ffAqSspi6U8= -github.com/openshift/api v0.0.0-20220715133027-dab5b363ebd1/go.mod h1:LEnw1IVscIxyDnltE3Wi7bQb/QzIM8BfPNKoGA1Qlxw= -github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.42.1 h1:/CZyIylkTNOiVdzTtHwkTHTMOCGJXuLtu3ZLAQrH4u0= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.42.1/go.mod h1:iIz0gzBgsmUvH3POupwMevtm74XmRcEBx8w/tE3sl4k= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/prometheus/statsd_exporter v0.21.0 h1:hA05Q5RFeIjgwKIYEdFd59xu5Wwaznf33yKI+pyX6T8= -github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/tektoncd/pipeline v0.49.0 h1:LxpgoPZvIDiOvPj6vtInnGG0uzuQ5CPA+h8FdJdklh4= -github.com/tektoncd/pipeline v0.49.0/go.mod h1:R3Qn/oTTf1SCLrj+rCg4sqUbpx7vE+6D8Z81+zKUdqQ= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw= -golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -667,86 +256,22 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.121.0 h1:8Oopoo8Vavxx6gt+sgs8s8/X60WBAtKQq6JqnkF+xow= -google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -755,33 +280,22 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -791,62 +305,28 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA= -k8s.io/api v0.24.0/go.mod h1:5Jl90IUrJHUJYEMANRURMiVvJ0g7Ax7r3R1bqO8zx8I= k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= k8s.io/apiextensions-apiserver v0.26.2 h1:/yTG2B9jGY2Q70iGskMf41qTLhL9XeNN2KhI0uDgwko= k8s.io/apiextensions-apiserver v0.26.2/go.mod h1:Y7UPgch8nph8mGCuVk0SK83LnS8Esf3n6fUBgew8SH8= -k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= -k8s.io/apimachinery v0.24.0/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apimachinery v0.26.5 h1:hTQVhJao2piX7vSgCn4Lwd6E0o/+TJIH4NqRf+q4EmE= k8s.io/apimachinery v0.26.5/go.mod h1:HUvk6wrOP4v22AIYqeCGSQ6xWCHo41J9d6psb3temAg= k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= -k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.26.2 h1:IfWgCGUDzrD6wLLgXEstJKYZKAFS2kO+rBRi0p3LqcI= k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= -k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY= k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -knative.dev/pkg v0.0.0-20230221145627-8efb3485adcf h1:TwvZFDpkyqpK2OCAwvNGV2Zjk14FzIh8X8Ci/du3jYI= -knative.dev/pkg v0.0.0-20230221145627-8efb3485adcf/go.mod h1:VO/fcEsq43seuONRQxZyftWHjpMabYzRHDtpSEQ/eoQ= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s= sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/mutators/mutators.go b/mutators/mutators.go new file mode 100644 index 0000000..f87fe12 --- /dev/null +++ b/mutators/mutators.go @@ -0,0 +1,81 @@ +package mutators + +import ( + "context" + "fmt" + + "github.com/3scale-ops/basereconciler/resource" + "github.com/3scale-ops/basereconciler/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// reconcileDeploymentReplicas reconciles the number of replicas of a Deployment +func SetDeploymentReplicas(enforce bool) resource.TemplateMutationFunction { + return func(ctx context.Context, cl client.Client, desired client.Object) error { + if enforce { + // Let the value in the template + // override the runtime value + return nil + } + + live := &appsv1.Deployment{} + if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("unable to retrieve live object: %w", err) + } + + // override the value in the template with the + // runtime value + desired.(*appsv1.Deployment).Spec.Replicas = live.Spec.Replicas + return nil + } +} + +func SetServiceLiveValues() resource.TemplateMutationFunction { + return func(ctx context.Context, cl client.Client, desired client.Object) error { + + svc := desired.(*corev1.Service) + live := &corev1.Service{} + if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("unable to retrieve live object: %w", err) + } + + // Set runtime values in the resource: + // "/spec/clusterIP", "/spec/clusterIPs", "/spec/ports/*/nodePort" + svc.Spec.ClusterIP = live.Spec.ClusterIP + svc.Spec.ClusterIPs = live.Spec.ClusterIPs + + // For services that are not ClusterIP we need to populate the runtime values + // of NodePort for each port + if svc.Spec.Type != corev1.ServiceTypeClusterIP { + for idx, port := range svc.Spec.Ports { + runtimePort := findPort(port.Port, port.Protocol, live.Spec.Ports) + if runtimePort != nil { + svc.Spec.Ports[idx].NodePort = runtimePort.NodePort + } + } + } + return nil + } +} + +func findPort(pNumber int32, pProtocol corev1.Protocol, ports []corev1.ServicePort) *corev1.ServicePort { + // Ports within a svc are uniquely identified by + // the "port" and "protocol" fields. This is documented in + // k8s API reference + for _, port := range ports { + if pNumber == port.Port && pProtocol == port.Protocol { + return &port + } + } + // not found + return nil +} diff --git a/resources/services_test.go b/mutators/mutators_test.go similarity index 63% rename from resources/services_test.go rename to mutators/mutators_test.go index 67aaad4..da4a949 100644 --- a/resources/services_test.go +++ b/mutators/mutators_test.go @@ -1,11 +1,13 @@ -package resources +package mutators import ( "context" "reflect" "testing" - "github.com/go-test/deep" + "github.com/3scale-ops/basereconciler/util" + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -14,11 +16,96 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func Test_populateServiceSpecRuntimeValues(t *testing.T) { +func TestSetDeploymentReplicas(t *testing.T) { type args struct { - ctx context.Context - cl client.Client - svc *corev1.Service + enforce bool + ctx context.Context + cl client.Client + desired *appsv1.Deployment + } + tests := []struct { + name string + args args + want *appsv1.Deployment + wantErr bool + }{ + { + name: "Enforces number of replicas", + args: args{ + enforce: true, + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects( + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](10)}, + }).Build(), + desired: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](2)}, + }, + }, + want: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](2)}, + }, + wantErr: false, + }, + { + name: "Sets live replicas in template", + args: args{ + enforce: false, + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects( + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](10)}, + }).Build(), + desired: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](2)}, + }, + }, + want: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](10)}, + }, + wantErr: false, + }, + { + name: "No error if deployment not found", + args: args{ + enforce: false, + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), + desired: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](2)}, + }, + }, + want: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Replicas: util.Pointer[int32](2)}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := SetDeploymentReplicas(tt.args.enforce)(tt.args.ctx, tt.args.cl, tt.args.desired); (err != nil) != tt.wantErr { + t.Errorf("SetDeploymentReplicas() error = %v, wantErr %v", err, tt.wantErr) + } + if diff := cmp.Diff(tt.args.desired, tt.want); len(diff) > 0 { + t.Errorf("SetDeploymentReplicas() = diff %s", diff) + } + }) + } +} + +func Test_SetServiceLiveValues(t *testing.T) { + type args struct { + ctx context.Context + cl client.Client + desired *corev1.Service } tests := []struct { name string @@ -37,18 +124,13 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{{ Name: "port", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 3333}}, }, }).Build(), - svc: &corev1.Service{ + desired: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, @@ -63,11 +145,6 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{{ @@ -87,18 +164,13 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{{ Name: "port", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 3333}}, }, }).Build(), - svc: &corev1.Service{ + desired: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, @@ -114,11 +186,6 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{ @@ -140,11 +207,6 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{ @@ -153,7 +215,7 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, }, }).Build(), - svc: &corev1.Service{ + desired: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, @@ -168,11 +230,6 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{ @@ -193,18 +250,13 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{{ Name: "port", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP}}, }, }).Build(), - svc: &corev1.Service{ + desired: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, @@ -219,11 +271,6 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, - IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, - IPFamilyPolicy: func() *corev1.IPFamilyPolicyType { - f := corev1.IPFamilyPolicySingleStack - return &f - }(), ClusterIP: "1.1.1.1", ClusterIPs: []string{"1.1.1.1"}, Ports: []corev1.ServicePort{{ @@ -237,7 +284,7 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { args: args{ ctx: context.TODO(), cl: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(), - svc: &corev1.Service{ + desired: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, @@ -261,12 +308,11 @@ func Test_populateServiceSpecRuntimeValues(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := populateServiceSpecRuntimeValues(tt.args.ctx, tt.args.cl, tt.args.svc); (err != nil) != tt.wantErr { - t.Errorf("populateServiceSpecRuntimeValues() error = %v, wantErr %v", err, tt.wantErr) + if err := SetServiceLiveValues()(tt.args.ctx, tt.args.cl, tt.args.desired); (err != nil) != tt.wantErr { + t.Errorf("SetServiceLiveValues() error = %v, wantErr %v", err, tt.wantErr) } - if diff := deep.Equal(tt.args.svc, tt.want); len(diff) > 0 { - t.Errorf("populateServiceSpecRuntimeValues() = diff %s", diff) - + if diff := cmp.Diff(tt.args.desired, tt.want); len(diff) > 0 { + t.Errorf("SetServiceLiveValues() = diff %s", diff) } }) } @@ -283,7 +329,47 @@ func Test_findPort(t *testing.T) { args args want *corev1.ServicePort }{ - // TODO: Add test cases. + { + name: "Fount", + args: args{ + pNumber: 80, + pProtocol: "TCP", + ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + { + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + }, + }, + want: &corev1.ServicePort{ + Name: "http", + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + { + name: "Not fount", + args: args{ + pNumber: 80, + pProtocol: "TCP", + ports: []corev1.ServicePort{{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }}, + }, + want: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/resources/rollout_triggers.go b/mutators/rollout_triggers.go similarity index 50% rename from resources/rollout_triggers.go rename to mutators/rollout_triggers.go index e019d97..d3e8919 100644 --- a/resources/rollout_triggers.go +++ b/mutators/rollout_triggers.go @@ -1,11 +1,13 @@ -package resources +package mutators import ( "context" "fmt" - "github.com/3scale-ops/basereconciler/reconciler" + "github.com/3scale-ops/basereconciler/config" + "github.com/3scale-ops/basereconciler/resource" "github.com/3scale-ops/basereconciler/util" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -22,7 +24,7 @@ type RolloutTrigger struct { // GetHash returns the hash of the data container in the RolloutTrigger // config source -func (rt *RolloutTrigger) GetHash(ctx context.Context, cl client.Client, namespace string) (string, error) { +func (rt RolloutTrigger) GetHash(ctx context.Context, cl client.Client, namespace string) (string, error) { if rt.SecretName != nil { secret := &corev1.Secret{} @@ -53,9 +55,36 @@ func (rt *RolloutTrigger) GetHash(ctx context.Context, cl client.Client, namespa // GetAnnotationKey returns the annotation key to be used in the Pods that read // from the config source defined in the RolloutTrigger -func (rt *RolloutTrigger) GetAnnotationKey() string { +func (rt RolloutTrigger) GetAnnotationKey(annotationsDomain string) string { if rt.SecretName != nil { - return fmt.Sprintf("%s/%s.%s", string(reconciler.Config.AnnotationsDomain), rt.Name, "secret-hash") + return fmt.Sprintf("%s/%s.%s", string(annotationsDomain), rt.Name, "secret-hash") + } + return fmt.Sprintf("%s/%s.%s", string(annotationsDomain), rt.Name, "configmap-hash") +} + +// Add adds the trigger to the Deployment/StatefulSet +func (trigger RolloutTrigger) Add(params ...string) resource.TemplateMutationFunction { + var domain string + if len(params) == 0 { + domain = config.GetAnnotationsDomain() + } else { + domain = params[0] + } + return func(ctx context.Context, cl client.Client, desired client.Object) error { + + hash, err := trigger.GetHash(ctx, cl, desired.GetNamespace()) + if err != nil { + return err + } + trigger := map[string]string{trigger.GetAnnotationKey(domain): hash} + + switch o := desired.(type) { + case *appsv1.Deployment: + o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) + case *appsv1.StatefulSet: + o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) + } + + return nil } - return fmt.Sprintf("%s/%s.%s", string(reconciler.Config.AnnotationsDomain), rt.Name, "configmap-hash") } diff --git a/mutators/rollout_triggers_test.go b/mutators/rollout_triggers_test.go new file mode 100644 index 0000000..ddc25d2 --- /dev/null +++ b/mutators/rollout_triggers_test.go @@ -0,0 +1,270 @@ +package mutators + +import ( + "context" + "testing" + + "github.com/3scale-ops/basereconciler/util" + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestRolloutTrigger_GetHash(t *testing.T) { + type fields struct { + Name string + ConfigMapName *string + SecretName *string + } + type args struct { + ctx context.Context + cl client.Client + namespace string + } + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + { + name: "Secret hash", + fields: fields{ + Name: "secret", + SecretName: util.Pointer("secret"), + }, + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: "ns"}, + Data: map[string][]byte{"key": []byte("data")}, + }, + ).Build(), + namespace: "ns", + }, + want: util.Hash(map[string][]byte{"key": []byte("data")}), + wantErr: false, + }, + { + name: "ConfigMap hash", + fields: fields{ + Name: "cm", + ConfigMapName: util.Pointer("cm"), + }, + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Data: map[string]string{"key": "data"}, + }, + ).Build(), + namespace: "ns", + }, + want: util.Hash(map[string]string{"key": "data"}), + wantErr: false, + }, + { + name: "Returns '' if secret does not exist", + fields: fields{ + Name: "secret", + SecretName: util.Pointer("secret"), + }, + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().Build(), + namespace: "ns", + }, + want: "", + wantErr: false, + }, + { + name: "Returns '' if cm does not exist", + fields: fields{ + Name: "secret", + ConfigMapName: util.Pointer("secret"), + }, + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().Build(), + namespace: "ns", + }, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rt := RolloutTrigger{ + Name: tt.fields.Name, + ConfigMapName: tt.fields.ConfigMapName, + SecretName: tt.fields.SecretName, + } + got, err := rt.GetHash(tt.args.ctx, tt.args.cl, tt.args.namespace) + if (err != nil) != tt.wantErr { + t.Errorf("RolloutTrigger.GetHash() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("RolloutTrigger.GetHash() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRolloutTrigger_GetAnnotationKey(t *testing.T) { + type fields struct { + Name string + ConfigMapName *string + SecretName *string + } + type args struct { + annotationsDomain string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "", + fields: fields{ + Name: "secret", + SecretName: util.Pointer("secret"), + }, + args: args{ + annotationsDomain: "example.com", + }, + want: "example.com/secret.secret-hash", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rt := RolloutTrigger{ + Name: tt.fields.Name, + ConfigMapName: tt.fields.ConfigMapName, + SecretName: tt.fields.SecretName, + } + if got := rt.GetAnnotationKey(tt.args.annotationsDomain); got != tt.want { + t.Errorf("RolloutTrigger.GetAnnotationKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRolloutTrigger_Add(t *testing.T) { + type fields struct { + Name string + ConfigMapName *string + SecretName *string + } + type args struct { + domain string + ctx context.Context + cl client.Client + desired client.Object + } + tests := []struct { + name string + fields fields + args args + want client.Object + wantErr bool + }{ + { + name: "Adds rollout annotation to Deployment's pods", + fields: fields{ + Name: "cm", + ConfigMapName: util.Pointer("cm"), + }, + args: args{ + domain: "example.com", + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Data: map[string]string{"key": "data"}}, + ).Build(), + desired: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}}, + }, + want: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"example.com/cm.configmap-hash": util.Hash(map[string]string{"key": "data"})}, + }}}, + }, + wantErr: false, + }, + { + name: "Adds rollout annotation to Deployment's pods (II)", + fields: fields{ + Name: "cm", + ConfigMapName: util.Pointer("cm"), + }, + args: args{ + domain: "example.com", + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Data: map[string]string{"key": "data"}}, + ).Build(), + desired: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"key": "label"}, + }}}}, + }, + want: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"example.com/cm.configmap-hash": util.Hash(map[string]string{"key": "data"}), "key": "label"}, + }}}, + }, + wantErr: false, + }, + { + name: "Adds rollout annotation to StatefulSet's pods", + fields: fields{ + Name: "cm", + ConfigMapName: util.Pointer("cm"), + }, + args: args{ + domain: "example.com", + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Data: map[string]string{"key": "data"}}, + ).Build(), + desired: &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}}, + }, + want: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}, + Spec: appsv1.StatefulSetSpec{Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"example.com/cm.configmap-hash": util.Hash(map[string]string{"key": "data"})}, + }}}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + trigger := RolloutTrigger{ + Name: tt.fields.Name, + ConfigMapName: tt.fields.ConfigMapName, + SecretName: tt.fields.SecretName, + } + err := trigger.Add(tt.args.domain)(tt.args.ctx, tt.args.cl, tt.args.desired) + if (err != nil) != tt.wantErr { + t.Errorf("RolloutTrigger.Add() error = %v, wantErr %v", err, tt.wantErr) + } + if diff := cmp.Diff(tt.args.desired, tt.want, util.IgnoreProperty("ResourceVersion")); len(diff) > 0 { + t.Errorf("RolloutTrigger.Add() diff = %v", diff) + } + }) + } +} diff --git a/property/changeset.go b/property/changeset.go deleted file mode 100644 index 8cd2882..0000000 --- a/property/changeset.go +++ /dev/null @@ -1,52 +0,0 @@ -package property - -import ( - "github.com/go-logr/logr" - "github.com/google/go-cmp/cmp" - "k8s.io/apimachinery/pkg/api/equality" -) - -type ChangeSet[T any] struct { - path string - current *T - desired *T -} - -func NewChangeSet[T any](path string, current *T, desired *T) *ChangeSet[T] { - return &ChangeSet[T]{path: path, current: current, desired: desired} -} - -// EnsureDesired checks if two structs are equal. If they are not, current is overwriten -// with the value of desired. Bool flag is returned to indicate if the value of current was changed. -func (set *ChangeSet[T]) EnsureDesired(logger logr.Logger) bool { - - if equality.Semantic.DeepEqual(set.current, set.desired) { - return false - } - - logger.V(1).Info("differences detected", "path", set.path, "diff", cmp.Diff(set.current, set.desired)) - if set.desired == nil { - set.current = nil - } else { - *set.current = *set.desired - } - - return true -} - -type ReconcilableProperty interface { - EnsureDesired(logger logr.Logger) bool -} - -func EnsureDesired(logger logr.Logger, changeSets ...ReconcilableProperty) bool { - changed := false - - for _, set := range changeSets { - - if set.EnsureDesired(logger) { - changed = true - } - } - - return changed -} diff --git a/reconciler/pruner.go b/reconciler/pruner.go new file mode 100644 index 0000000..97fcfb8 --- /dev/null +++ b/reconciler/pruner.go @@ -0,0 +1,89 @@ +package reconciler + +import ( + "context" + "fmt" + "reflect" + "strconv" + "sync" + + "github.com/3scale-ops/basereconciler/config" + "github.com/3scale-ops/basereconciler/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (r *Reconciler) pruneOrphaned(ctx context.Context, owner client.Object, managed []corev1.ObjectReference) error { + logger := log.FromContext(ctx) + + ownerGVK, err := apiutil.GVKForObject(owner, r.Scheme) + if err != nil { + return fmt.Errorf("unable to get GVK for owner: %w", err) + } + + for _, gvk := range r.typeTracker.seenTypes { + + objectList, err := util.NewObjectListFromGVK(gvk, r.Scheme) + if err != nil { + return fmt.Errorf("unable to get list type for '%s': %w", gvk.String(), err) + } + err = r.Client.List(ctx, objectList, client.InNamespace(owner.GetNamespace())) + if err != nil { + return err + } + + for _, obj := range util.GetItems(objectList) { + + owned := util.ContainsBy(obj.GetOwnerReferences(), func(ref metav1.OwnerReference) bool { + return ref.Kind == ownerGVK.Kind && ref.Name == owner.GetName() && ref.APIVersion == ownerGVK.GroupVersion().String() + }) + managed := util.ContainsBy(managed, func(ref corev1.ObjectReference) bool { + return ref.Name == obj.GetName() && ref.Namespace == obj.GetNamespace() && ref.Kind == gvk.Kind && ref.APIVersion == gvk.GroupVersion().String() + }) + + // if isOwned(owner, obj) && !util.IsBeingDeleted(obj) && !isManaged(util.ObjectKey(obj), gvk.Kind, managed) { + if owned && !util.IsBeingDeleted(obj) && !managed { + err := r.Client.Delete(ctx, obj) + if err != nil { + return err + } + logger.Info("resource deleted", "kind", gvk.Kind, "resource", obj.GetName()) + } + } + } + return nil +} + +func isPrunerEnabled(owner client.Object) bool { + // prune is active by default + prune := true + + // get the per resource switch (annotation) + if value, ok := owner.GetAnnotations()[fmt.Sprintf("%s/prune", config.GetAnnotationsDomain())]; ok { + var err error + prune, err = strconv.ParseBool(value) + if err != nil { + prune = true + } + } + return prune && config.IsResourcePrunerEnabled() +} + +type typeTracker struct { + seenTypes []schema.GroupVersionKind + mu sync.Mutex +} + +func (tt *typeTracker) trackType(gvk schema.GroupVersionKind) { + if !util.ContainsBy(tt.seenTypes, func(x schema.GroupVersionKind) bool { + return reflect.DeepEqual(x, gvk) + }) { + tt.mu.Lock() + defer tt.mu.Unlock() + tt.seenTypes = append(tt.seenTypes, gvk) + } +} diff --git a/reconciler/pruner_test.go b/reconciler/pruner_test.go new file mode 100644 index 0000000..bcd8792 --- /dev/null +++ b/reconciler/pruner_test.go @@ -0,0 +1,294 @@ +package reconciler + +import ( + "context" + "reflect" + "testing" + + "github.com/3scale-ops/basereconciler/config" + "github.com/3scale-ops/basereconciler/util" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestReconciler_pruneOrphaned(t *testing.T) { + type fields struct { + Client client.Client + Scheme *runtime.Scheme + seenTypes []schema.GroupVersionKind + } + type args struct { + ctx context.Context + owner client.Object + managed []corev1.ObjectReference + } + type check struct { + absent bool + obj client.Object + } + tests := []struct { + name string + fields fields + args args + want []check + wantErr bool + }{ + { + name: "Prunes resource 1", + fields: fields{ + Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects( + &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ + Name: "deploy", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{ + Name: "hpa", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{ + Name: "pdb", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + ).Build(), + Scheme: scheme.Scheme, + seenTypes: []schema.GroupVersionKind{ + schema.FromAPIVersionAndKind("autoscaling/v2", "HorizontalPodAutoscaler"), + schema.FromAPIVersionAndKind("policy/v1", "PodDisruptionBudget"), + }, + }, + args: args{ + ctx: context.TODO(), + owner: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}, + }, + managed: []corev1.ObjectReference{ + {Namespace: "ns", Name: "deploy", Kind: "Deployment", APIVersion: "apps/v1"}, + }, + }, + want: []check{ + {absent: false, obj: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deploy", Namespace: "ns"}}}, + {absent: true, obj: &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{Name: "hpa", Namespace: "ns"}}}, + {absent: true, obj: &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{Name: "pdb", Namespace: "ns"}}}, + }, + wantErr: false, + }, + { + name: "Prunes resource 2", + fields: fields{ + Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects( + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: "aaaa", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: "bbbb", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: "cccc", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: "dddd", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + ).Build(), + Scheme: scheme.Scheme, + seenTypes: []schema.GroupVersionKind{ + schema.FromAPIVersionAndKind("v1", "Secret"), + }, + }, + args: args{ + ctx: context.TODO(), + owner: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}, + }, + managed: []corev1.ObjectReference{ + {Namespace: "ns", Name: "aaaa", Kind: "Secret", APIVersion: "v1"}, + {Namespace: "ns", Name: "bbbb", Kind: "Secret", APIVersion: "v1"}, + {Namespace: "ns", Name: "cccc", Kind: "Secret", APIVersion: "v1"}, + }, + }, + want: []check{ + {absent: false, obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "aaaa", Namespace: "ns"}}}, + {absent: false, obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "bbbb", Namespace: "ns"}}}, + {absent: false, obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cccc", Namespace: "ns"}}}, + {absent: true, obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "dddd", Namespace: "ns"}}}, + }, + wantErr: false, + }, + { + name: "Does nothing", + fields: fields{ + Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects( + &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ + Name: "deploy", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + Name: "sa", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{ + Name: "hpa", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{ + Name: "pdb", Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{APIVersion: "v1", Kind: "ServiceAccount", Name: "owner"}}}}, + ).Build(), + Scheme: scheme.Scheme, + seenTypes: []schema.GroupVersionKind{ + schema.FromAPIVersionAndKind("v1", "ServiceAccount"), + schema.FromAPIVersionAndKind("apps/v1", "Deployment"), + schema.FromAPIVersionAndKind("autoscaling/v2", "HorizontalPodAutoscaler"), + schema.FromAPIVersionAndKind("policy/v1", "PodDisruptionBudget"), + }, + }, + args: args{ + ctx: context.TODO(), + owner: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}, + }, + managed: []corev1.ObjectReference{ + {Namespace: "ns", Name: "deploy", Kind: "Deployment", APIVersion: "apps/v1"}, + {Namespace: "ns", Name: "sa", Kind: "ServiceAccount", APIVersion: "v1"}, + {Namespace: "ns", Name: "hpa", Kind: "HorizontalPodAutoscaler", APIVersion: "autoscaling/v2"}, + {Namespace: "ns", Name: "pdb", Kind: "PodDisruptionBudget", APIVersion: "policy/v1"}, + }, + }, + want: []check{ + {absent: false, obj: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deploy", Namespace: "ns"}}}, + {absent: false, obj: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sa", Namespace: "ns"}}}, + {absent: false, obj: &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{Name: "hpa", Namespace: "ns"}}}, + {absent: false, obj: &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{Name: "pdb", Namespace: "ns"}}}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Reconciler{ + Client: tt.fields.Client, + Scheme: tt.fields.Scheme, + typeTracker: typeTracker{seenTypes: tt.fields.seenTypes}, + } + if err := r.pruneOrphaned(tt.args.ctx, tt.args.owner, tt.args.managed); (err != nil) != tt.wantErr { + t.Errorf("Reconciler.pruneOrphaned() error = %v, wantErr %v", err, tt.wantErr) + return + } + for _, check := range tt.want { + err := tt.fields.Client.Get(tt.args.ctx, util.ObjectKey(check.obj), check.obj) + if (err != nil && errors.IsNotFound(err)) != check.absent { + t.Errorf("Reconciler.pruneOrphaned() want %s to be absent=%v", check.obj.GetName(), check.absent) + } + } + }) + } +} + +func Test_isPrunerEnabled(t *testing.T) { + type args struct { + owner client.Object + } + tests := []struct { + name string + args args + preExec func() + want bool + }{ + { + name: "Returns true", + args: args{ + owner: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns"}, + }, + }, + preExec: func() {}, + want: true, + }, + { + name: "Disabled by annotation", + args: args{ + owner: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns", + Annotations: map[string]string{"example.com/prune": "false"}, + }, + }, + }, + preExec: func() { config.SetAnnotationsDomain("example.com") }, + want: false, + }, + { + name: "Disabled by global config", + args: args{ + owner: &corev1.Service{}, + }, + preExec: func() { config.DisableResourcePruner() }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.preExec() + if got := isPrunerEnabled(tt.args.owner); got != tt.want { + t.Errorf("isPrunerEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_typeTracker_trackType(t *testing.T) { + type fields struct { + seenTypes []schema.GroupVersionKind + } + type args struct { + gvk schema.GroupVersionKind + } + tests := []struct { + name string + fields fields + args args + want []schema.GroupVersionKind + }{ + { + name: "Adds the type", + fields: fields{ + seenTypes: []schema.GroupVersionKind{ + {Group: "", Version: "v1", Kind: "ServiceAccount"}, + }, + }, + args: args{ + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, + }, + want: []schema.GroupVersionKind{ + {Group: "", Version: "v1", Kind: "ServiceAccount"}, + {Group: "", Version: "v1", Kind: "Service"}, + }, + }, + { + name: "Does nothing", + fields: fields{ + seenTypes: []schema.GroupVersionKind{ + {Group: "", Version: "v1", Kind: "Service"}, + }, + }, + args: args{ + gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, + }, + want: []schema.GroupVersionKind{ + {Group: "", Version: "v1", Kind: "Service"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tracker := &typeTracker{ + seenTypes: tt.fields.seenTypes, + } + tracker.trackType(tt.args.gvk) + if !reflect.DeepEqual(tracker.seenTypes, tt.want) { + t.Errorf("(*typeTracker).trackType() = %v, want %v", tracker.seenTypes, tt.want) + } + }) + } +} diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 9e9bd9b..f54e249 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -3,22 +3,14 @@ package reconciler import ( "context" "fmt" - "reflect" - "strconv" + "github.com/3scale-ops/basereconciler/resource" "github.com/3scale-ops/basereconciler/util" - externalsecretsv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" "github.com/go-logr/logr" - grafanav1alpha1 "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - appsv1 "k8s.io/api/apps/v1" - autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,54 +21,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type ReconcilerManagedTypes []client.ObjectList - -func (mts ReconcilerManagedTypes) Register(mt client.ObjectList) ReconcilerManagedTypes { - mts = append(mts, mt) - return mts -} - -func NewManagedTypes() ReconcilerManagedTypes { - return ReconcilerManagedTypes{} -} - -type ReconcilerOptions struct { - ManagedTypes ReconcilerManagedTypes - AnnotationsDomain string - ResourcePruner bool -} - -var Config ReconcilerOptions = ReconcilerOptions{ - AnnotationsDomain: "basereconciler.3cale.net", - ResourcePruner: true, - ManagedTypes: ReconcilerManagedTypes{ - &corev1.ServiceList{}, - &corev1.ConfigMapList{}, - &appsv1.DeploymentList{}, - &appsv1.StatefulSetList{}, - &externalsecretsv1beta1.ExternalSecretList{}, - &grafanav1alpha1.GrafanaDashboardList{}, - &autoscalingv2.HorizontalPodAutoscalerList{}, - &policyv1.PodDisruptionBudgetList{}, - &monitoringv1.PodMonitorList{}, - &rbacv1.RoleBindingList{}, - &rbacv1.RoleList{}, - &corev1.ServiceAccountList{}, - &pipelinev1beta1.PipelineList{}, - &pipelinev1beta1.TaskList{}, - }, -} - -type Resource interface { - Build(ctx context.Context, cl client.Client) (client.Object, error) - Enabled() bool - ResourceReconciler(context.Context, client.Client, client.Object) error -} - // Reconciler computes a list of resources that it needs to keep in place type Reconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + typeTracker typeTracker } func NewFromManager(mgr manager.Manager) Reconciler { @@ -152,114 +101,38 @@ func (r *Reconciler) IsInitialized(instance client.Object, finalizer *string) bo // ManageCleanupLogic contains finalization logic for the LockedResourcesReconciler // Functionality can be extended by passing extra cleanup functions func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), log logr.Logger) error { - // Call any cleanup functions passed for _, fn := range fns { fn() } - return nil } // ReconcileOwnedResources handles generalized resource reconcile logic for // all controllers -func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, resources []Resource) error { - +func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) error { managedResources := []corev1.ObjectReference{} - for _, res := range resources { - - object, err := res.Build(ctx, r.Client) + for _, template := range list { + ref, err := resource.CreateOrUpdate(ctx, r.Client, r.Scheme, owner, template) if err != nil { - return err - } - - if err := controllerutil.SetControllerReference(owner, object, r.Scheme); err != nil { - return err - } - - if err := res.ResourceReconciler(ctx, r.Client, object); err != nil { - return err + return fmt.Errorf("unable to CreateOrUpdate resource: %w", err) } - - managedResources = append(managedResources, corev1.ObjectReference{ - Namespace: object.GetNamespace(), - Name: object.GetName(), - Kind: reflect.TypeOf(object).Elem().Name(), - }) - } - - if IsPrunerEnabled(owner) { - if err := r.PruneOrphaned(ctx, owner, managedResources); err != nil { - return err + if ref != nil { + managedResources = append(managedResources, *ref) + r.typeTracker.trackType(schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)) } } - return nil -} - -func IsPrunerEnabled(owner client.Object) bool { - // prune is active by default - prune := true - - // get the per resource switch (annotation) - if value, ok := owner.GetAnnotations()[fmt.Sprintf("%s/prune", Config.AnnotationsDomain)]; ok { - var err error - prune, err = strconv.ParseBool(value) - if err != nil { - prune = true + if isPrunerEnabled(owner) { + if err := r.pruneOrphaned(ctx, owner, managedResources); err != nil { + return fmt.Errorf("unable to prune orphaned resources: %w", err) } } - return prune && Config.ResourcePruner -} - -func (r *Reconciler) PruneOrphaned(ctx context.Context, owner client.Object, managed []corev1.ObjectReference) error { - logger := log.FromContext(ctx) - - for _, lType := range Config.ManagedTypes { - - err := r.Client.List(ctx, lType, client.InNamespace(owner.GetNamespace())) - if err != nil { - return err - } - - for _, obj := range util.GetItems(lType) { - - kind := reflect.TypeOf(obj).Elem().Name() - if isOwned(owner, obj) && !util.IsBeingDeleted(obj) && !isManaged(util.ObjectKey(obj), kind, managed) { - - err := r.Client.Delete(ctx, obj) - if err != nil { - return err - } - logger.Info("resource deleted", "kind", reflect.TypeOf(obj).Elem().Name(), "resource", obj.GetName()) - } - } - } return nil } -func isOwned(owner client.Object, owned client.Object) bool { - refs := owned.GetOwnerReferences() - for _, ref := range refs { - if ref.Kind == owner.GetObjectKind().GroupVersionKind().Kind && ref.Name == owner.GetName() { - return true - } - } - return false -} - -func isManaged(key types.NamespacedName, kind string, managed []corev1.ObjectReference) bool { - - for _, m := range managed { - if m.Name == key.Name && m.Namespace == key.Namespace && m.Kind == kind { - return true - } - } - return false -} - // SecretEventHandler returns an EventHandler for the specific client.ObjectList // list object passed as parameter func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger) handler.EventHandler { @@ -274,7 +147,14 @@ func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger return []reconcile.Request{} } - return []reconcile.Request{{NamespacedName: util.ObjectKey(items[0])}} + // This is a bit undiscriminate as we don't have a way to detect which + // resources are interested in the event, so we just wake them all up + // TODO: pass a function that can decide if the event is of interest for a given resource + req := make([]reconcile.Request, 0, len(items)) + for _, item := range items { + req = append(req, reconcile.Request{NamespacedName: util.ObjectKey(item)}) + } + return req }, ) } diff --git a/reconciler/reconciler_test.go b/reconciler/reconciler_test.go deleted file mode 100644 index 3e4dcc6..0000000 --- a/reconciler/reconciler_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package reconciler - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func Test_isManaged(t *testing.T) { - type args struct { - key types.NamespacedName - kind string - managed []corev1.ObjectReference - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "Returns true", - args: args{ - key: types.NamespacedName{Name: "system-recaptcha", Namespace: "ns"}, - kind: "Secret", - managed: []corev1.ObjectReference{ - {Namespace: "ns", Name: "system-recaptcha", Kind: "Secret"}, - {Namespace: "ns", Name: "system-smtp", Kind: "Secret"}, - {Namespace: "ns", Name: "system-zync", Kind: "Secret"}, - {Namespace: "ns", Name: "system", Kind: "Secret"}, - {Namespace: "ns", Name: "system-app", Kind: "Deployment"}, - {Namespace: "ns", Name: "system-app", Kind: "ServiceAccount"}, - {Namespace: "ns", Name: "system-app", Kind: "HorizontalPodAutoscaler"}, - {Namespace: "ns", Name: "system-app", Kind: "PodDisruptionBudget"}, - {Namespace: "ns", Name: "system-app", Kind: "PodMonitor"}, - }, - }, - want: true, - }, - { - name: "Returns false", - args: args{ - key: types.NamespacedName{Name: "system-recaptcha", Namespace: "ns"}, - kind: "Secret", - managed: []corev1.ObjectReference{ - {Namespace: "ns", Name: "system-smtp", Kind: "Secret"}, - {Namespace: "ns", Name: "system-zync", Kind: "Secret"}, - {Namespace: "ns", Name: "system", Kind: "Secret"}, - {Namespace: "ns", Name: "system-app", Kind: "Deployment"}, - {Namespace: "ns", Name: "system-app", Kind: "ServiceAccount"}, - {Namespace: "ns", Name: "system-app", Kind: "HorizontalPodAutoscaler"}, - {Namespace: "ns", Name: "system-app", Kind: "PodDisruptionBudget"}, - {Namespace: "ns", Name: "system-app", Kind: "PodMonitor"}, - }, - }, - want: false, - }, - { - name: "Returns false", - args: args{ - key: types.NamespacedName{Name: "system-app", Namespace: "ns"}, - kind: "Role", - managed: []corev1.ObjectReference{ - {Namespace: "ns", Name: "system-smtp", Kind: "Secret"}, - {Namespace: "ns", Name: "system-zync", Kind: "Secret"}, - {Namespace: "ns", Name: "system", Kind: "Secret"}, - {Namespace: "ns", Name: "system-app", Kind: "Deployment"}, - {Namespace: "ns", Name: "system-app", Kind: "ServiceAccount"}, - {Namespace: "ns", Name: "system-app", Kind: "HorizontalPodAutoscaler"}, - {Namespace: "ns", Name: "system-app", Kind: "PodDisruptionBudget"}, - {Namespace: "ns", Name: "system-app", Kind: "PodMonitor"}, - }, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := isManaged(tt.args.key, tt.args.kind, tt.args.managed); got != tt.want { - t.Errorf("isManaged() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_isPrunerEnabled(t *testing.T) { - type args struct { - owner client.Object - } - tests := []struct { - name string - args args - preExec func() - want bool - }{ - { - name: "Returns true", - args: args{ - owner: &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns"}, - }, - }, - preExec: func() {}, - want: true, - }, - { - name: "Disabled by annotation", - args: args{ - owner: &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns", - Annotations: map[string]string{"example.com/prune": "false"}, - }, - }, - }, - preExec: func() { Config.AnnotationsDomain = "example.com" }, - want: false, - }, - { - name: "Disabled by config", - args: args{ - owner: &corev1.Service{}, - }, - preExec: func() { Config.ResourcePruner = false }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.preExec() - if got := IsPrunerEnabled(tt.args.owner); got != tt.want { - t.Errorf("isPrunerEnabled() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/resource/create_or_update.go b/resource/create_or_update.go new file mode 100644 index 0000000..7e431df --- /dev/null +++ b/resource/create_or_update.go @@ -0,0 +1,175 @@ +package resource + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/3scale-ops/basereconciler/config" + "github.com/3scale-ops/basereconciler/util" + "github.com/nsf/jsondiff" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// CreateOrUpdate cretes or updates resources +func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Scheme, + owner client.Object, template TemplateInterface) (*corev1.ObjectReference, error) { + + desired, err := template.Build(ctx, cl, nil) + if err != nil { + return nil, fmt.Errorf("unable to build template: %w", err) + } + + key := util.ObjectKey(desired) + gvk, err := apiutil.GVKForObject(desired, scheme) + if err != nil { + return nil, err + } + logger := log.FromContext(ctx, "gvk", gvk, "resource", desired.GetName()) + + live, err := util.NewObjectFromGVK(gvk, scheme) + if err != nil { + return nil, wrapError("unable to create object from GVK", key, gvk, err) + } + err = cl.Get(ctx, key, live) + if err != nil { + if errors.IsNotFound(err) { + if template.Enabled() { + if err := controllerutil.SetControllerReference(owner, desired, scheme); err != nil { + return nil, wrapError("unable to set controller reference", key, gvk, err) + } + err = cl.Create(ctx, desired) + if err != nil { + return nil, wrapError("unable to create resource", key, gvk, err) + } + logger.Info("resource created") + return util.ObjectReference(desired, gvk), nil + + } else { + return nil, nil + } + } + return nil, wrapError("unable to get resource", key, gvk, err) + } + + /* Delete and return if not enabled */ + if !template.Enabled() { + err := cl.Delete(ctx, live) + if err != nil { + return nil, wrapError("unable to delete object", key, gvk, err) + } + logger.Info("resource deleted") + return nil, nil + } + + ensure, ignore, err := reconcilerConfig(template, gvk) + if err != nil { + return nil, wrapError("unable to retrieve config for resource reconciler", key, gvk, err) + } + + // normalizedLive is a struct that will be populated with only the reconciled + // properties and their respective live values. It will be used to compare it with + // the desire and determine in an update is required. + normalizedLive, err := util.NewObjectFromGVK(gvk, scheme) + if err != nil { + return nil, wrapError("unable to create object from GVK", key, gvk, err) + } + normalizedLive.SetName(desired.GetName()) + normalizedLive.SetNamespace(desired.GetNamespace()) + + // convert to unstructured + u_desired, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desired) + if err != nil { + return nil, wrapError("unable to convert to unstructured", key, gvk, err) + + } + + u_live, err := runtime.DefaultUnstructuredConverter.ToUnstructured(live) + if err != nil { + return nil, wrapError("unable to convert to unstructured", key, gvk, err) + } + + u_normalizedLive, err := runtime.DefaultUnstructuredConverter.ToUnstructured(normalizedLive) + if err != nil { + return nil, wrapError("unable to convert to unstructured", key, gvk, err) + } + + // reconcile properties + for _, property := range ensure { + if err := property.Reconcile(u_live, u_desired, u_normalizedLive, logger); err != nil { + return nil, wrapError(fmt.Sprintf("unable to reconcile property %s", property), key, gvk, err) + } + } + + // ignore properties + for _, property := range ignore { + for _, m := range []map[string]any{u_live, u_desired, u_normalizedLive} { + if err := property.Ignore(m); err != nil { + return nil, wrapError(fmt.Sprintf("unable to ignore property %s", property), key, gvk, err) + } + } + } + + // do the comparison using structs so "equality.Semantic" can be used + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u_normalizedLive, normalizedLive); err != nil { + return nil, wrapError("unable to convert from unstructured", key, gvk, err) + } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u_desired, desired); err != nil { + return nil, wrapError("unable to convert from unstructured", key, gvk, err) + } + if !equality.Semantic.DeepEqual(normalizedLive, desired) { + logger.V(1).Info("resource update required", "diff", printfDiff(normalizedLive, desired)) + err := cl.Update(ctx, client.Object(&unstructured.Unstructured{Object: u_live})) + if err != nil { + return nil, wrapError("unable to update resource", key, gvk, err) + } + logger.Info("Resource updated") + } + + return util.ObjectReference(live, gvk), nil +} + +func printfDiff(a, b client.Object) string { + ajson, err := json.Marshal(a) + if err != nil { + return fmt.Errorf("unable to log differences: %w", err).Error() + } + bjson, err := json.Marshal(b) + if err != nil { + return fmt.Errorf("unable to log differences: %w", err).Error() + } + opts := jsondiff.DefaultJSONOptions() + opts.SkipMatches = true + opts.Indent = "\t" + _, diff := jsondiff.Compare(ajson, bjson, &opts) + return diff +} + +func wrapError(msg string, key types.NamespacedName, gvk schema.GroupVersionKind, err error) error { + return fmt.Errorf("%s %s/%s/%s: %w", msg, gvk.Kind, key.Name, key.Namespace, err) +} + +func reconcilerConfig(template TemplateInterface, gvk schema.GroupVersionKind) ([]Property, []Property, error) { + + if len(template.GetEnsureProperties()) == 0 { + cfg, err := config.GetDefaultReconcileConfigForGVK(gvk) + if err != nil { + return nil, nil, err + } + ensure := util.ConvertStringSlice[string, Property](cfg.EnsureProperties) + ignore := util.ConvertStringSlice[string, Property](cfg.IgnoreProperties) + return ensure, ignore, nil + } + + return template.GetEnsureProperties(), template.GetIgnoreProperties(), nil +} diff --git a/resource/create_or_update_test.go b/resource/create_or_update_test.go new file mode 100644 index 0000000..5b3a88c --- /dev/null +++ b/resource/create_or_update_test.go @@ -0,0 +1,403 @@ +package resource + +import ( + "context" + "reflect" + "testing" + + "github.com/3scale-ops/basereconciler/util" + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestCreateOrUpdate(t *testing.T) { + type args struct { + ctx context.Context + cl client.Client + scheme *runtime.Scheme + owner client.Object + template TemplateInterface + } + tests := []struct { + name string + args args + want *corev1.ObjectReference + wantErr bool + wantObject client.Object + wantObjectErr func(error) bool + }{ + { + name: "Reconciles properties and applies mutations", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }).Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*corev1.Service]{ + TemplateBuilder: func(client.Object) (*corev1.Service, error) { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + Annotations: map[string]string{"key": "value"}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + InternalTrafficPolicy: util.Pointer(corev1.ServiceInternalTrafficPolicyLocal), + Ports: []corev1.ServicePort{{ + Name: "port1", Port: 90, TargetPort: intstr.FromInt(90), Protocol: corev1.ProtocolTCP}}, + }, + }, nil + }, + TemplateMutations: []TemplateMutationFunction{ + func(ctx context.Context, cl client.Client, o client.Object) error { + o.(*corev1.Service).Spec.Ports[0].NodePort = 33333 + return nil + }, + }, + IsEnabled: true, + EnsureProperties: []Property{"metadata.annotations", "spec.selector", "spec.ports", "spec.internalTrafficPolicy"}, + IgnoreProperties: []Property{}, + }, + }, + want: &corev1.ObjectReference{ + Kind: "Service", + Namespace: "ns", + Name: "service", + APIVersion: "v1", + }, + wantErr: false, + wantObject: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + Annotations: map[string]string{"key": "value"}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + InternalTrafficPolicy: util.Pointer(corev1.ServiceInternalTrafficPolicyLocal), + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{{ + Name: "port1", Port: 90, TargetPort: intstr.FromInt(90), Protocol: corev1.ProtocolTCP, NodePort: 33333}}, + }, + }, + }, + { + name: "Ignores properties", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects( + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + InternalTrafficPolicy: util.Pointer(corev1.ServiceInternalTrafficPolicyCluster), + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }).Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*corev1.Service]{ + TemplateBuilder: func(client.Object) (*corev1.Service, error) { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }, nil + }, + IsEnabled: true, + EnsureProperties: []Property{"metadata.annotations", "spec"}, + IgnoreProperties: []Property{"spec.internalTrafficPolicy"}, + }, + }, + want: &corev1.ObjectReference{ + Kind: "Service", + Namespace: "ns", + Name: "service", + APIVersion: "v1", + }, + wantErr: false, + wantObject: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + InternalTrafficPolicy: util.Pointer(corev1.ServiceInternalTrafficPolicyCluster), + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }, + }, + { + name: "Creates object", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*corev1.Service]{ + TemplateBuilder: func(client.Object) (*corev1.Service, error) { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }, nil + }, + IsEnabled: true, + }, + }, + want: &corev1.ObjectReference{ + Kind: "Service", + Namespace: "ns", + Name: "service", + APIVersion: "v1", + }, + wantErr: false, + wantObject: &corev1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "ServiceAccount", + Name: "owner", + Controller: util.Pointer(true), + BlockOwnerDeletion: util.Pointer(true), + }}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{ + {Name: "port1", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP, NodePort: 33333}, + {Name: "port2", Port: 8080, TargetPort: intstr.FromInt(8080), Protocol: corev1.ProtocolTCP, NodePort: 33334}, + }, + Selector: map[string]string{"selector": "deployment"}, + }, + }, + }, + { + name: "Object is disabled and being deleted, do nothing", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*corev1.Service]{ + TemplateBuilder: func(client.Object) (*corev1.Service, error) { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}}, nil + }, + IsEnabled: false, + }, + }, + want: nil, + wantErr: false, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}}, + wantObjectErr: errors.IsNotFound, + }, + { + name: "Object is disabled, delete", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().WithObjects(&corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}}).Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*corev1.Service]{ + TemplateBuilder: func(client.Object) (*corev1.Service, error) { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}}, nil + }, + IsEnabled: false, + }, + }, + want: nil, + wantErr: false, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}}, + wantObjectErr: errors.IsNotFound, + }, + { + name: "Create a Deployment", + args: args{ + ctx: context.TODO(), + cl: fake.NewClientBuilder().Build(), + scheme: scheme.Scheme, + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + template: &Template[*appsv1.Deployment]{ + TemplateBuilder: func(client.Object) (*appsv1.Deployment, error) { + return &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: "ns"}}, nil + }, + IsEnabled: true, + }, + }, + want: &corev1.ObjectReference{ + Kind: "Deployment", + Namespace: "ns", + Name: "deployment", + APIVersion: "apps/v1", + }, + wantErr: false, + wantObject: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment", + Namespace: "ns", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "ServiceAccount", + Name: "owner", + Controller: util.Pointer(true), + BlockOwnerDeletion: util.Pointer(true), + }}, + }, + }, + wantObjectErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := CreateOrUpdate(tt.args.ctx, tt.args.cl, tt.args.scheme, tt.args.owner, tt.args.template) + if (err != nil) != tt.wantErr { + t.Errorf("CreateOrUpdate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(ref, tt.want, util.IgnoreProperty("ResourceVersion"), util.IgnoreProperty("UID")); len(diff) > 0 { + t.Errorf("CreateOrUpdate() ref diff = %v", diff) + return + } + if tt.wantObjectErr == nil { + o, _ := util.NewObjectFromGVK(schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind), tt.args.scheme) + tt.args.cl.Get(context.TODO(), types.NamespacedName{Name: tt.wantObject.GetName(), Namespace: tt.wantObject.GetNamespace()}, o) + if diff := cmp.Diff(o, tt.wantObject, util.IgnoreProperty("ResourceVersion")); len(diff) > 0 { + t.Errorf("CreateOrUpdate() object diff = %v", diff) + } + } else { + o := tt.wantObject.DeepCopyObject().(client.Object) + err := tt.args.cl.Get(context.TODO(), types.NamespacedName{Name: tt.wantObject.GetName(), Namespace: tt.wantObject.GetNamespace()}, o) + if !tt.wantObjectErr(err) { + t.Errorf("CreateOrUpdate() got err retrieving object = %v", err) + } + } + }) + } +} + +func Test_reconcilerConfig(t *testing.T) { + type args struct { + template TemplateInterface + gvk schema.GroupVersionKind + } + tests := []struct { + name string + args args + want []Property + want1 []Property + wantErr bool + }{ + { + name: "Returns default config", + args: args{ + template: &Template[*corev1.Pod]{}, + gvk: schema.FromAPIVersionAndKind("v1", "Pod"), + }, + want: []Property{"metadata.annotations", "metadata.labels", "spec"}, + want1: []Property{}, + wantErr: false, + }, + { + name: "Returns explicit config", + args: args{ + template: &Template[*corev1.Pod]{ + EnsureProperties: []Property{"a.b.c"}, + IgnoreProperties: []Property{"x"}, + }, + gvk: schema.FromAPIVersionAndKind("v1", "Pod"), + }, + want: []Property{"a.b.c"}, + want1: []Property{"x"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := reconcilerConfig(tt.args.template, tt.args.gvk) + if (err != nil) != tt.wantErr { + t.Errorf("reconcilerConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("reconcilerConfig() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("reconcilerConfig() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/resource/property.go b/resource/property.go new file mode 100644 index 0000000..3ff45ec --- /dev/null +++ b/resource/property.go @@ -0,0 +1,90 @@ +package resource + +import ( + "fmt" + + "github.com/go-logr/logr" + "github.com/ohler55/ojg/jp" + "k8s.io/apimachinery/pkg/api/equality" +) + +type PropertyDelta int + +const ( + MissingInBoth PropertyDelta = 0 + MissingFromDesiredPresentInLive PropertyDelta = 1 + PresentInDesiredMissingFromLive PropertyDelta = 2 + PresentInBoth PropertyDelta = 3 +) + +type Property string + +func (p Property) JSONPath() string { return string(p) } + +func (p Property) Reconcile(u_live, u_desired, u_normalizedLive map[string]any, logger logr.Logger) error { + expr, err := jp.ParseString(p.JSONPath()) + if err != nil { + return fmt.Errorf("unable to parse JSONPath '%s': %w", p.JSONPath(), err) + } + + desiredVal := expr.Get(u_desired) + liveVal := expr.Get(u_live) + if len(desiredVal) > 1 || len(liveVal) > 1 { + return fmt.Errorf("multi-valued JSONPath (%s) not supported when reconciling properties", p.JSONPath()) + } + + // store the live value for later comparison in u_normalizedLive + if len(liveVal) != 0 { + if err := expr.Set(u_normalizedLive, liveVal[0]); err != nil { + return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", liveVal[0], p.JSONPath()) + } + } + + switch delta(len(desiredVal), len(liveVal)) { + + case MissingInBoth: + // nothing to do + return nil + + case MissingFromDesiredPresentInLive: + // delete property from u_live + if err := expr.Del(u_live); err != nil { + return fmt.Errorf("usable to delete JSONPath '%s'", p.JSONPath()) + } + return nil + + case PresentInDesiredMissingFromLive: + // add property to u_live + if err := expr.Set(u_live, desiredVal[0]); err != nil { + return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", desiredVal[0], p.JSONPath()) + } + return nil + + case PresentInBoth: + // replace property in u_live if values differ + if !equality.Semantic.DeepEqual(desiredVal[0], liveVal[0]) { + if err := expr.Set(u_live, desiredVal[0]); err != nil { + return fmt.Errorf("usable to replace value '%v' in JSONPath '%s'", desiredVal[0], p.JSONPath()) + } + return nil + } + + } + + return nil +} + +func delta(a, b int) PropertyDelta { + return PropertyDelta(a<<1 + b) +} + +func (p Property) Ignore(m map[string]any) error { + expr, err := jp.ParseString(p.JSONPath()) + if err != nil { + return fmt.Errorf("unable to parse JSONPath '%s': %w", p.JSONPath(), err) + } + if err = expr.Del(m); err != nil { + return fmt.Errorf("unable to parse delete JSONPath '%s' from unstructured: %w", p.JSONPath(), err) + } + return nil +} diff --git a/resource/property_test.go b/resource/property_test.go new file mode 100644 index 0000000..6f12803 --- /dev/null +++ b/resource/property_test.go @@ -0,0 +1,102 @@ +package resource + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + "github.com/onsi/gomega" +) + +func TestProperty_Reconcile(t *testing.T) { + type args struct { + u_live map[string]any + u_desired map[string]any + u_normalizedLive map[string]any + logger logr.Logger + } + tests := []struct { + name string + p Property + args args + wantErr bool + wantLive map[string]any + wantNormalizedLive map[string]any + }{ + { + name: "PresentInBoth", + p: "a.b.c", + args: args{ + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, + u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, + u_normalizedLive: map[string]any{}, + logger: logr.Discard(), + }, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue", "d": 1}}}, + wantNormalizedLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value"}}}, + }, + { + name: "MissingInBoth", + p: "a.b.c", + args: args{ + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, + u_desired: map[string]any{}, + u_normalizedLive: map[string]any{}, + logger: logr.Discard(), + }, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, + wantNormalizedLive: map[string]any{}, + }, + { + name: "PresentInDesiredMissingFromLive", + p: "a.b.c", + args: args{ + u_live: map[string]any{"a": map[string]any{}}, + u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, + u_normalizedLive: map[string]any{}, + logger: logr.Discard(), + }, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, + wantNormalizedLive: map[string]any{}, + }, + { + name: "MissingFromDesiredPresentInLive", + p: "a.b.c", + args: args{ + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, + u_desired: map[string]any{"a": map[string]any{}}, + u_normalizedLive: map[string]any{}, + logger: logr.Discard(), + }, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, + wantNormalizedLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value"}}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.p.Reconcile(tt.args.u_live, tt.args.u_desired, tt.args.u_normalizedLive, tt.args.logger) + if (err != nil) != tt.wantErr { + t.Errorf("Property.Reconcile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.args.u_live, tt.wantLive); len(diff) > 0 { + t.Errorf("Property.Reconcile() diff in live %v", diff) + } + if diff := cmp.Diff(tt.args.u_normalizedLive, tt.wantNormalizedLive); len(diff) > 0 { + t.Errorf("Property.Reconcile() diff in normalizedLive %v", diff) + } + }) + } +} + +func Test_delta(t *testing.T) { + g := gomega.NewWithT(t) + g.Expect(delta(0, 0)).To(gomega.Equal(MissingInBoth)) + g.Expect(delta(0, 1)).To(gomega.Equal(MissingFromDesiredPresentInLive)) + g.Expect(delta(1, 0)).To(gomega.Equal(PresentInDesiredMissingFromLive)) + g.Expect(delta(1, 1)).To(gomega.Equal(PresentInBoth)) +} diff --git a/resource/template.go b/resource/template.go new file mode 100644 index 0000000..e9d23a5 --- /dev/null +++ b/resource/template.go @@ -0,0 +1,119 @@ +package resource + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type TemplateInterface interface { + Build(ctx context.Context, cl client.Client, o client.Object) (client.Object, error) + Enabled() bool + GetEnsureProperties() []Property + GetIgnoreProperties() []Property +} + +// TemplateBuilderFunction is a function that returns a k8s API object (client.Object) when +// called. TemplateBuilderFunction has no access to cluster live info +type TemplateBuilderFunction[T client.Object] func(client.Object) (T, error) + +// func NewBuilderFunctionFromObject[T client.Object](o T) TemplateBuilderFunction[T] { +// return func(client.Object) (T, error) { +// return o, nil +// } +// } + +// TemplateMutationFunction represents mutation functions that require an API client, generally +// because they need to retrieve live cluster information to mutate the object +type TemplateMutationFunction func(context.Context, client.Client, client.Object) error + +type Template[T client.Object] struct { + // TemplateBuilder is the function that is used as the basic + // tempalte for the object. It is called by Build() to create the + // object. + TemplateBuilder TemplateBuilderFunction[T] + // TemplateMutations are functions that are called during Build() after + // TemplateBuilder has ben invoked, to perform mutations on the object that require + // access to an API client. + TemplateMutations []TemplateMutationFunction + // IsEnabled specifies whether the resourse describe by this Template should + // exists or not + IsEnabled bool + // EnsureProperties are the properties from the desired object that should be enforced + // to the live object. The syntax is jsonpath. + EnsureProperties []Property + // IgnoreProperties are the properties from the live object that should not trigger + // updates. This is used to ignore nested properties within the "EnsuredProperties". The + // syntax is jsonpath. + IgnoreProperties []Property +} + +func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], + enabled bool, mutations ...TemplateMutationFunction) *Template[T] { + return &Template[T]{ + TemplateBuilder: tb, + TemplateMutations: mutations, + IsEnabled: enabled, + EnsureProperties: []Property{}, + IgnoreProperties: []Property{}, + } +} + +func NewTemplateFromObjectFunction[T client.Object](fn func() T, + enabled bool, mutations ...TemplateMutationFunction) *Template[T] { + return &Template[T]{ + TemplateBuilder: func(client.Object) (T, error) { return fn(), nil }, + TemplateMutations: mutations, + IsEnabled: enabled, + EnsureProperties: []Property{}, + IgnoreProperties: []Property{}, + } +} + +// Build returns a T resource by executing its template function +func (t *Template[T]) Build(ctx context.Context, cl client.Client, o client.Object) (client.Object, error) { + o, err := t.TemplateBuilder(o) + if err != nil { + return nil, err + } + for _, fn := range t.TemplateMutations { + if err := fn(ctx, cl, o); err != nil { + return nil, err + } + } + return o.DeepCopyObject().(client.Object), nil +} + +// Enabled indicates if the resource should be present or not +func (t *Template[T]) Enabled() bool { + return t.IsEnabled +} + +// GetEnsureProperties returns the list of properties that should be reconciled +func (t *Template[T]) GetEnsureProperties() []Property { + return t.EnsureProperties +} + +// GetIgnoreProperties returns the list of properties that should be ignored +func (t *Template[T]) GetIgnoreProperties() []Property { + return t.IgnoreProperties +} + +// Apply chains template functions to make them composable +func (t *Template[T]) Apply(mutation TemplateBuilderFunction[T]) *Template[T] { + + fn := t.TemplateBuilder + t.TemplateBuilder = func(in client.Object) (T, error) { + o, err := fn(in) + if err != nil { + return o, err + } + return mutation(o) + } + + return t +} + +func (t *Template[T]) Chain(mutation TemplateBuilderFunction[T]) *Template[T] { + return t.Apply(mutation) +} diff --git a/resource/template_test.go b/resource/template_test.go new file mode 100644 index 0000000..b32fff8 --- /dev/null +++ b/resource/template_test.go @@ -0,0 +1,37 @@ +package resource + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestTemplate_Apply(t *testing.T) { + + podTemplate := &Template[*corev1.Pod]{ + TemplateBuilder: func(client.Object) (*corev1.Pod, error) { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "ns"}, + }, nil + }, + } + + podTemplate.Apply(func(o client.Object) (*corev1.Pod, error) { + o.SetAnnotations(map[string]string{"key": "value"}) + return o.(*corev1.Pod), nil + }) + + got, _ := podTemplate.Build(context.TODO(), fake.NewClientBuilder().Build(), nil) + want := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "ns", Annotations: map[string]string{"key": "value"}}, + } + + if diff := cmp.Diff(got, want); len(diff) > 0 { + t.Errorf("(Template).Apply() diff = %v", diff) + } +} diff --git a/resources/configmap.go b/resources/configmap.go deleted file mode 100644 index 86d69b7..0000000 --- a/resources/configmap.go +++ /dev/null @@ -1,87 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = ConfigMapTemplate{} - -// ConfigMapTemplate has methods to generate and reconcile a ConfigMap -type ConfigMapTemplate struct { - Template func() *corev1.ConfigMap - IsEnabled bool -} - -// Build returns a ConfigMap resource -func (cmt ConfigMapTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return cmt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (cmt ConfigMapTemplate) Enabled() bool { - return cmt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for ConfigMap resources -func (cmt ConfigMapTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "ConfigMap", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*corev1.ConfigMap) - - instance := &corev1.ConfigMap{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if cmt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !cmt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("data", &instance.Data, &desired.Data), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/resources/deployment.go b/resources/deployment.go deleted file mode 100644 index b4cb0b5..0000000 --- a/resources/deployment.go +++ /dev/null @@ -1,168 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - "github.com/3scale-ops/basereconciler/util" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = DeploymentTemplate{} - -// DeploymentTemplate specifies a Deployment resource and its rollout triggers -type DeploymentTemplate struct { - Template func() *appsv1.Deployment - RolloutTriggers []RolloutTrigger - EnforceReplicas bool - IsEnabled bool -} - -func (dt DeploymentTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - - dep := dt.Template() - - if err := dt.reconcileDeploymentReplicas(ctx, cl, dep); err != nil { - return nil, err - } - - if err := dt.reconcileRolloutTriggers(ctx, cl, dep); err != nil { - return nil, err - } - - return dep.DeepCopy(), nil -} - -func (dt DeploymentTemplate) Enabled() bool { - return dt.IsEnabled -} - -func (dep DeploymentTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "Deployment", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*appsv1.Deployment) - - instance := &appsv1.Deployment{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if dep.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !dep.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Merge annotations */ - desired.ObjectMeta.Annotations = util.MergeMaps( - map[string]string{}, - desired.GetAnnotations(), - map[string]string{"deployment.kubernetes.io/revision": instance.GetAnnotations()["deployment.kubernetes.io/revision"]}, - ) - - /* Inherit some values usually defaulted by the cluster if not defined on the template */ - if desired.Spec.Template.Spec.DNSPolicy == "" { - desired.Spec.Template.Spec.DNSPolicy = instance.Spec.Template.Spec.DNSPolicy - } - if desired.Spec.Template.Spec.SchedulerName == "" { - desired.Spec.Template.Spec.SchedulerName = instance.Spec.Template.Spec.SchedulerName - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[int32]("spec.minReadySeconds", &instance.Spec.MinReadySeconds, &desired.Spec.MinReadySeconds), - property.NewChangeSet[int32]("spec.replicas", instance.Spec.Replicas, desired.Spec.Replicas), - property.NewChangeSet[metav1.LabelSelector]("spec.selector", instance.Spec.Selector, desired.Spec.Selector), - property.NewChangeSet[appsv1.DeploymentStrategy]("spec.strategy", &instance.Spec.Strategy, &desired.Spec.Strategy), - property.NewChangeSet[map[string]string]("spec.template.metadata.labels", &instance.Spec.Template.ObjectMeta.Labels, &desired.Spec.Template.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("spec.template.metadata.annotations", &instance.Spec.Template.ObjectMeta.Annotations, &desired.Spec.Template.ObjectMeta.Annotations), - property.NewChangeSet[corev1.PodSpec]("spec.template.spec", &instance.Spec.Template.Spec, &desired.Spec.Template.Spec), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} - -// reconcileDeploymentReplicas reconciles the number of replicas of a Deployment -func (dt DeploymentTemplate) reconcileDeploymentReplicas(ctx context.Context, cl client.Client, dep *appsv1.Deployment) error { - - if dt.EnforceReplicas { - // Let the value in the template - // override the runtime value - return nil - } - - key := types.NamespacedName{ - Name: dep.GetName(), - Namespace: dep.GetNamespace(), - } - instance := &appsv1.Deployment{} - err := cl.Get(ctx, key, instance) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - return err - } - - // override the value in the template with the - // runtime value - dep.Spec.Replicas = instance.Spec.Replicas - return nil -} - -// reconcileRolloutTriggers modifies the Deployment with the appropriate rollout triggers (annotations) -func (dt DeploymentTemplate) reconcileRolloutTriggers(ctx context.Context, cl client.Client, dep *appsv1.Deployment) error { - - if dep.Spec.Template.ObjectMeta.Annotations == nil { - dep.Spec.Template.ObjectMeta.Annotations = map[string]string{} - } - - for _, trigger := range dt.RolloutTriggers { - hash, err := trigger.GetHash(ctx, cl, dep.GetNamespace()) - if err != nil { - return err - } - dep.Spec.Template.ObjectMeta.Annotations[trigger.GetAnnotationKey()] = hash - } - - return nil -} diff --git a/resources/external_secret.go b/resources/external_secret.go deleted file mode 100644 index 20c8c72..0000000 --- a/resources/external_secret.go +++ /dev/null @@ -1,87 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - externalsecretsv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = ExternalSecretTemplate{} - -// ExternalSecretTemplate has methods to generate and reconcile an ExternalSecret -type ExternalSecretTemplate struct { - Template func() *externalsecretsv1beta1.ExternalSecret - IsEnabled bool -} - -// Build returns an ExternalSecret resource -func (est ExternalSecretTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return est.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (est ExternalSecretTemplate) Enabled() bool { - return est.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for ExternalSecret resources -func (est ExternalSecretTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "ExternalSecret", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*externalsecretsv1beta1.ExternalSecret) - - instance := &externalsecretsv1beta1.ExternalSecret{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if est.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !est.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[externalsecretsv1beta1.ExternalSecretSpec]("spec", &instance.Spec, &desired.Spec), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} diff --git a/resources/grafana_dashboard.go b/resources/grafana_dashboard.go deleted file mode 100644 index 59b761b..0000000 --- a/resources/grafana_dashboard.go +++ /dev/null @@ -1,87 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - grafanav1alpha1 "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = GrafanaDashboardTemplate{} - -// GrafanaDashboardTemplate has methods to generate and reconcile a GrafanaDashboard -type GrafanaDashboardTemplate struct { - Template func() *grafanav1alpha1.GrafanaDashboard - IsEnabled bool -} - -// Build returns a GrafanaDashboard resource -func (gdt GrafanaDashboardTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return gdt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (gdt GrafanaDashboardTemplate) Enabled() bool { - return gdt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for GrafanaDashboard resources -func (gdt GrafanaDashboardTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "GrafanaDashboard", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*grafanav1alpha1.GrafanaDashboard) - - instance := &grafanav1alpha1.GrafanaDashboard{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if gdt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !gdt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[grafanav1alpha1.GrafanaDashboardSpec]("spec", &instance.Spec, &desired.Spec), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} diff --git a/resources/hpa.go b/resources/hpa.go deleted file mode 100644 index 852346f..0000000 --- a/resources/hpa.go +++ /dev/null @@ -1,102 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - autoscalingv2 "k8s.io/api/autoscaling/v2" - "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = HorizontalPodAutoscalerTemplate{} - -// HorizontalPodAutoscalerTemplate has methods to generate and reconcile a HorizontalPodAutoscaler -type HorizontalPodAutoscalerTemplate struct { - Template func() *autoscalingv2.HorizontalPodAutoscaler - IsEnabled bool -} - -// Build returns a HorizontalPodAutoscaler resource -func (hpat HorizontalPodAutoscalerTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return hpat.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (hpat HorizontalPodAutoscalerTemplate) Enabled() bool { - return hpat.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for HorizontalPodAutoscaler resources -func (hpat HorizontalPodAutoscalerTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "HorizontalPodAutoscaler", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*autoscalingv2.HorizontalPodAutoscaler) - - instance := &autoscalingv2.HorizontalPodAutoscaler{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if hpat.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !hpat.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Reconcile metadata */ - if !equality.Semantic.DeepEqual(instance.GetAnnotations(), desired.GetAnnotations()) { - instance.ObjectMeta.Annotations = desired.GetAnnotations() - needsUpdate = true - } - if !equality.Semantic.DeepEqual(instance.GetLabels(), desired.GetLabels()) { - instance.ObjectMeta.Labels = desired.GetLabels() - needsUpdate = true - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[autoscalingv2.CrossVersionObjectReference]("spec.scaleTargetRef", &instance.Spec.ScaleTargetRef, &desired.Spec.ScaleTargetRef), - property.NewChangeSet[int32]("spec.minReplicas", instance.Spec.MinReplicas, desired.Spec.MinReplicas), - property.NewChangeSet[int32]("spec.maxReplicas", &instance.Spec.MaxReplicas, &desired.Spec.MaxReplicas), - property.NewChangeSet[[]autoscalingv2.MetricSpec]("spec.metrics", &instance.Spec.Metrics, &desired.Spec.Metrics), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} diff --git a/resources/pdb.go b/resources/pdb.go deleted file mode 100644 index 0493b2d..0000000 --- a/resources/pdb.go +++ /dev/null @@ -1,92 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - policyv1 "k8s.io/api/policy/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = PodDisruptionBudgetTemplate{} - -// PodDisruptionBudgetTemplate has methods to generate and reconcile a PodDisruptionBudget -type PodDisruptionBudgetTemplate struct { - Template func() *policyv1.PodDisruptionBudget - IsEnabled bool -} - -// Build returns a PodDisruptionBudget resource -func (pdbt PodDisruptionBudgetTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return pdbt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (pdbt PodDisruptionBudgetTemplate) Enabled() bool { - return pdbt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for PodDisruptionBudget resources -func (pdbt PodDisruptionBudgetTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "PodDisruptionBudget", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*policyv1.PodDisruptionBudget) - - instance := &policyv1.PodDisruptionBudget{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if pdbt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !pdbt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[intstr.IntOrString]("spec.maxUnavailable", instance.Spec.MaxUnavailable, desired.Spec.MaxUnavailable), - property.NewChangeSet[intstr.IntOrString]("spec.minAvailable", instance.Spec.MinAvailable, desired.Spec.MinAvailable), - property.NewChangeSet[metav1.LabelSelector]("spec.selector", instance.Spec.Selector, desired.Spec.Selector), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} diff --git a/resources/pipeline.go b/resources/pipeline.go deleted file mode 100644 index f559a4a..0000000 --- a/resources/pipeline.go +++ /dev/null @@ -1,94 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = PipelineTemplate{} - -// PipelineTemplate has methods to generate and reconcile a Pipeline -type PipelineTemplate struct { - Template func() *pipelinev1beta1.Pipeline - IsEnabled bool -} - -// Build returns a Pipeline resource -func (pt PipelineTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return pt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (pt PipelineTemplate) Enabled() bool { - return pt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for Pipeline resources -func (pt PipelineTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "Pipeline", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*pipelinev1beta1.Pipeline) - - instance := &pipelinev1beta1.Pipeline{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if pt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !pt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[string]("spec.displayName", &instance.Spec.DisplayName, &desired.Spec.DisplayName), - property.NewChangeSet[string]("spec.description", &instance.Spec.Description, &desired.Spec.Description), - property.NewChangeSet[pipelinev1beta1.ParamSpecs]("spec.params", &instance.Spec.Params, &desired.Spec.Params), - property.NewChangeSet[[]pipelinev1beta1.PipelineTask]("spec.tasks", &instance.Spec.Tasks, &desired.Spec.Tasks), - property.NewChangeSet[[]pipelinev1beta1.PipelineWorkspaceDeclaration]("spec.workspaces", &instance.Spec.Workspaces, &desired.Spec.Workspaces), - property.NewChangeSet[[]pipelinev1beta1.PipelineResult]("spec.results", &instance.Spec.Results, &desired.Spec.Results), - property.NewChangeSet[[]pipelinev1beta1.PipelineTask]("spec.finally", &instance.Spec.Finally, &desired.Spec.Finally), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/resources/pod_monitor.go b/resources/pod_monitor.go deleted file mode 100644 index c689187..0000000 --- a/resources/pod_monitor.go +++ /dev/null @@ -1,87 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = PodMonitorTemplate{} - -// PodMonitorTemplate has methods to generate and reconcile a PodMonitor -type PodMonitorTemplate struct { - Template func() *monitoringv1.PodMonitor - IsEnabled bool -} - -// Build returns a PodMonitor resource -func (pmt PodMonitorTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return pmt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (pmt PodMonitorTemplate) Enabled() bool { - return pmt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for PodMonitor resources -func (pmt PodMonitorTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "PodMonitor", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*monitoringv1.PodMonitor) - - instance := &monitoringv1.PodMonitor{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if pmt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !pmt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[monitoringv1.PodMonitorSpec]("data", &instance.Spec, &desired.Spec), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} diff --git a/resources/role.go b/resources/role.go deleted file mode 100644 index 3ab9202..0000000 --- a/resources/role.go +++ /dev/null @@ -1,88 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = RoleTemplate{} - -// RoleTemplate has methods to generate and reconcile a Role -type RoleTemplate struct { - Template func() *rbacv1.Role - IsEnabled bool -} - -// Build returns a Role resource -func (rt RoleTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return rt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (rt RoleTemplate) Enabled() bool { - return rt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for Role resources -func (rt RoleTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "Role", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*rbacv1.Role) - - instance := &rbacv1.Role{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if rt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !rt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[[]rbacv1.PolicyRule]("rules", &instance.Rules, &desired.Rules), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/resources/rolebinding.go b/resources/rolebinding.go deleted file mode 100644 index f1c39bf..0000000 --- a/resources/rolebinding.go +++ /dev/null @@ -1,89 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = RoleBindingTemplate{} - -// RoleBindingTemplate has methods to generate and reconcile a RoleBinding -type RoleBindingTemplate struct { - Template func() *rbacv1.RoleBinding - IsEnabled bool -} - -// Build returns a RoleBinding resource -func (rbt RoleBindingTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return rbt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (rbt RoleBindingTemplate) Enabled() bool { - return rbt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for RoleBinding resources -func (rbt RoleBindingTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "RoleBinding", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*rbacv1.RoleBinding) - - instance := &rbacv1.RoleBinding{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if rbt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !rbt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[rbacv1.RoleRef]("roleRef", &instance.RoleRef, &desired.RoleRef), - property.NewChangeSet[[]rbacv1.Subject]("subjects", &instance.Subjects, &desired.Subjects), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/resources/service_account.go b/resources/service_account.go deleted file mode 100644 index 237bfe4..0000000 --- a/resources/service_account.go +++ /dev/null @@ -1,87 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = ServiceAccountTemplate{} - -// ServiceAccountTemplate has methods to generate and reconcile a ServiceAccount -type ServiceAccountTemplate struct { - Template func() *corev1.ServiceAccount - IsEnabled bool -} - -// Build returns a ServiceAccount resource -func (sat ServiceAccountTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return sat.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (sat ServiceAccountTemplate) Enabled() bool { - return sat.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for ServiceAccount resources -func (sat ServiceAccountTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "ServiceAccount", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*corev1.ServiceAccount) - - instance := &corev1.ServiceAccount{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if sat.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !sat.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/resources/services.go b/resources/services.go deleted file mode 100644 index 0c4a575..0000000 --- a/resources/services.go +++ /dev/null @@ -1,126 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = ServiceTemplate{} - -// ServiceTemplate has methods to generate and reconcile a Service -type ServiceTemplate struct { - Template func() *corev1.Service - IsEnabled bool -} - -// Build returns a Service resource -func (st ServiceTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - - svc := st.Template() - - if err := populateServiceSpecRuntimeValues(ctx, cl, svc); err != nil { - return nil, err - } - - return svc.DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (dt ServiceTemplate) Enabled() bool { - return dt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for Service resources -func (st ServiceTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "Service", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*corev1.Service) - - instance := &corev1.Service{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - } - return err - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[[]corev1.ServicePort]("spec.ports", &instance.Spec.Ports, &desired.Spec.Ports), - property.NewChangeSet[map[string]string]("spec.selector", &instance.Spec.Selector, &desired.Spec.Selector), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} - -func populateServiceSpecRuntimeValues(ctx context.Context, cl client.Client, svc *corev1.Service) error { - - instance := &corev1.Service{} - err := cl.Get(ctx, types.NamespacedName{Name: svc.GetName(), Namespace: svc.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - // Resource not found, return the template as is - // because there are not runtime values yet - return nil - } - return err - } - - // Set runtime values in the resource: - // "/spec/clusterIP", "/spec/clusterIPs", "/spec/ipFamilies", "/spec/ipFamilyPolicy", "/spec/ports/*/nodePort" - svc.Spec.ClusterIP = instance.Spec.ClusterIP - svc.Spec.ClusterIPs = instance.Spec.ClusterIPs - svc.Spec.IPFamilies = instance.Spec.IPFamilies - svc.Spec.IPFamilyPolicy = instance.Spec.IPFamilyPolicy - - // For services that are not ClusterIP we need to populate the runtime values - // of NodePort for each port - if svc.Spec.Type != corev1.ServiceTypeClusterIP { - for idx, port := range svc.Spec.Ports { - runtimePort := findPort(port.Port, port.Protocol, instance.Spec.Ports) - if runtimePort != nil { - svc.Spec.Ports[idx].NodePort = runtimePort.NodePort - } - } - } - - return nil -} - -func findPort(pNumber int32, pProtocol corev1.Protocol, ports []corev1.ServicePort) *corev1.ServicePort { - // Ports within a svc are uniquely identified by - // the "port" and "protocol" fields. This is documented in - // k8s API reference - for _, port := range ports { - if pNumber == port.Port && pProtocol == port.Protocol { - return &port - } - } - // not found - return nil -} diff --git a/resources/statefulset.go b/resources/statefulset.go deleted file mode 100644 index c654270..0000000 --- a/resources/statefulset.go +++ /dev/null @@ -1,129 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = StatefulSetTemplate{} - -// StatefulSet specifies a StatefulSet resource and its rollout triggers -type StatefulSetTemplate struct { - Template func() *appsv1.StatefulSet - RolloutTriggers []RolloutTrigger - IsEnabled bool -} - -func (sst StatefulSetTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - - ss := sst.Template() - - if err := sst.reconcileRolloutTriggers(ctx, cl, ss); err != nil { - return nil, err - } - - return ss.DeepCopy(), nil -} - -func (sst StatefulSetTemplate) Enabled() bool { - return sst.IsEnabled -} - -func (sts StatefulSetTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "StatefulSet", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*appsv1.StatefulSet) - - instance := &appsv1.StatefulSet{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if sts.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !sts.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - if desired.Spec.Template.Spec.SchedulerName == "" { - desired.Spec.Template.Spec.SchedulerName = instance.Spec.Template.Spec.SchedulerName - } - if desired.Spec.Template.Spec.DNSPolicy == "" { - desired.Spec.Template.Spec.DNSPolicy = instance.Spec.Template.Spec.DNSPolicy - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[int32]("spec.minReadySeconds", &instance.Spec.MinReadySeconds, &desired.Spec.MinReadySeconds), - property.NewChangeSet[appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy]("spec.persistentVolumeClaimRetentionPolicy", instance.Spec.PersistentVolumeClaimRetentionPolicy, desired.Spec.PersistentVolumeClaimRetentionPolicy), - property.NewChangeSet[int32]("spec.replicas", instance.Spec.Replicas, desired.Spec.Replicas), - property.NewChangeSet[metav1.LabelSelector]("spec.selector", instance.Spec.Selector, desired.Spec.Selector), - property.NewChangeSet[string]("spec.serviceName", &instance.Spec.ServiceName, &desired.Spec.ServiceName), - property.NewChangeSet[appsv1.StatefulSetUpdateStrategy]("spec.updateStrategy", &instance.Spec.UpdateStrategy, &desired.Spec.UpdateStrategy), - property.NewChangeSet[[]corev1.PersistentVolumeClaim]("spec.volumeClaimTemplates", &instance.Spec.VolumeClaimTemplates, &desired.Spec.VolumeClaimTemplates), - property.NewChangeSet[map[string]string]("spec.template.metadata.labels", &instance.Spec.Template.ObjectMeta.Labels, &desired.Spec.Template.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("spec.template.metadata.annotations", &instance.Spec.Template.ObjectMeta.Annotations, &desired.Spec.Template.ObjectMeta.Annotations), - property.NewChangeSet[corev1.PodSpec]("spec.template.spec", &instance.Spec.Template.Spec, &desired.Spec.Template.Spec), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("resource updated") - } - - return nil -} - -// reconcileRolloutTriggers modifies the StatefulSet with the appropriate rollout triggers (annotations) -func (sst StatefulSetTemplate) reconcileRolloutTriggers(ctx context.Context, cl client.Client, ss *appsv1.StatefulSet) error { - - if ss.Spec.Template.ObjectMeta.Annotations == nil { - ss.Spec.Template.ObjectMeta.Annotations = map[string]string{} - } - - for _, trigger := range sst.RolloutTriggers { - hash, err := trigger.GetHash(ctx, cl, ss.GetNamespace()) - if err != nil { - return err - } - ss.Spec.Template.ObjectMeta.Annotations[trigger.GetAnnotationKey()] = hash - } - - return nil -} diff --git a/resources/task.go b/resources/task.go deleted file mode 100644 index d60903f..0000000 --- a/resources/task.go +++ /dev/null @@ -1,97 +0,0 @@ -package resources - -import ( - "context" - "fmt" - - "github.com/3scale-ops/basereconciler/property" - "github.com/3scale-ops/basereconciler/reconciler" - pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var _ reconciler.Resource = TaskTemplate{} - -// TaskTemplate has methods to generate and reconcile a Task -type TaskTemplate struct { - Template func() *pipelinev1beta1.Task - IsEnabled bool -} - -// Build returns a Task resource -func (tt TaskTemplate) Build(ctx context.Context, cl client.Client) (client.Object, error) { - return tt.Template().DeepCopy(), nil -} - -// Enabled indicates if the resource should be present or not -func (tt TaskTemplate) Enabled() bool { - return tt.IsEnabled -} - -// ResourceReconciler implements a generic reconciler for Task resources -func (tt TaskTemplate) ResourceReconciler(ctx context.Context, cl client.Client, obj client.Object) error { - logger := log.FromContext(ctx, "kind", "Task", "resource", obj.GetName()) - - needsUpdate := false - desired := obj.(*pipelinev1beta1.Task) - - instance := &pipelinev1beta1.Task{} - err := cl.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, instance) - if err != nil { - if errors.IsNotFound(err) { - - if tt.Enabled() { - err = cl.Create(ctx, desired) - if err != nil { - return fmt.Errorf("unable to create object: " + err.Error()) - } - logger.Info("resource created") - return nil - - } else { - return nil - } - } - - return err - } - - /* Delete and return if not enabled */ - if !tt.Enabled() { - err := cl.Delete(ctx, instance) - if err != nil { - return fmt.Errorf("unable to delete object: " + err.Error()) - } - logger.Info("resource deleted") - return nil - } - - /* Ensure the resource is in its desired state */ - needsUpdate = property.EnsureDesired(logger, - property.NewChangeSet[map[string]string]("metadata.labels", &instance.ObjectMeta.Labels, &desired.ObjectMeta.Labels), - property.NewChangeSet[map[string]string]("metadata.annotations", &instance.ObjectMeta.Annotations, &desired.ObjectMeta.Annotations), - property.NewChangeSet[string]("spec.displayName", &instance.Spec.DisplayName, &desired.Spec.DisplayName), - property.NewChangeSet[string]("spec.description", &instance.Spec.Description, &desired.Spec.Description), - property.NewChangeSet[pipelinev1beta1.ParamSpecs]("spec.params", &instance.Spec.Params, &desired.Spec.Params), - property.NewChangeSet[[]pipelinev1beta1.Step]("spec.steps", &instance.Spec.Steps, &desired.Spec.Steps), - property.NewChangeSet[pipelinev1beta1.StepTemplate]("spec.stepTemplate", instance.Spec.StepTemplate, desired.Spec.StepTemplate), - property.NewChangeSet[[]corev1.Volume]("spec.volumes", &instance.Spec.Volumes, &desired.Spec.Volumes), - property.NewChangeSet[[]pipelinev1beta1.Sidecar]("spec.sidecars", &instance.Spec.Sidecars, &desired.Spec.Sidecars), - property.NewChangeSet[[]pipelinev1beta1.WorkspaceDeclaration]("spec.workspaces", &instance.Spec.Workspaces, &desired.Spec.Workspaces), - property.NewChangeSet[[]pipelinev1beta1.TaskResult]("spec.results", &instance.Spec.Results, &desired.Spec.Results), - ) - - if needsUpdate { - err := cl.Update(ctx, instance) - if err != nil { - return err - } - logger.Info("Resource updated") - } - - return nil -} diff --git a/test/api/v1alpha1/example.com_tests.yaml b/test/api/v1alpha1/example.com_tests.yaml index 82c61c8..342ae93 100644 --- a/test/api/v1alpha1/example.com_tests.yaml +++ b/test/api/v1alpha1/example.com_tests.yaml @@ -39,6 +39,8 @@ spec: type: boolean pdb: type: boolean + pruneService: + type: boolean serviceAnnotations: additionalProperties: type: string diff --git a/test/api/v1alpha1/test_types.go b/test/api/v1alpha1/test_types.go index bb055cb..65602bf 100644 --- a/test/api/v1alpha1/test_types.go +++ b/test/api/v1alpha1/test_types.go @@ -47,6 +47,8 @@ type TestSpec struct { HPA *bool `json:"hpa,omitempty"` // +optional ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"` + // +optional + PruneService *bool `json:"pruneService,omitempty"` } // TestStatus defines the observed state of Test diff --git a/test/api/v1alpha1/zz_generated.deepcopy.go b/test/api/v1alpha1/zz_generated.deepcopy.go index da3910a..82c6505 100644 --- a/test/api/v1alpha1/zz_generated.deepcopy.go +++ b/test/api/v1alpha1/zz_generated.deepcopy.go @@ -105,6 +105,11 @@ func (in *TestSpec) DeepCopyInto(out *TestSpec) { (*out)[key] = val } } + if in.PruneService != nil { + in, out := &in.PruneService, &out.PruneService + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestSpec. diff --git a/test/external-apis/external-secrets.io_externalsecrets.yaml b/test/external-apis/external-secrets.io_externalsecrets.yaml deleted file mode 100644 index 0a740ab..0000000 --- a/test/external-apis/external-secrets.io_externalsecrets.yaml +++ /dev/null @@ -1,695 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null - name: externalsecrets.external-secrets.io -spec: - group: external-secrets.io - names: - categories: - - externalsecrets - kind: ExternalSecret - listKind: ExternalSecretList - plural: externalsecrets - shortNames: - - es - singular: externalsecret - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.secretStoreRef.name - name: Store - type: string - - jsonPath: .spec.refreshInterval - name: Refresh Interval - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].reason - name: Status - type: string - deprecated: true - name: v1alpha1 - schema: - openAPIV3Schema: - description: ExternalSecret is the Schema for the external-secrets API. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: ExternalSecretSpec defines the desired state of ExternalSecret. - properties: - data: - description: Data defines the connection between the Kubernetes Secret - keys and the Provider data - items: - description: ExternalSecretData defines the connection between the - Kubernetes Secret key (spec.data.) and the Provider data. - properties: - remoteRef: - description: ExternalSecretDataRemoteRef defines Provider data - location. - properties: - conversionStrategy: - default: Default - description: Used to define a conversion Strategy - type: string - key: - description: Key is the key used in the Provider, mandatory - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - secretKey: - type: string - required: - - remoteRef - - secretKey - type: object - type: array - dataFrom: - description: DataFrom is used to fetch all properties from a specific - Provider data If multiple entries are specified, the Secret keys - are merged in the specified order - items: - description: ExternalSecretDataRemoteRef defines Provider data location. - properties: - conversionStrategy: - default: Default - description: Used to define a conversion Strategy - type: string - key: - description: Key is the key used in the Provider, mandatory - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - type: array - refreshInterval: - default: 1h - description: RefreshInterval is the amount of time before the values - are read again from the SecretStore provider Valid time units are - "ns", "us" (or "µs"), "ms", "s", "m", "h" May be set to zero to - fetch and create it once. Defaults to 1h. - type: string - secretStoreRef: - description: SecretStoreRef defines which SecretStore to fetch the - ExternalSecret data. - properties: - kind: - description: Kind of the SecretStore resource (SecretStore or - ClusterSecretStore) Defaults to `SecretStore` - type: string - name: - description: Name of the SecretStore resource - type: string - required: - - name - type: object - target: - description: ExternalSecretTarget defines the Kubernetes Secret to - be created There can be only one target per ExternalSecret. - properties: - creationPolicy: - default: Owner - description: CreationPolicy defines rules on how to create the - resulting Secret Defaults to 'Owner' - type: string - immutable: - description: Immutable defines if the final secret will be immutable - type: boolean - name: - description: Name defines the name of the Secret resource to be - managed This field is immutable Defaults to the .metadata.name - of the ExternalSecret resource - type: string - template: - description: Template defines a blueprint for the created Secret - resource. - properties: - data: - additionalProperties: - type: string - type: object - engineVersion: - default: v1 - description: EngineVersion specifies the template engine version - that should be used to compile/execute the template specified - in .data and .templateFrom[]. - type: string - metadata: - description: ExternalSecretTemplateMetadata defines metadata - fields for the Secret blueprint. - properties: - annotations: - additionalProperties: - type: string - type: object - labels: - additionalProperties: - type: string - type: object - type: object - templateFrom: - items: - maxProperties: 1 - minProperties: 1 - properties: - configMap: - properties: - items: - items: - properties: - key: - type: string - required: - - key - type: object - type: array - name: - type: string - required: - - items - - name - type: object - secret: - properties: - items: - items: - properties: - key: - type: string - required: - - key - type: object - type: array - name: - type: string - required: - - items - - name - type: object - type: object - type: array - type: - type: string - type: object - type: object - required: - - secretStoreRef - - target - type: object - status: - properties: - conditions: - items: - properties: - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - type: string - required: - - status - - type - type: object - type: array - refreshTime: - description: refreshTime is the time and date the external secret - was fetched and the target secret updated - format: date-time - nullable: true - type: string - syncedResourceVersion: - description: SyncedResourceVersion keeps track of the last synced - version - type: string - type: object - type: object - served: true - storage: false - subresources: - status: {} - - additionalPrinterColumns: - - jsonPath: .spec.secretStoreRef.name - name: Store - type: string - - jsonPath: .spec.refreshInterval - name: Refresh Interval - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].reason - name: Status - type: string - - jsonPath: .status.conditions[?(@.type=="Ready")].status - name: Ready - type: string - name: v1beta1 - schema: - openAPIV3Schema: - description: ExternalSecret is the Schema for the external-secrets API. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: ExternalSecretSpec defines the desired state of ExternalSecret. - properties: - data: - description: Data defines the connection between the Kubernetes Secret - keys and the Provider data - items: - description: ExternalSecretData defines the connection between the - Kubernetes Secret key (spec.data.) and the Provider data. - properties: - remoteRef: - description: RemoteRef points to the remote secret and defines - which secret (version/property/..) to fetch. - properties: - conversionStrategy: - default: Default - description: Used to define a conversion Strategy - type: string - decodingStrategy: - default: None - description: Used to define a decoding Strategy - type: string - key: - description: Key is the key used in the Provider, mandatory - type: string - metadataPolicy: - description: Policy for fetching tags/labels from provider - secrets, possible options are Fetch, None. Defaults to - None - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - secretKey: - description: SecretKey defines the key in which the controller - stores the value. This is the key in the Kind=Secret - type: string - sourceRef: - description: SourceRef allows you to override the source from - which the value will pulled from. - maxProperties: 1 - properties: - generatorRef: - description: GeneratorRef points to a generator custom resource - in - properties: - apiVersion: - default: generators.external-secrets.io/v1alpha1 - description: Specify the apiVersion of the generator - resource - type: string - kind: - description: Specify the Kind of the resource, e.g. - Password, ACRAccessToken etc. - type: string - name: - description: Specify the name of the generator resource - type: string - required: - - kind - - name - type: object - storeRef: - description: SecretStoreRef defines which SecretStore to - fetch the ExternalSecret data. - properties: - kind: - description: Kind of the SecretStore resource (SecretStore - or ClusterSecretStore) Defaults to `SecretStore` - type: string - name: - description: Name of the SecretStore resource - type: string - required: - - name - type: object - type: object - required: - - remoteRef - - secretKey - type: object - type: array - dataFrom: - description: DataFrom is used to fetch all properties from a specific - Provider data If multiple entries are specified, the Secret keys - are merged in the specified order - items: - properties: - extract: - description: 'Used to extract multiple key/value pairs from - one secret Note: Extract does not support sourceRef.Generator - or sourceRef.GeneratorRef.' - properties: - conversionStrategy: - default: Default - description: Used to define a conversion Strategy - type: string - decodingStrategy: - default: None - description: Used to define a decoding Strategy - type: string - key: - description: Key is the key used in the Provider, mandatory - type: string - metadataPolicy: - description: Policy for fetching tags/labels from provider - secrets, possible options are Fetch, None. Defaults to - None - type: string - property: - description: Used to select a specific property of the Provider - value (if a map), if supported - type: string - version: - description: Used to select a specific version of the Provider - value, if supported - type: string - required: - - key - type: object - find: - description: 'Used to find secrets based on tags or regular - expressions Note: Find does not support sourceRef.Generator - or sourceRef.GeneratorRef.' - properties: - conversionStrategy: - default: Default - description: Used to define a conversion Strategy - type: string - decodingStrategy: - default: None - description: Used to define a decoding Strategy - type: string - name: - description: Finds secrets based on the name. - properties: - regexp: - description: Finds secrets base - type: string - type: object - path: - description: A root path to start the find operations. - type: string - tags: - additionalProperties: - type: string - description: Find secrets based on tags. - type: object - type: object - rewrite: - description: Used to rewrite secret Keys after getting them - from the secret Provider Multiple Rewrite operations can be - provided. They are applied in a layered order (first to last) - items: - properties: - regexp: - description: Used to rewrite with regular expressions. - The resulting key will be the output of a regexp.ReplaceAll - operation. - properties: - source: - description: Used to define the regular expression - of a re.Compiler. - type: string - target: - description: Used to define the target pattern of - a ReplaceAll operation. - type: string - required: - - source - - target - type: object - type: object - type: array - sourceRef: - description: SourceRef points to a store or generator which - contains secret values ready to use. Use this in combination - with Extract or Find pull values out of a specific SecretStore. - When sourceRef points to a generator Extract or Find is not - supported. The generator returns a static map of values - maxProperties: 1 - properties: - generatorRef: - description: GeneratorRef points to a generator custom resource - in - properties: - apiVersion: - default: generators.external-secrets.io/v1alpha1 - description: Specify the apiVersion of the generator - resource - type: string - kind: - description: Specify the Kind of the resource, e.g. - Password, ACRAccessToken etc. - type: string - name: - description: Specify the name of the generator resource - type: string - required: - - kind - - name - type: object - storeRef: - description: SecretStoreRef defines which SecretStore to - fetch the ExternalSecret data. - properties: - kind: - description: Kind of the SecretStore resource (SecretStore - or ClusterSecretStore) Defaults to `SecretStore` - type: string - name: - description: Name of the SecretStore resource - type: string - required: - - name - type: object - type: object - type: object - type: array - refreshInterval: - default: 1h - description: RefreshInterval is the amount of time before the values - are read again from the SecretStore provider Valid time units are - "ns", "us" (or "µs"), "ms", "s", "m", "h" May be set to zero to - fetch and create it once. Defaults to 1h. - type: string - secretStoreRef: - description: SecretStoreRef defines which SecretStore to fetch the - ExternalSecret data. - properties: - kind: - description: Kind of the SecretStore resource (SecretStore or - ClusterSecretStore) Defaults to `SecretStore` - type: string - name: - description: Name of the SecretStore resource - type: string - required: - - name - type: object - target: - default: - creationPolicy: Owner - deletionPolicy: Retain - description: ExternalSecretTarget defines the Kubernetes Secret to - be created There can be only one target per ExternalSecret. - properties: - creationPolicy: - default: Owner - description: CreationPolicy defines rules on how to create the - resulting Secret Defaults to 'Owner' - enum: - - Owner - - Orphan - - Merge - - None - type: string - deletionPolicy: - default: Retain - description: DeletionPolicy defines rules on how to delete the - resulting Secret Defaults to 'Retain' - enum: - - Delete - - Merge - - Retain - type: string - immutable: - description: Immutable defines if the final secret will be immutable - type: boolean - name: - description: Name defines the name of the Secret resource to be - managed This field is immutable Defaults to the .metadata.name - of the ExternalSecret resource - type: string - template: - description: Template defines a blueprint for the created Secret - resource. - properties: - data: - additionalProperties: - type: string - type: object - engineVersion: - default: v2 - type: string - mergePolicy: - default: Replace - type: string - metadata: - description: ExternalSecretTemplateMetadata defines metadata - fields for the Secret blueprint. - properties: - annotations: - additionalProperties: - type: string - type: object - labels: - additionalProperties: - type: string - type: object - type: object - templateFrom: - items: - properties: - configMap: - properties: - items: - items: - properties: - key: - type: string - templateAs: - default: Values - type: string - required: - - key - type: object - type: array - name: - type: string - required: - - items - - name - type: object - literal: - type: string - secret: - properties: - items: - items: - properties: - key: - type: string - templateAs: - default: Values - type: string - required: - - key - type: object - type: array - name: - type: string - required: - - items - - name - type: object - target: - default: Data - type: string - type: object - type: array - type: - type: string - type: object - type: object - type: object - status: - properties: - conditions: - items: - properties: - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - type: string - required: - - status - - type - type: object - type: array - refreshTime: - description: refreshTime is the time and date the external secret - was fetched and the target secret updated - format: date-time - nullable: true - type: string - syncedResourceVersion: - description: SyncedResourceVersion keeps track of the last synced - version - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} \ No newline at end of file diff --git a/test/external-apis/grafana.integreatly.org_grafanadashboards.yaml b/test/external-apis/grafana.integreatly.org_grafanadashboards.yaml deleted file mode 100644 index a74e977..0000000 --- a/test/external-apis/grafana.integreatly.org_grafanadashboards.yaml +++ /dev/null @@ -1,165 +0,0 @@ - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.6.2 - creationTimestamp: null - name: grafanadashboards.integreatly.org -spec: - group: integreatly.org - names: - kind: GrafanaDashboard - listKind: GrafanaDashboardList - plural: grafanadashboards - singular: grafanadashboard - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: GrafanaDashboard is the Schema for the grafanadashboards API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: GrafanaDashboardSpec defines the desired state of GrafanaDashboard - properties: - configMapRef: - description: ConfigMapRef is a reference to a ConfigMap data field - containing the dashboard's JSON - properties: - key: - description: The key to select. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the ConfigMap or its key must be - defined - type: boolean - required: - - key - type: object - contentCacheDuration: - description: ContentCacheDuration sets how often the operator should - resync with the external source when using the `grafanaCom.id` or - `url` field to specify the source of the dashboard. The default - value is decided by the `dashboardContentCacheDuration` field in - the `Grafana` resource. The default is 0 which is interpreted as - never refetching. - type: string - customFolderName: - type: string - datasources: - items: - properties: - datasourceName: - type: string - inputName: - type: string - required: - - datasourceName - - inputName - type: object - type: array - grafanaCom: - properties: - id: - type: integer - revision: - type: integer - required: - - id - type: object - gzipConfigMapRef: - description: GzipConfigMapRef is a reference to a ConfigMap binaryData - field containing the dashboard's JSON, compressed with Gzip. - properties: - key: - description: The key to select. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the ConfigMap or its key must be - defined - type: boolean - required: - - key - type: object - gzipJson: - description: GzipJson the dashboard's JSON compressed with Gzip. Base64-encoded - when in YAML. - format: byte - type: string - json: - description: Json is the dashboard's JSON - type: string - jsonnet: - type: string - plugins: - items: - description: GrafanaPlugin contains information about a single plugin - properties: - name: - type: string - version: - type: string - required: - - name - - version - type: object - type: array - url: - type: string - type: object - status: - properties: - contentCache: - format: byte - type: string - contentTimestamp: - format: date-time - type: string - contentUrl: - type: string - error: - properties: - code: - type: integer - error: - type: string - retries: - type: integer - required: - - code - - error - type: object - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] \ No newline at end of file diff --git a/test/external-apis/podmonitors.monitoring.coreos.com.yaml b/test/external-apis/podmonitors.monitoring.coreos.com.yaml deleted file mode 100644 index db04fdb..0000000 --- a/test/external-apis/podmonitors.monitoring.coreos.com.yaml +++ /dev/null @@ -1,353 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.4.1 - creationTimestamp: null - name: podmonitors.monitoring.coreos.com -spec: - group: monitoring.coreos.com - names: - kind: PodMonitor - listKind: PodMonitorList - plural: podmonitors - singular: podmonitor - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: PodMonitor defines monitoring for a set of pods. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Specification of desired Pod selection for target discovery by Prometheus. - properties: - jobLabel: - description: The label to use to retrieve the job name from. - type: string - namespaceSelector: - description: Selector to select which namespaces the Endpoints objects are discovered from. - properties: - any: - description: Boolean describing whether all namespaces are selected in contrast to a list restricting them. - type: boolean - matchNames: - description: List of namespace names. - items: - type: string - type: array - type: object - podMetricsEndpoints: - description: A list of endpoints allowed as part of this PodMonitor. - items: - description: PodMetricsEndpoint defines a scrapeable endpoint of a Kubernetes Pod serving Prometheus metrics. - properties: - basicAuth: - description: 'BasicAuth allow an endpoint to authenticate over basic authentication. More info: https://prometheus.io/docs/operating/configuration/#endpoint' - properties: - password: - description: The secret in the service monitor namespace that contains the password for authentication. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - username: - description: The secret in the service monitor namespace that contains the username for authentication. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - type: object - bearerTokenSecret: - description: Secret to mount to read bearer token for scraping targets. The secret needs to be in the same namespace as the pod monitor and accessible by the Prometheus Operator. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - honorLabels: - description: HonorLabels chooses the metric's labels on collisions with target labels. - type: boolean - honorTimestamps: - description: HonorTimestamps controls whether Prometheus respects the timestamps present in scraped data. - type: boolean - interval: - description: Interval at which metrics should be scraped - type: string - metricRelabelings: - description: MetricRelabelConfigs to apply to samples before ingestion. - items: - description: 'RelabelConfig allows dynamic rewriting of the label set, being applied to samples before ingestion. It defines ``-section of Prometheus configuration. More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs' - properties: - action: - description: Action to perform based on regex matching. Default is 'replace' - type: string - modulus: - description: Modulus to take of the hash of the source label values. - format: int64 - type: integer - regex: - description: Regular expression against which the extracted value is matched. Default is '(.*)' - type: string - replacement: - description: Replacement value against which a regex replace is performed if the regular expression matches. Regex capture groups are available. Default is '$1' - type: string - separator: - description: Separator placed between concatenated source label values. default is ';'. - type: string - sourceLabels: - description: The source labels select values from existing labels. Their content is concatenated using the configured separator and matched against the configured regular expression for the replace, keep, and drop actions. - items: - type: string - type: array - targetLabel: - description: Label to which the resulting value is written in a replace action. It is mandatory for replace actions. Regex capture groups are available. - type: string - type: object - type: array - params: - additionalProperties: - items: - type: string - type: array - description: Optional HTTP URL parameters - type: object - path: - description: HTTP path to scrape for metrics. - type: string - port: - description: Name of the pod port this endpoint refers to. Mutually exclusive with targetPort. - type: string - proxyUrl: - description: ProxyURL eg http://proxyserver:2195 Directs scrapes to proxy through this endpoint. - type: string - relabelings: - description: 'RelabelConfigs to apply to samples before ingestion. More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config' - items: - description: 'RelabelConfig allows dynamic rewriting of the label set, being applied to samples before ingestion. It defines ``-section of Prometheus configuration. More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs' - properties: - action: - description: Action to perform based on regex matching. Default is 'replace' - type: string - modulus: - description: Modulus to take of the hash of the source label values. - format: int64 - type: integer - regex: - description: Regular expression against which the extracted value is matched. Default is '(.*)' - type: string - replacement: - description: Replacement value against which a regex replace is performed if the regular expression matches. Regex capture groups are available. Default is '$1' - type: string - separator: - description: Separator placed between concatenated source label values. default is ';'. - type: string - sourceLabels: - description: The source labels select values from existing labels. Their content is concatenated using the configured separator and matched against the configured regular expression for the replace, keep, and drop actions. - items: - type: string - type: array - targetLabel: - description: Label to which the resulting value is written in a replace action. It is mandatory for replace actions. Regex capture groups are available. - type: string - type: object - type: array - scheme: - description: HTTP scheme to use for scraping. - type: string - scrapeTimeout: - description: Timeout after which the scrape is ended - type: string - targetPort: - anyOf: - - type: integer - - type: string - description: 'Deprecated: Use ''port'' instead.' - x-kubernetes-int-or-string: true - tlsConfig: - description: TLS configuration to use when scraping the endpoint. - properties: - ca: - description: Struct containing the CA cert to use for the targets. - properties: - configMap: - description: ConfigMap containing data to use for the targets. - properties: - key: - description: The key to select. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - secret: - description: Secret containing data to use for the targets. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - type: object - cert: - description: Struct containing the client cert file for the targets. - properties: - configMap: - description: ConfigMap containing data to use for the targets. - properties: - key: - description: The key to select. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - secret: - description: Secret containing data to use for the targets. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - type: object - insecureSkipVerify: - description: Disable target certificate validation. - type: boolean - keySecret: - description: Secret containing the client key file for the targets. - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - serverName: - description: Used to verify the hostname for the targets. - type: string - type: object - type: object - type: array - podTargetLabels: - description: PodTargetLabels transfers labels on the Kubernetes Pod onto the target. - items: - type: string - type: array - sampleLimit: - description: SampleLimit defines per-scrape limit on number of scraped samples that will be accepted. - format: int64 - type: integer - selector: - description: Selector to select Pod objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - targetLimit: - description: TargetLimit defines a limit on the number of scraped targets that will be accepted. - format: int64 - type: integer - required: - - podMetricsEndpoints - - selector - type: object - required: - - spec - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] \ No newline at end of file diff --git a/test/suite_test.go b/test/suite_test.go index 06d6f48..42c46f7 100644 --- a/test/suite_test.go +++ b/test/suite_test.go @@ -39,9 +39,6 @@ import ( // +kubebuilder:scaffold:imports "github.com/3scale-ops/basereconciler/test/api/v1alpha1" - externalsecretsv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" - grafanav1alpha1 "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -64,15 +61,14 @@ func TestAPIs(t *testing.T) { } var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(false))) + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("api", "v1alpha1"), - filepath.Join("external-apis"), }, - // UseExistingCluster: pointer.Bool(true), + // UseExistingCluster: util.Pointer(true), } nBig, err := rand.Int(rand.Reader, big.NewInt(1000000)) @@ -84,9 +80,6 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme)) - utilruntime.Must(externalsecretsv1beta1.AddToScheme(scheme.Scheme)) - utilruntime.Must(grafanav1alpha1.AddToScheme(scheme.Scheme)) - utilruntime.Must(monitoringv1.AddToScheme(scheme.Scheme)) mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, diff --git a/test/test_controller.go b/test/test_controller.go index 0753844..2033f80 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -18,15 +18,13 @@ package test import ( "context" - "time" + "github.com/3scale-ops/basereconciler/mutators" "github.com/3scale-ops/basereconciler/reconciler" - "github.com/3scale-ops/basereconciler/resources" + "github.com/3scale-ops/basereconciler/resource" "github.com/3scale-ops/basereconciler/test/api/v1alpha1" - externalsecretsv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + "github.com/3scale-ops/basereconciler/util" "github.com/go-logr/logr" - grafanav1alpha1 "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -36,23 +34,11 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/source" ) -func init() { - reconciler.Config.AnnotationsDomain = "example.com" - reconciler.Config.ResourcePruner = true - reconciler.Config.ManagedTypes = reconciler.NewManagedTypes(). - Register(&corev1.ServiceList{}). - Register(&appsv1.DeploymentList{}). - Register(&externalsecretsv1beta1.ExternalSecretList{}). - Register(&grafanav1alpha1.GrafanaDashboardList{}). - Register(&autoscalingv2.HorizontalPodAutoscalerList{}). - Register(&policyv1.PodDisruptionBudgetList{}). - Register(&monitoringv1.PodMonitorList{}) -} - // Reconciler reconciles a Test object // +kubebuilder:object:generate=false type Reconciler struct { @@ -73,42 +59,83 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return *result, err } - err = r.ReconcileOwnedResources(ctx, instance, []reconciler.Resource{ - resources.DeploymentTemplate{ - Template: deployment(req.Namespace), - RolloutTriggers: []resources.RolloutTrigger{{ - Name: "secret", - SecretName: pointer.String("secret"), - }}, - EnforceReplicas: true, + resources := []resource.TemplateInterface{ + &resource.Template[*appsv1.Deployment]{ + TemplateBuilder: deployment(req.Namespace), IsEnabled: true, + EnsureProperties: []resource.Property{ + "metadata.annotations", + "metadata.labels", + "spec.minReadySeconds", + "spec.replicas", + "spec.selector", + "spec.strategy", + "spec.template.metadata", + "spec.template.spec", + }, + IgnoreProperties: []resource.Property{ + "metadata.annotations['deployment.kubernetes.io/revision']", + "spec.template.spec.dnsPolicy", + "spec.template.spec.schedulerName", + "spec.template.spec.restartPolicy", + "spec.template.spec.securityContext", + "spec.template.spec.terminationGracePeriodSeconds", + "spec.template.spec.containers[*].terminationMessagePath", + "spec.template.spec.containers[*].terminationMessagePolicy", + }, + TemplateMutations: []resource.TemplateMutationFunction{ + mutators.SetDeploymentReplicas(true), + mutators.RolloutTrigger{ + Name: "secret", + SecretName: pointer.String("secret"), + }.Add("example.com"), + }, }, - resources.ExternalSecretTemplate{ - Template: externalSecret(req.Namespace), - IsEnabled: true, - }, - resources.ServiceTemplate{ - Template: service(req.Namespace, instance.Spec.ServiceAnnotations), - IsEnabled: true, - }, - resources.PodDisruptionBudgetTemplate{ - Template: pdb(req.Namespace), - IsEnabled: instance.Spec.PDB != nil && *instance.Spec.PDB, - }, - resources.HorizontalPodAutoscalerTemplate{ - Template: hpa(req.Namespace), - IsEnabled: instance.Spec.HPA != nil && *instance.Spec.HPA, - }, - resources.PodMonitorTemplate{ - Template: podmonitor(req.Namespace), - IsEnabled: instance.Spec.HPA != nil && *instance.Spec.HPA, + + &resource.Template[*autoscalingv2.HorizontalPodAutoscaler]{ + TemplateBuilder: hpa(req.Namespace), + IsEnabled: instance.Spec.HPA != nil && *instance.Spec.HPA, + EnsureProperties: []resource.Property{ + "metadata.annotations", + "metadata.labels", + "spec.scaleTargetRef", + "spec.minReplicas", + "spec.maxReplicas", + "spec.metrics", + }, }, - resources.GrafanaDashboardTemplate{ - Template: dashboard(req.Namespace), - IsEnabled: instance.Spec.HPA != nil && *instance.Spec.HPA, + &resource.Template[*policyv1.PodDisruptionBudget]{ + TemplateBuilder: pdb(req.Namespace), + IsEnabled: instance.Spec.PDB != nil && *instance.Spec.PDB, + EnsureProperties: []resource.Property{ + "metadata.annotations", + "metadata.labels", + "spec.maxUnavailable", + "spec.minAvailable", + "spec.selector", + }, }, - }) + } + + if instance.Spec.PruneService == nil || !*instance.Spec.PruneService { + resources = append(resources, &resource.Template[*corev1.Service]{ + TemplateBuilder: service(req.Namespace, instance.Spec.ServiceAnnotations), + IsEnabled: true, + EnsureProperties: []resource.Property{ + "metadata.annotations", + "metadata.labels", + "spec.type", + "spec.selector", + "spec.ports", + "spec.clusterIP", + }, + TemplateMutations: []resource.TemplateMutationFunction{ + mutators.SetServiceLiveValues(), + }, + }) + } + err = r.ReconcileOwnedResources(ctx, instance, resources) if err != nil { logger.Error(err, "unable to reconcile owned resources") return ctrl.Result{}, err @@ -132,14 +159,13 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.Service{}). Owns(&policyv1.PodDisruptionBudget{}). Owns(&autoscalingv2.HorizontalPodAutoscaler{}). - Owns(&externalsecretsv1beta1.ExternalSecret{}). Watches(&source.Kind{Type: &corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: "Secret"}}}, r.SecretEventHandler(&v1alpha1.TestList{}, r.Log)). Complete(r) } -func deployment(namespace string) func() *appsv1.Deployment { - return func() *appsv1.Deployment { +func deployment(namespace string) resource.TemplateBuilderFunction[*appsv1.Deployment] { + return func(client.Object) (*appsv1.Deployment, error) { dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment", @@ -147,6 +173,12 @@ func deployment(namespace string) func() *appsv1.Deployment { }, Spec: appsv1.DeploymentSpec{ Replicas: pointer.Int32(1), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: util.Pointer(intstr.FromString("25%")), + MaxUnavailable: util.Pointer(intstr.FromString("25%"))}, + }, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"selector": "deployment"}, }, @@ -157,9 +189,10 @@ func deployment(namespace string) func() *appsv1.Deployment { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "container", - Image: "example.com:latest", - Resources: corev1.ResourceRequirements{}, + Name: "container", + Image: "example.com:latest", + ImagePullPolicy: corev1.PullAlways, + Resources: corev1.ResourceRequirements{}, }, }, }, @@ -167,12 +200,12 @@ func deployment(namespace string) func() *appsv1.Deployment { }, } - return dep + return dep, nil } } -func service(namespace string, annotations map[string]string) func() *corev1.Service { - return func() *corev1.Service { +func service(namespace string, annotations map[string]string) resource.TemplateBuilderFunction[*corev1.Service] { + return func(client.Object) (*corev1.Service, error) { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service", @@ -180,45 +213,17 @@ func service(namespace string, annotations map[string]string) func() *corev1.Ser Annotations: annotations, }, Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeLoadBalancer, - ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeCluster, - SessionAffinity: corev1.ServiceAffinityNone, + Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{{ Name: "port", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP}}, Selector: map[string]string{"selector": "deployment"}, }, - } - } -} - -func externalSecret(namespace string) func() *externalsecretsv1beta1.ExternalSecret { - - return func() *externalsecretsv1beta1.ExternalSecret { - return &externalsecretsv1beta1.ExternalSecret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: namespace, - }, - Spec: externalsecretsv1beta1.ExternalSecretSpec{ - SecretStoreRef: externalsecretsv1beta1.SecretStoreRef{Name: "vault-mgmt", Kind: "ClusterSecretStore"}, - Target: externalsecretsv1beta1.ExternalSecretTarget{Name: "secret"}, - RefreshInterval: &metav1.Duration{Duration: 60 * time.Second}, - Data: []externalsecretsv1beta1.ExternalSecretData{ - { - SecretKey: "KEY", - RemoteRef: externalsecretsv1beta1.ExternalSecretDataRemoteRef{ - Key: "vault-path", - Property: "vault-key", - }, - }, - }, - }, - } + }, nil } } -func hpa(namespace string) func() *autoscalingv2.HorizontalPodAutoscaler { - return func() *autoscalingv2.HorizontalPodAutoscaler { +func hpa(namespace string) resource.TemplateBuilderFunction[*autoscalingv2.HorizontalPodAutoscaler] { + return func(client.Object) (*autoscalingv2.HorizontalPodAutoscaler, error) { return &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "hpa", @@ -232,13 +237,23 @@ func hpa(namespace string) func() *autoscalingv2.HorizontalPodAutoscaler { }, MinReplicas: pointer.Int32(1), MaxReplicas: 1, + Metrics: []autoscalingv2.MetricSpec{{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: util.Pointer(int32(90)), + }, + }, + }}, }, - } + }, nil } } -func pdb(namespace string) func() *policyv1.PodDisruptionBudget { - return func() *policyv1.PodDisruptionBudget { +func pdb(namespace string) resource.TemplateBuilderFunction[*policyv1.PodDisruptionBudget] { + return func(client.Object) (*policyv1.PodDisruptionBudget, error) { return &policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ @@ -251,40 +266,6 @@ func pdb(namespace string) func() *policyv1.PodDisruptionBudget { }, MinAvailable: intstr.ValueOrDefault(nil, intstr.FromInt(1)), }, - } - } -} - -func podmonitor(namespace string) func() *monitoringv1.PodMonitor { - return func() *monitoringv1.PodMonitor { - - return &monitoringv1.PodMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pm", - Namespace: namespace, - }, - Spec: monitoringv1.PodMonitorSpec{ - JobLabel: "job", - PodMetricsEndpoints: []monitoringv1.PodMetricsEndpoint{}, - Selector: metav1.LabelSelector{ - MatchLabels: map[string]string{"selector": "deployment"}, - }, - }, - } - } -} - -func dashboard(namespace string) func() *grafanav1alpha1.GrafanaDashboard { - return func() *grafanav1alpha1.GrafanaDashboard { - - return &grafanav1alpha1.GrafanaDashboard{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dashboard", - Namespace: namespace, - }, - Spec: grafanav1alpha1.GrafanaDashboardSpec{ - Json: "{}", - }, - } + }, nil } } diff --git a/test/test_controller_suite_test.go b/test/test_controller_suite_test.go index 6448315..1b35f14 100644 --- a/test/test_controller_suite_test.go +++ b/test/test_controller_suite_test.go @@ -5,11 +5,8 @@ import ( "github.com/3scale-ops/basereconciler/test/api/v1alpha1" "github.com/3scale-ops/basereconciler/util" - externalsecretsv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" - grafanav1alpha1 "github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -24,27 +21,27 @@ import ( var _ = Describe("Test controller", func() { var namespace string var instance *v1alpha1.Test + var resources []client.Object BeforeEach(func() { // Create a namespace for each block namespace = "test-ns-" + nameGenerator.Generate() - - // Add any setup steps that needs to be executed before each test - testNamespace := &corev1.Namespace{ - TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, - ObjectMeta: metav1.ObjectMeta{Name: namespace}, - } - - err := k8sClient.Create(context.Background(), testNamespace) + n := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + err := k8sClient.Create(context.Background(), n) Expect(err).ToNot(HaveOccurred()) - n := &corev1.Namespace{} Eventually(func() error { return k8sClient.Get(context.Background(), types.NamespacedName{Name: namespace}, n) }, timeout, poll).ShouldNot(HaveOccurred()) }) + AfterEach(func() { + n := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + err := k8sClient.Delete(context.Background(), n, client.PropagationPolicy(metav1.DeletePropagationForeground)) + Expect(err).ToNot(HaveOccurred()) + }) + Context("Creates resources", func() { BeforeEach(func() { @@ -61,85 +58,29 @@ var _ = Describe("Test controller", func() { Eventually(func() error { return k8sClient.Get(context.Background(), types.NamespacedName{Name: "instance", Namespace: namespace}, instance) }, timeout, poll).ShouldNot(HaveOccurred()) - }) - - It("creates the required resources", func() { - - dep := &appsv1.Deployment{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "deployment", Namespace: namespace}, - dep, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - svc := &corev1.Service{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "service", Namespace: namespace}, - svc, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - es := &externalsecretsv1beta1.ExternalSecret{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "secret", Namespace: namespace}, - es, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - pdb := &policyv1.PodDisruptionBudget{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "pdb", Namespace: namespace}, - pdb, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - hpa := &autoscalingv2.HorizontalPodAutoscaler{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "hpa", Namespace: namespace}, - hpa, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + By("checking that owned resources are created") + resources = []client.Object{ + &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deployment", Namespace: namespace}}, + &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: namespace}}, + &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{Name: "pdb", Namespace: namespace}}, + &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{Name: "hpa", Namespace: namespace}}, + } - pm := &monitoringv1.PodMonitor{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "pm", Namespace: namespace}, - pm, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + for _, res := range resources { + Eventually(func() error { + return k8sClient.Get(context.Background(), util.ObjectKey(res), res) + }, timeout, poll).ShouldNot(HaveOccurred()) + } + }) - dashboard := &grafanav1alpha1.GrafanaDashboard{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "dashboard", Namespace: namespace}, - dashboard, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + AfterEach(func() { + k8sClient.Delete(context.Background(), instance, client.PropagationPolicy(metav1.DeletePropagationForeground)) }) It("triggers a Deployment rollout on Secret contents change", func() { - dep := &appsv1.Deployment{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "deployment", Namespace: namespace}, - dep, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - + dep := resources[0].(*appsv1.Deployment) // Annotations should be empty when Secret does not exists value, ok := dep.Spec.Template.ObjectMeta.Annotations["example.com/secret.secret-hash"] Expect(ok).To(BeTrue()) @@ -186,131 +127,56 @@ var _ = Describe("Test controller", func() { }) It("deletes specific resources when disabled", func() { - // Wait for resources to be created - pdb := &policyv1.PodDisruptionBudget{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "pdb", Namespace: namespace}, - pdb, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - hpa := &autoscalingv2.HorizontalPodAutoscaler{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "hpa", Namespace: namespace}, - hpa, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + pdb := resources[2].(*policyv1.PodDisruptionBudget) + hpa := resources[3].(*autoscalingv2.HorizontalPodAutoscaler) // disable pdb and hpa - instance = &v1alpha1.Test{} - Eventually(func() error { - err := k8sClient.Get(context.Background(), types.NamespacedName{Name: "instance", Namespace: namespace}, instance) - if err != nil { - return err - } - instance.Spec.PDB = pointer.Bool(false) - instance.Spec.HPA = pointer.Bool(false) - err = k8sClient.Update(context.Background(), instance) - return err - - }, timeout, poll).ShouldNot(HaveOccurred()) + patch := client.MergeFrom(instance.DeepCopy()) + instance.Spec.PDB = util.Pointer(false) + instance.Spec.HPA = util.Pointer(false) + err := k8sClient.Patch(context.Background(), instance, patch) + Expect(err).ToNot(HaveOccurred()) Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "pdb", Namespace: namespace}, - pdb, - ) + return k8sClient.Get(context.Background(), util.ObjectKey(pdb), pdb) }, timeout, poll).Should(HaveOccurred()) Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "hpa", Namespace: namespace}, - hpa, - ) + return k8sClient.Get(context.Background(), util.ObjectKey(hpa), hpa) }, timeout, poll).Should(HaveOccurred()) }) - It("deletes all owned resources when custom resource is deleted", func() { - // Wait for all resources to be created - - dep := &appsv1.Deployment{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "deployment", Namespace: namespace}, - dep, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - svc := &corev1.Service{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "service", Namespace: namespace}, - svc, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - es := &externalsecretsv1beta1.ExternalSecret{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "secret", Namespace: namespace}, - es, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + It("updates service annotations", func() { + svc := resources[1].(*corev1.Service) - // Delete the custom resource - err := k8sClient.Delete(context.Background(), instance) + patch := client.MergeFrom(instance.DeepCopy()) + instance.Spec.ServiceAnnotations = map[string]string{"key": "value"} + err := k8sClient.Patch(context.Background(), instance, patch) Expect(err).ToNot(HaveOccurred()) Eventually(func() bool { - err := k8sClient.Get(context.Background(), types.NamespacedName{Name: "instance", Namespace: namespace}, instance) - if err != nil && errors.IsNotFound(err) { - return true + if err := k8sClient.Get(context.Background(), util.ObjectKey(svc), svc); err != nil { + return false } - return false + return svc.GetAnnotations()["key"] == "value" }, timeout, poll).Should(BeTrue()) - }) - It("updates service annotations", func() { - svc := &corev1.Service{} - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "service", Namespace: namespace}, - svc, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) - - Eventually(func() error { - return k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "instance", Namespace: namespace}, - instance, - ) - }, timeout, poll).ShouldNot(HaveOccurred()) + It("prunes the service", func() { patch := client.MergeFrom(instance.DeepCopy()) - instance.Spec.ServiceAnnotations = map[string]string{"key": "value"} + instance.Spec.PruneService = util.Pointer(true) err := k8sClient.Patch(context.Background(), instance, patch) Expect(err).ToNot(HaveOccurred()) + svc := resources[1].(*corev1.Service) Eventually(func() bool { - err := k8sClient.Get( - context.Background(), - types.NamespacedName{Name: "service", Namespace: namespace}, - svc, - ) - Expect(err).ToNot(HaveOccurred()) - return svc.GetAnnotations()["key"] == "value" + err := k8sClient.Get(context.Background(), util.ObjectKey(svc), svc) + if err != nil && errors.IsNotFound(err) { + return true + } + return false }, timeout, poll).Should(BeTrue()) }) diff --git a/util/cmp.go b/util/cmp.go new file mode 100644 index 0000000..3c4d167 --- /dev/null +++ b/util/cmp.go @@ -0,0 +1,19 @@ +package util + +import ( + "strings" + + "github.com/google/go-cmp/cmp" +) + +func IgnoreProperty(p string) cmp.Option { + return cmp.FilterPath( + func(path cmp.Path) bool { + if field, ok := path.Last().(cmp.StructField); ok { + return strings.HasPrefix(field.Name(), p) + } + return false + }, + cmp.Ignore(), + ) +} diff --git a/util/k8s.go b/util/k8s.go index c705bb9..fd5d037 100644 --- a/util/k8s.go +++ b/util/k8s.go @@ -1,8 +1,13 @@ package util import ( + "fmt" "reflect" + "strings" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -32,3 +37,41 @@ func GetItems(list client.ObjectList) []client.Object { func IsBeingDeleted(o client.Object) bool { return !o.GetDeletionTimestamp().IsZero() } + +func NewObjectFromGVK(gvk schema.GroupVersionKind, s *runtime.Scheme) (client.Object, error) { + o, err := s.New(gvk) + if err != nil { + return nil, err + } + new, ok := o.(client.Object) + if !ok { + return nil, fmt.Errorf("runtime object %T does not implement client.Object", o) + } + return new, nil +} + +func NewObjectListFromGVK(gvk schema.GroupVersionKind, s *runtime.Scheme) (client.ObjectList, error) { + if !strings.HasSuffix(gvk.Kind, "List") { + gvk.Kind = gvk.Kind + "List" + } + o, err := s.New(gvk) + if err != nil { + return nil, err + } + new, ok := o.(client.ObjectList) + if !ok { + return nil, fmt.Errorf("runtime object %T does not implement client.ObjectList", o) + } + return new, nil +} + +func ObjectReference(o client.Object, gvk schema.GroupVersionKind) *corev1.ObjectReference { + return &corev1.ObjectReference{ + Kind: gvk.Kind, + Namespace: o.GetNamespace(), + Name: o.GetName(), + UID: o.GetUID(), + APIVersion: gvk.GroupVersion().String(), + ResourceVersion: o.GetResourceVersion(), + } +} diff --git a/util/k8s_test.go b/util/k8s_test.go index 6196606..61360bd 100644 --- a/util/k8s_test.go +++ b/util/k8s_test.go @@ -4,9 +4,11 @@ import ( "reflect" "testing" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -35,18 +37,18 @@ func TestGetItems(t *testing.T) { }, }, { - name: "Returns items of a monitoringv1.PodMonitorList as []client.Object", + name: "Returns items of a corev1.PodList as []client.Object", args: args{ - list: &monitoringv1.PodMonitorList{ - Items: []*monitoringv1.PodMonitor{ + list: &corev1.PodList{ + Items: []corev1.Pod{ {ObjectMeta: metav1.ObjectMeta{Name: "one"}}, {ObjectMeta: metav1.ObjectMeta{Name: "two"}}, }, }, }, want: []client.Object{ - &monitoringv1.PodMonitor{ObjectMeta: metav1.ObjectMeta{Name: "one"}}, - &monitoringv1.PodMonitor{ObjectMeta: metav1.ObjectMeta{Name: "two"}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "one"}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "two"}}, }, }} for _, tt := range tests { @@ -57,3 +59,94 @@ func TestGetItems(t *testing.T) { }) } } + +func TestNewObjectFromGVK(t *testing.T) { + type args struct { + gvk schema.GroupVersionKind + s *runtime.Scheme + } + tests := []struct { + name string + args args + want client.Object + wantErr bool + }{ + { + name: "Returns an object of the given gvk", + args: args{ + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + s: scheme.Scheme, + }, + want: &corev1.Service{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewObjectFromGVK(tt.args.gvk, tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("NewObjectFromGVK() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewObjectFromGVK() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewObjectListFromGVK(t *testing.T) { + type args struct { + gvk schema.GroupVersionKind + s *runtime.Scheme + } + tests := []struct { + name string + args args + want client.ObjectList + wantErr bool + }{ + { + name: "Returns a list when given an Object gvk", + args: args{ + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + s: scheme.Scheme, + }, + want: &corev1.ServiceList{}, + wantErr: false, + }, + { + name: "Returns a list when given an ObjectList gvk", + args: args{ + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "ServiceList", + }, + s: scheme.Scheme, + }, + want: &corev1.ServiceList{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewObjectListFromGVK(tt.args.gvk, tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("NewFromGVK() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewFromGVK() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/util/maps.go b/util/maps.go deleted file mode 100644 index 6695279..0000000 --- a/util/maps.go +++ /dev/null @@ -1,11 +0,0 @@ -package util - -// MergeMaps merges a list maps into the first one. B overrides A if keys collide. -func MergeMaps(base map[string]string, merges ...map[string]string) map[string]string { - for _, m := range merges { - for key, value := range m { - base[key] = value - } - } - return base -} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..d3fa76c --- /dev/null +++ b/util/util.go @@ -0,0 +1,33 @@ +package util + +// Returns a pointer of any type +func Pointer[T any](t T) *T { + return &t +} + +// MergeMaps merges a list maps into the first one. B overrides A if keys collide. +func MergeMaps(base map[string]string, merges ...map[string]string) map[string]string { + for _, m := range merges { + for key, value := range m { + base[key] = value + } + } + return base +} + +func ConvertStringSlice[T1 ~string, T2 ~string](collection []T1) []T2 { + out := make([]T2, 0, len(collection)) + for _, item := range collection { + out = append(out, T2(item)) + } + return out +} + +func ContainsBy[T any](collection []T, predicate func(item T) bool) bool { + for _, item := range collection { + if predicate(item) { + return true + } + } + return false +} From 629bdfa10ce0ffbe10bbd2bbe2c3256b70bf19a7 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 11 Dec 2023 12:29:34 +0100 Subject: [PATCH 03/20] Add documentation --- config/global.go | 36 +++++++++++++++++-- mutators/doc.go | 6 ++++ mutators/mutators.go | 33 ++++++++++++++++- mutators/rollout_triggers.go | 70 ++++++++++++++++++++---------------- reconciler/doc.go | 4 +++ reconciler/reconciler.go | 25 ++++++++++--- reconciler/status.go | 5 +++ resource/create_or_update.go | 13 +++++-- resource/property.go | 51 +++++++++++++------------- resource/property_test.go | 10 +++--- resource/template.go | 35 +++++++++--------- 11 files changed, 200 insertions(+), 88 deletions(-) create mode 100644 mutators/doc.go create mode 100644 reconciler/doc.go diff --git a/config/global.go b/config/global.go index c2d8bbc..d576f29 100644 --- a/config/global.go +++ b/config/global.go @@ -1,3 +1,6 @@ +// Package config provides global configuration for the basereconciler. The package +// provides some barebones configuration, but in most cases the user will want to +// tailor this configuration to the needs and requirements of the specific controller/s. package config import ( @@ -31,13 +34,34 @@ var config = struct { }, } -func GetAnnotationsDomain() string { return config.annotationsDomain } +// GetAnnotationsDomain returns the globally configured annotations domain. The annotations +// domain is used for the rollout trigger annotations (see the mutators package) and the resource +// finalizers +func GetAnnotationsDomain() string { return config.annotationsDomain } + +// SetAnnotationsDomain globally configures the annotations domain. The annotations +// domain is used for the rollout trigger annotations (see the mutators package) and the resource +// finalizers func SetAnnotationsDomain(domain string) { config.annotationsDomain = domain } -func EnableResourcePruner() { config.resourcePruner = true } -func DisableResourcePruner() { config.resourcePruner = false } +// EnableResourcePruner enables the resource pruner. The resource pruner keeps track of +// the owned resources of a given custom resource and deletes all that are not present in the list +// of managed resoures to reconcile. +func EnableResourcePruner() { config.resourcePruner = true } + +// DisableResourcePruner disables the resource pruner. The resource pruner keeps track of +// the owned resources of a given custom resource and deletes all that are not present in the list +// of managed resoures to reconcile. +func DisableResourcePruner() { config.resourcePruner = false } + +// IsResourcePrunerEnabled returs a boolean indicating wheter the resource pruner is enabled or not. func IsResourcePrunerEnabled() bool { return config.resourcePruner } +// GetDefaultReconcileConfigForGVK returns the default configuration that instructs basereconciler how to reconcile +// a given kubernetes GVK (GroupVersionKind). This default config will be used if the "resource.Template" object (see +// the resource package) does not specify a configuration itself. +// When the passed GVK does not match any of the configured, this function returns the "wildcard", which is a default +// set of basic reconclie rules that the reconciler will try to use when no other configuration is available. func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConfigForGVK, error) { if cfg, ok := config.defaultResourceReconcileConfig[gvk.String()]; ok { return cfg, nil @@ -47,6 +71,12 @@ func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConf return ReconcileConfigForGVK{}, fmt.Errorf("no config registered for gvk %s", gvk) } } + +// SetDefaultReconcileConfigForGVK sets the default configuration that instructs basereconciler how to reconcile +// a given kubernetes GVK (GroupVersionKind). This default config will be used if the "resource.Template" object (see +// the resource package) does not specify a configuration itself. +// If the passed GVK is an empty one ("schema.GroupVersionKind{}"), the function will set the wildcard instead, which +// is a default set of basic reconclie rules that the reconciler will try to use when no other configuration is available. func SetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind, cfg ReconcileConfigForGVK) { if reflect.DeepEqual(gvk, schema.GroupVersionKind{}) { config.defaultResourceReconcileConfig["*"] = cfg diff --git a/mutators/doc.go b/mutators/doc.go new file mode 100644 index 0000000..c3a0c0b --- /dev/null +++ b/mutators/doc.go @@ -0,0 +1,6 @@ +// Package mutators contains useful functions to perform template +// mutations that require to retrieve live state values from the +// Kubernetes API. +// These functions adhere to the "template.TemplateMutationFunction" function +// signature. +package mutators diff --git a/mutators/mutators.go b/mutators/mutators.go index f87fe12..b92a0f2 100644 --- a/mutators/mutators.go +++ b/mutators/mutators.go @@ -12,7 +12,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// reconcileDeploymentReplicas reconciles the number of replicas of a Deployment +// SetDeploymentReplicas reconciles the number of replicas of a Deployment. If "enforce" +// is set to true, the value in the template is enforce, overwritting the live value. If +// the "enforce" is set to false, the live value obtained from the Kubernetes API is used. +// In general, if the Deployment uses HorizontalPodAutoscaler or any other controller modifies +// the number of replicas, enforce needs to be "false". +// Example usage: +// +// &resource.Template[*appsv1.Deployment]{ +// TemplateBuilder: deployment(), +// IsEnabled: true, +// TemplateMutations: []resource.TemplateMutationFunction{ +// mutators.SetDeploymentReplicas(!hpaExists()), +// }, +// }, func SetDeploymentReplicas(enforce bool) resource.TemplateMutationFunction { return func(ctx context.Context, cl client.Client, desired client.Object) error { if enforce { @@ -36,6 +49,23 @@ func SetDeploymentReplicas(enforce bool) resource.TemplateMutationFunction { } } +// SetServiceLiveValues retrieves some live values of the Service spec from the Kubernetes +// API to avoid overwriting them. These values are typically set the by the kube-controller-manager +// (in some rare occasions the user might explicitely set them) and should not be modified by the +// reconciler. The fields that this function keeps in sync with the live state are: +// - spec.clusterIP +// - spec.ClisterIPs +// - spec.pors[*].nodePort (when the Service type is not ClusterIP) +// +// Example usage: +// +// &resource.Template[*corev1.Service]{ +// TemplateBuilder: service(req.Namespace, instance.Spec.ServiceAnnotations), +// IsEnabled: true, +// TemplateMutations: []resource.TemplateMutationFunction{ +// mutators.SetServiceLiveValues(), +// }, +// } func SetServiceLiveValues() resource.TemplateMutationFunction { return func(ctx context.Context, cl client.Client, desired client.Object) error { @@ -67,6 +97,7 @@ func SetServiceLiveValues() resource.TemplateMutationFunction { } } +// findPort returns the Service port identified by port/protocol func findPort(pNumber int32, pProtocol corev1.Protocol, ports []corev1.ServicePort) *corev1.ServicePort { // Ports within a svc are uniquely identified by // the "port" and "protocol" fields. This is documented in diff --git a/mutators/rollout_triggers.go b/mutators/rollout_triggers.go index d3e8919..8876e59 100644 --- a/mutators/rollout_triggers.go +++ b/mutators/rollout_triggers.go @@ -15,14 +15,51 @@ import ( ) // RolloutTrigger defines a configuration source that should trigger a -// rollout whenever the data within that configuration source changes +// rollout whenever the data within that configuration source changes. +// Example usage: +// +// &resource.Template[*appsv1.Deployment]{ +// TemplateBuilder: deployment(), +// IsEnabled: true, +// TemplateMutations: []resource.TemplateMutationFunction{ +// mutators.RolloutTrigger{Name: "secret", SecretName: pointer.String("secret")}.Add(), +// }, +// }, + type RolloutTrigger struct { Name string ConfigMapName *string SecretName *string } -// GetHash returns the hash of the data container in the RolloutTrigger +// Add adds the trigger to the Deployment/StatefulSet +func (trigger RolloutTrigger) Add(params ...string) resource.TemplateMutationFunction { + var domain string + if len(params) == 0 { + domain = config.GetAnnotationsDomain() + } else { + domain = params[0] + } + return func(ctx context.Context, cl client.Client, desired client.Object) error { + + hash, err := trigger.GetHash(ctx, cl, desired.GetNamespace()) + if err != nil { + return err + } + trigger := map[string]string{trigger.GetAnnotationKey(domain): hash} + + switch o := desired.(type) { + case *appsv1.Deployment: + o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) + case *appsv1.StatefulSet: + o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) + } + + return nil + } +} + +// GetHash returns the hash of the data contained in the RolloutTrigger // config source func (rt RolloutTrigger) GetHash(ctx context.Context, cl client.Client, namespace string) (string, error) { @@ -54,37 +91,10 @@ func (rt RolloutTrigger) GetHash(ctx context.Context, cl client.Client, namespac } // GetAnnotationKey returns the annotation key to be used in the Pods that read -// from the config source defined in the RolloutTrigger +// from the config source defined in the RolloutTrigger. func (rt RolloutTrigger) GetAnnotationKey(annotationsDomain string) string { if rt.SecretName != nil { return fmt.Sprintf("%s/%s.%s", string(annotationsDomain), rt.Name, "secret-hash") } return fmt.Sprintf("%s/%s.%s", string(annotationsDomain), rt.Name, "configmap-hash") } - -// Add adds the trigger to the Deployment/StatefulSet -func (trigger RolloutTrigger) Add(params ...string) resource.TemplateMutationFunction { - var domain string - if len(params) == 0 { - domain = config.GetAnnotationsDomain() - } else { - domain = params[0] - } - return func(ctx context.Context, cl client.Client, desired client.Object) error { - - hash, err := trigger.GetHash(ctx, cl, desired.GetNamespace()) - if err != nil { - return err - } - trigger := map[string]string{trigger.GetAnnotationKey(domain): hash} - - switch o := desired.(type) { - case *appsv1.Deployment: - o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) - case *appsv1.StatefulSet: - o.Spec.Template.ObjectMeta.Annotations = util.MergeMaps(map[string]string{}, o.Spec.Template.ObjectMeta.Annotations, trigger) - } - - return nil - } -} diff --git a/reconciler/doc.go b/reconciler/doc.go new file mode 100644 index 0000000..73c361e --- /dev/null +++ b/reconciler/doc.go @@ -0,0 +1,4 @@ +// Package reconciler contatins types and methods that can be used in controller +// reconciliation logic. It depends on controller-runtime, so the use of kubebuilder or +// operator-sdk is highly recommended. +package reconciler diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index f54e249..262a596 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -28,12 +28,19 @@ type Reconciler struct { typeTracker typeTracker } +// NewFromManager returns a new Reconciler from a controller-runtime manager.Manager func NewFromManager(mgr manager.Manager) Reconciler { return Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()} } // GetInstance tries to retrieve the custom resource instance and perform some standard -// tasks like initialization and cleanup when required. +// tasks like initialization and cleanup. The behaviour can be modified depending on the +// parameters passed to the function: +// - finalizer: if a non-nil finalizer is passed to the function, it will ensure that the +// custom resource has a finalizer in place, updasting it if required. +// - cleanupFns: variadic parameter that allows passing cleanup functions that will be +// run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise +// the custom resource will be immediately deleted and the functions won't run. func (r *Reconciler) GetInstance(ctx context.Context, key types.NamespacedName, instance client.Object, finalizer *string, cleanupFns []func()) (*ctrl.Result, error) { logger := log.FromContext(ctx) @@ -98,8 +105,7 @@ func (r *Reconciler) IsInitialized(instance client.Object, finalizer *string) bo return ok } -// ManageCleanupLogic contains finalization logic for the LockedResourcesReconciler -// Functionality can be extended by passing extra cleanup functions +// ManageCleanupLogic contains finalization logic for the Reconciler func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), log logr.Logger) error { // Call any cleanup functions passed for _, fn := range fns { @@ -108,8 +114,16 @@ func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), lo return nil } -// ReconcileOwnedResources handles generalized resource reconcile logic for -// all controllers +// ReconcileOwnedResources handles generalized resource reconcile logic for a controller: +// +// - Takes a list of templates and calls resource.CreateOrUpdate on each one of them. The templates +// need to implement the resource.TemplateInterface interface. Users can take advantage of the generic +// resource.Template[T] struct that the resource package provides, which already implements the +// resource.TemplateInterface. +// - Each template is added to the list of managed resources if resource.CreateOrUpdate returns with no error +// - If the resource pruner is enabled any resource owned by the custom resource not present in the list of managed +// resources is deleted. The resource pruner must be enabled in the global config (see package config) and also not +// explicitely disabled in the resource by the '/prune: true/false' annotation. func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) error { managedResources := []corev1.ObjectReference{} @@ -135,6 +149,7 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O // SecretEventHandler returns an EventHandler for the specific client.ObjectList // list object passed as parameter +// TODO: generalize this to watch any object type func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger) handler.EventHandler { return handler.EnqueueRequestsFromMapFunc( func(o client.Object) []reconcile.Request { diff --git a/reconciler/status.go b/reconciler/status.go index f1b82a5..eab46e8 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -10,6 +10,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +// ReconcileStatus can reconcile the status of a custom resource when the resource implements +// the status.ObjectWithAppStatus interface. It is specifically targeted for the status of custom +// resources that deploy Deployments/StatefulSets, as it can aggregate the status of those into the +// status of the custom resource. It also accepts functions with signature "func() bool" that can +// reconcile the status of the custom resource and return whether update is required or not. func (r *Reconciler) ReconcileStatus(ctx context.Context, instance status.ObjectWithAppStatus, deployments, statefulsets []types.NamespacedName, mutators ...func() bool) error { logger := log.FromContext(ctx) diff --git a/resource/create_or_update.go b/resource/create_or_update.go index 7e431df..47b4052 100644 --- a/resource/create_or_update.go +++ b/resource/create_or_update.go @@ -21,7 +21,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -// CreateOrUpdate cretes or updates resources +// CreateOrUpdate cretes or updates resources. The function receives several paremters: +// - ctx: the context +// - cl: the kubernetes API client +// - scheme: the kubernetes API scheme +// - owner: the object that owns the resource. Used to set the OwnerReference in the resource +// - template: the struct that describes how the resource needs to be reconciled. It must implement +// the TemplateInterface interface. When template.GetEnsureProperties is not set or an empty list, this +// function will lookup for configuration in the global configuration (see pacakge config). func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Scheme, owner client.Object, template TemplateInterface) (*corev1.ObjectReference, error) { @@ -106,7 +113,7 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem // reconcile properties for _, property := range ensure { - if err := property.Reconcile(u_live, u_desired, u_normalizedLive, logger); err != nil { + if err := property.reconcile(u_live, u_desired, u_normalizedLive, logger); err != nil { return nil, wrapError(fmt.Sprintf("unable to reconcile property %s", property), key, gvk, err) } } @@ -114,7 +121,7 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem // ignore properties for _, property := range ignore { for _, m := range []map[string]any{u_live, u_desired, u_normalizedLive} { - if err := property.Ignore(m); err != nil { + if err := property.ignore(m); err != nil { return nil, wrapError(fmt.Sprintf("unable to ignore property %s", property), key, gvk, err) } } diff --git a/resource/property.go b/resource/property.go index 3ff45ec..fcde261 100644 --- a/resource/property.go +++ b/resource/property.go @@ -8,63 +8,66 @@ import ( "k8s.io/apimachinery/pkg/api/equality" ) -type PropertyDelta int +type propertyDelta int const ( - MissingInBoth PropertyDelta = 0 - MissingFromDesiredPresentInLive PropertyDelta = 1 - PresentInDesiredMissingFromLive PropertyDelta = 2 - PresentInBoth PropertyDelta = 3 + missingInBoth propertyDelta = 0 + missingFromDesiredPresentInLive propertyDelta = 1 + presentInDesiredMissingFromLive propertyDelta = 2 + presentInBoth propertyDelta = 3 ) +// Property represents a json path to a field in the resource that can +// be either reconciled to ensure it mathes the desired value or can be ignored +// to avoid reconciling certain fields in the rource we are not interested in. type Property string -func (p Property) JSONPath() string { return string(p) } +func (p Property) jsonPath() string { return string(p) } -func (p Property) Reconcile(u_live, u_desired, u_normalizedLive map[string]any, logger logr.Logger) error { - expr, err := jp.ParseString(p.JSONPath()) +func (p Property) reconcile(u_live, u_desired, u_normalizedLive map[string]any, logger logr.Logger) error { + expr, err := jp.ParseString(p.jsonPath()) if err != nil { - return fmt.Errorf("unable to parse JSONPath '%s': %w", p.JSONPath(), err) + return fmt.Errorf("unable to parse JSONPath '%s': %w", p.jsonPath(), err) } desiredVal := expr.Get(u_desired) liveVal := expr.Get(u_live) if len(desiredVal) > 1 || len(liveVal) > 1 { - return fmt.Errorf("multi-valued JSONPath (%s) not supported when reconciling properties", p.JSONPath()) + return fmt.Errorf("multi-valued JSONPath (%s) not supported when reconciling properties", p.jsonPath()) } // store the live value for later comparison in u_normalizedLive if len(liveVal) != 0 { if err := expr.Set(u_normalizedLive, liveVal[0]); err != nil { - return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", liveVal[0], p.JSONPath()) + return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", liveVal[0], p.jsonPath()) } } switch delta(len(desiredVal), len(liveVal)) { - case MissingInBoth: + case missingInBoth: // nothing to do return nil - case MissingFromDesiredPresentInLive: + case missingFromDesiredPresentInLive: // delete property from u_live if err := expr.Del(u_live); err != nil { - return fmt.Errorf("usable to delete JSONPath '%s'", p.JSONPath()) + return fmt.Errorf("usable to delete JSONPath '%s'", p.jsonPath()) } return nil - case PresentInDesiredMissingFromLive: + case presentInDesiredMissingFromLive: // add property to u_live if err := expr.Set(u_live, desiredVal[0]); err != nil { - return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", desiredVal[0], p.JSONPath()) + return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", desiredVal[0], p.jsonPath()) } return nil - case PresentInBoth: + case presentInBoth: // replace property in u_live if values differ if !equality.Semantic.DeepEqual(desiredVal[0], liveVal[0]) { if err := expr.Set(u_live, desiredVal[0]); err != nil { - return fmt.Errorf("usable to replace value '%v' in JSONPath '%s'", desiredVal[0], p.JSONPath()) + return fmt.Errorf("usable to replace value '%v' in JSONPath '%s'", desiredVal[0], p.jsonPath()) } return nil } @@ -74,17 +77,17 @@ func (p Property) Reconcile(u_live, u_desired, u_normalizedLive map[string]any, return nil } -func delta(a, b int) PropertyDelta { - return PropertyDelta(a<<1 + b) +func delta(a, b int) propertyDelta { + return propertyDelta(a<<1 + b) } -func (p Property) Ignore(m map[string]any) error { - expr, err := jp.ParseString(p.JSONPath()) +func (p Property) ignore(m map[string]any) error { + expr, err := jp.ParseString(p.jsonPath()) if err != nil { - return fmt.Errorf("unable to parse JSONPath '%s': %w", p.JSONPath(), err) + return fmt.Errorf("unable to parse JSONPath '%s': %w", p.jsonPath(), err) } if err = expr.Del(m); err != nil { - return fmt.Errorf("unable to parse delete JSONPath '%s' from unstructured: %w", p.JSONPath(), err) + return fmt.Errorf("unable to parse delete JSONPath '%s' from unstructured: %w", p.jsonPath(), err) } return nil } diff --git a/resource/property_test.go b/resource/property_test.go index 6f12803..23a25ac 100644 --- a/resource/property_test.go +++ b/resource/property_test.go @@ -78,7 +78,7 @@ func TestProperty_Reconcile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.p.Reconcile(tt.args.u_live, tt.args.u_desired, tt.args.u_normalizedLive, tt.args.logger) + err := tt.p.reconcile(tt.args.u_live, tt.args.u_desired, tt.args.u_normalizedLive, tt.args.logger) if (err != nil) != tt.wantErr { t.Errorf("Property.Reconcile() error = %v, wantErr %v", err, tt.wantErr) return @@ -95,8 +95,8 @@ func TestProperty_Reconcile(t *testing.T) { func Test_delta(t *testing.T) { g := gomega.NewWithT(t) - g.Expect(delta(0, 0)).To(gomega.Equal(MissingInBoth)) - g.Expect(delta(0, 1)).To(gomega.Equal(MissingFromDesiredPresentInLive)) - g.Expect(delta(1, 0)).To(gomega.Equal(PresentInDesiredMissingFromLive)) - g.Expect(delta(1, 1)).To(gomega.Equal(PresentInBoth)) + g.Expect(delta(0, 0)).To(gomega.Equal(missingInBoth)) + g.Expect(delta(0, 1)).To(gomega.Equal(missingFromDesiredPresentInLive)) + g.Expect(delta(1, 0)).To(gomega.Equal(presentInDesiredMissingFromLive)) + g.Expect(delta(1, 1)).To(gomega.Equal(presentInBoth)) } diff --git a/resource/template.go b/resource/template.go index e9d23a5..0733e34 100644 --- a/resource/template.go +++ b/resource/template.go @@ -6,6 +6,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// TemplateInterface represents a template that can has methods that instruct how a certain +// resource needs to be progressed to match its desired state. type TemplateInterface interface { Build(ctx context.Context, cl client.Client, o client.Object) (client.Object, error) Enabled() bool @@ -14,30 +16,29 @@ type TemplateInterface interface { } // TemplateBuilderFunction is a function that returns a k8s API object (client.Object) when -// called. TemplateBuilderFunction has no access to cluster live info +// called. TemplateBuilderFunction has no access to cluster live info. +// A TemplateBuilderFunction is used to return the basic shape of a resource (a template) that can +// then be further modified before it's compared with it's live state and reconciled. type TemplateBuilderFunction[T client.Object] func(client.Object) (T, error) -// func NewBuilderFunctionFromObject[T client.Object](o T) TemplateBuilderFunction[T] { -// return func(client.Object) (T, error) { -// return o, nil -// } -// } - // TemplateMutationFunction represents mutation functions that require an API client, generally -// because they need to retrieve live cluster information to mutate the object +// because they need to retrieve live cluster information to mutate the object. +// A TemplateMutationFunction is typically used to modify a template using live values obtained from +// a kubernetes API server. type TemplateMutationFunction func(context.Context, client.Client, client.Object) error +// Template implements TemplateInterface. type Template[T client.Object] struct { // TemplateBuilder is the function that is used as the basic - // tempalte for the object. It is called by Build() to create the + // template for the object. It is called by Build() to create the // object. TemplateBuilder TemplateBuilderFunction[T] // TemplateMutations are functions that are called during Build() after // TemplateBuilder has ben invoked, to perform mutations on the object that require - // access to an API client. + // access to a kubernetes API server. TemplateMutations []TemplateMutationFunction - // IsEnabled specifies whether the resourse describe by this Template should - // exists or not + // IsEnabled specifies whether the resourse described by this Template should + // exist or not. IsEnabled bool // EnsureProperties are the properties from the desired object that should be enforced // to the live object. The syntax is jsonpath. @@ -48,6 +49,7 @@ type Template[T client.Object] struct { IgnoreProperties []Property } +// NewTemplate returns a new Template struct using the passed parameters func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], enabled bool, mutations ...TemplateMutationFunction) *Template[T] { return &Template[T]{ @@ -59,6 +61,8 @@ func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], } } +// NewTemplateFromObjectFunction returns a new Template using the given kubernetes +// object as the base. func NewTemplateFromObjectFunction[T client.Object](fn func() T, enabled bool, mutations ...TemplateMutationFunction) *Template[T] { return &Template[T]{ @@ -70,7 +74,8 @@ func NewTemplateFromObjectFunction[T client.Object](fn func() T, } } -// Build returns a T resource by executing its template function +// Build returns a T resource. It first executes the TemplateBuilder function and then each of the +// TemplateMutationFunction functions specified by the TemplateMutations field. func (t *Template[T]) Build(ctx context.Context, cl client.Client, o client.Object) (client.Object, error) { o, err := t.TemplateBuilder(o) if err != nil { @@ -113,7 +118,3 @@ func (t *Template[T]) Apply(mutation TemplateBuilderFunction[T]) *Template[T] { return t } - -func (t *Template[T]) Chain(mutation TemplateBuilderFunction[T]) *Template[T] { - return t.Apply(mutation) -} From 0390a848381600636b77bd1805fea858cbaeccac Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 11 Dec 2023 13:17:48 +0100 Subject: [PATCH 04/20] Merge status and reconciler packages --- reconciler/status.go | 63 +++++++++++++++++++++++++++++++-- status/interface.go | 58 ------------------------------ test/api/v1alpha1/test_types.go | 10 +++--- 3 files changed, 65 insertions(+), 66 deletions(-) delete mode 100644 status/interface.go diff --git a/reconciler/status.go b/reconciler/status.go index eab46e8..56cd3f3 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -3,19 +3,19 @@ package reconciler import ( "context" - "github.com/3scale-ops/basereconciler/status" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" ) // ReconcileStatus can reconcile the status of a custom resource when the resource implements -// the status.ObjectWithAppStatus interface. It is specifically targeted for the status of custom +// the ObjectWithAppStatus interface. It is specifically targeted for the status of custom // resources that deploy Deployments/StatefulSets, as it can aggregate the status of those into the // status of the custom resource. It also accepts functions with signature "func() bool" that can // reconcile the status of the custom resource and return whether update is required or not. -func (r *Reconciler) ReconcileStatus(ctx context.Context, instance status.ObjectWithAppStatus, +func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithAppStatus, deployments, statefulsets []types.NamespacedName, mutators ...func() bool) error { logger := log.FromContext(ctx) update := false @@ -69,3 +69,60 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance status.Object return nil } + +// ObjectWithAppStatus is an interface that implements +// both client.Object and AppStatus +type ObjectWithAppStatus interface { + client.Object + GetStatus() AppStatus +} + +// Health not yet implemented +type Health string + +const ( + Health_Healthy Health = "Healthy" + Health_Progressing Health = "Progressing" + Health_Degraded Health = "Degraded" + Health_Suspended Health = "Suspended" + Health_Unknown Health = "Unknown" +) + +// AppStatus is an interface describing a custom resource with +// an status that can be reconciled by the reconciler +type AppStatus interface { + // GetHealth(types.NamespacedName) Health + // SetHealth(types.NamespacedName, Health) + GetDeploymentStatus(types.NamespacedName) *appsv1.DeploymentStatus + SetDeploymentStatus(types.NamespacedName, *appsv1.DeploymentStatus) + GetStatefulSetStatus(types.NamespacedName) *appsv1.StatefulSetStatus + SetStatefulSetStatus(types.NamespacedName, *appsv1.StatefulSetStatus) +} + +// UnimplementedDeploymentStatus type can be used for resources that doesn't use Deployments +type UnimplementedDeploymentStatus struct{} + +func (u *UnimplementedDeploymentStatus) GetDeployments() []types.NamespacedName { + return nil +} + +func (u *UnimplementedDeploymentStatus) GetDeploymentStatus(types.NamespacedName) *appsv1.DeploymentStatus { + return nil +} + +func (u *UnimplementedDeploymentStatus) SetDeploymentStatus(types.NamespacedName, *appsv1.DeploymentStatus) { +} + +// UnimplementedStatefulSetStatus type can be used for resources that doesn't use StatefulSets +type UnimplementedStatefulSetStatus struct{} + +func (u *UnimplementedStatefulSetStatus) GetStatefulSets() []types.NamespacedName { + return nil +} + +func (u *UnimplementedStatefulSetStatus) GetStatefulSetStatus(types.NamespacedName) *appsv1.StatefulSetStatus { + return nil +} + +func (u *UnimplementedStatefulSetStatus) SetStatefulSetStatus(types.NamespacedName, *appsv1.StatefulSetStatus) { +} diff --git a/status/interface.go b/status/interface.go deleted file mode 100644 index 8531862..0000000 --- a/status/interface.go +++ /dev/null @@ -1,58 +0,0 @@ -package status - -import ( - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type ObjectWithAppStatus interface { - client.Object - GetStatus() AppStatus -} - -// Health not yet implemented -type Health string - -const ( - Health_Healthy Health = "Healthy" - Health_Progressing Health = "Progressing" - Health_Degraded Health = "Degraded" - Health_Suspended Health = "Suspended" - Health_Unknown Health = "Unknown" -) - -type AppStatus interface { - // GetHealth(types.NamespacedName) Health - // SetHealth(types.NamespacedName, Health) - GetDeploymentStatus(types.NamespacedName) *appsv1.DeploymentStatus - SetDeploymentStatus(types.NamespacedName, *appsv1.DeploymentStatus) - GetStatefulSetStatus(types.NamespacedName) *appsv1.StatefulSetStatus - SetStatefulSetStatus(types.NamespacedName, *appsv1.StatefulSetStatus) -} - -type UnimplementedDeploymentStatus struct{} - -func (u *UnimplementedDeploymentStatus) GetDeployments() []types.NamespacedName { - return nil -} - -func (u *UnimplementedDeploymentStatus) GetDeploymentStatus(types.NamespacedName) *appsv1.DeploymentStatus { - return nil -} - -func (u *UnimplementedDeploymentStatus) SetDeploymentStatus(types.NamespacedName, *appsv1.DeploymentStatus) { -} - -type UnimplementedStatefulSetStatus struct{} - -func (u *UnimplementedStatefulSetStatus) GetStatefulSets() []types.NamespacedName { - return nil -} - -func (u *UnimplementedStatefulSetStatus) GetStatefulSetStatus(types.NamespacedName) *appsv1.StatefulSetStatus { - return nil -} - -func (u *UnimplementedStatefulSetStatus) SetStatefulSetStatus(types.NamespacedName, *appsv1.StatefulSetStatus) { -} diff --git a/test/api/v1alpha1/test_types.go b/test/api/v1alpha1/test_types.go index 65602bf..482e4c5 100644 --- a/test/api/v1alpha1/test_types.go +++ b/test/api/v1alpha1/test_types.go @@ -20,7 +20,7 @@ limitations under the License. package v1alpha1 import ( - "github.com/3scale-ops/basereconciler/status" + "github.com/3scale-ops/basereconciler/reconciler" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -55,8 +55,8 @@ type TestSpec struct { type TestStatus struct { // +operator-sdk:csv:customresourcedefinitions:type=spec // +optional - *appsv1.DeploymentStatus `json:"deploymentStatus,omitempty"` - status.UnimplementedStatefulSetStatus `json:"-"` + *appsv1.DeploymentStatus `json:"deploymentStatus,omitempty"` + reconciler.UnimplementedStatefulSetStatus `json:"-"` } func (dss *TestStatus) GetDeploymentStatus(key types.NamespacedName) *appsv1.DeploymentStatus { @@ -79,9 +79,9 @@ type Test struct { Status TestStatus `json:"status,omitempty"` } -var _ status.ObjectWithAppStatus = &Test{} +var _ reconciler.ObjectWithAppStatus = &Test{} -func (t *Test) GetStatus() status.AppStatus { +func (t *Test) GetStatus() reconciler.AppStatus { return &t.Status } From 8c0cd60aa74fcac6026aae2aebdf34c729e61c99 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 11 Dec 2023 16:41:29 +0100 Subject: [PATCH 05/20] Logger as part of the basereconciler --- reconciler/pruner.go | 4 ++-- reconciler/reconciler.go | 29 ++++++++++++++++++++++++----- reconciler/status.go | 4 ++-- resource/create_or_update.go | 4 ++-- test/suite_test.go | 4 ++-- test/test_controller.go | 2 -- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/reconciler/pruner.go b/reconciler/pruner.go index 97fcfb8..57550b6 100644 --- a/reconciler/pruner.go +++ b/reconciler/pruner.go @@ -9,16 +9,16 @@ import ( "github.com/3scale-ops/basereconciler/config" "github.com/3scale-ops/basereconciler/util" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) func (r *Reconciler) pruneOrphaned(ctx context.Context, owner client.Object, managed []corev1.ObjectReference) error { - logger := log.FromContext(ctx) + logger := logr.FromContextOrDiscard(ctx) ownerGVK, err := apiutil.GVKForObject(owner, r.Scheme) if err != nil { diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 262a596..8795e5e 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -16,7 +16,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -24,13 +23,33 @@ import ( // Reconciler computes a list of resources that it needs to keep in place type Reconciler struct { client.Client + Log logr.Logger Scheme *runtime.Scheme typeTracker typeTracker } // NewFromManager returns a new Reconciler from a controller-runtime manager.Manager -func NewFromManager(mgr manager.Manager) Reconciler { - return Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()} +func NewFromManager(mgr manager.Manager) *Reconciler { + return &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logr.Discard()} +} + +func (r *Reconciler) WithLogger(logger logr.Logger) *Reconciler { + r.Log = logger + return r +} + +func (r *Reconciler) GetLogger(ctx context.Context) logr.Logger { + if logger, err := logr.FromContext(ctx); err != nil { + return r.Log + } else { + return logger + } +} + +func (r *Reconciler) SetLogger(ctx *context.Context, keysAndValues ...interface{}) logr.Logger { + logger := r.GetLogger(*ctx).WithValues(keysAndValues) + *ctx = logr.NewContext(*ctx, logger) + return logger } // GetInstance tries to retrieve the custom resource instance and perform some standard @@ -42,8 +61,8 @@ func NewFromManager(mgr manager.Manager) Reconciler { // run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise // the custom resource will be immediately deleted and the functions won't run. func (r *Reconciler) GetInstance(ctx context.Context, key types.NamespacedName, - instance client.Object, finalizer *string, cleanupFns []func()) (*ctrl.Result, error) { - logger := log.FromContext(ctx) + instance client.Object, finalizer *string, cleanupFns ...func()) (*ctrl.Result, error) { + logger := logr.FromContextOrDiscard(ctx) err := r.Client.Get(ctx, key, instance) if err != nil { diff --git a/reconciler/status.go b/reconciler/status.go index 56cd3f3..4d0a486 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -3,11 +3,11 @@ package reconciler import ( "context" + "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" ) // ReconcileStatus can reconcile the status of a custom resource when the resource implements @@ -17,7 +17,7 @@ import ( // reconcile the status of the custom resource and return whether update is required or not. func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithAppStatus, deployments, statefulsets []types.NamespacedName, mutators ...func() bool) error { - logger := log.FromContext(ctx) + logger := logr.FromContextOrDiscard(ctx) update := false status := instance.GetStatus() diff --git a/resource/create_or_update.go b/resource/create_or_update.go index 47b4052..db29933 100644 --- a/resource/create_or_update.go +++ b/resource/create_or_update.go @@ -7,6 +7,7 @@ import ( "github.com/3scale-ops/basereconciler/config" "github.com/3scale-ops/basereconciler/util" + "github.com/go-logr/logr" "github.com/nsf/jsondiff" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -18,7 +19,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) // CreateOrUpdate cretes or updates resources. The function receives several paremters: @@ -42,7 +42,7 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem if err != nil { return nil, err } - logger := log.FromContext(ctx, "gvk", gvk, "resource", desired.GetName()) + logger := logr.FromContextOrDiscard(ctx).WithValues("gvk", gvk, "resource", desired.GetName()) live, err := util.NewObjectFromGVK(gvk, scheme) if err != nil { diff --git a/test/suite_test.go b/test/suite_test.go index 42c46f7..97faed7 100644 --- a/test/suite_test.go +++ b/test/suite_test.go @@ -102,8 +102,8 @@ var _ = BeforeSuite(func() { // Add controllers for testing err = (&Reconciler{ - Reconciler: reconciler.NewFromManager(mgr), - Log: ctrl.Log.WithName("controllers").WithName("Test"), + Reconciler: *reconciler.NewFromManager(mgr). + WithLogger(ctrl.Log.WithName("controllers").WithName("Test")), }).SetupWithManager(mgr) Expect(err).ToNot(HaveOccurred()) diff --git a/test/test_controller.go b/test/test_controller.go index 2033f80..4dd1f60 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -24,7 +24,6 @@ import ( "github.com/3scale-ops/basereconciler/resource" "github.com/3scale-ops/basereconciler/test/api/v1alpha1" "github.com/3scale-ops/basereconciler/util" - "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -43,7 +42,6 @@ import ( // +kubebuilder:object:generate=false type Reconciler struct { reconciler.Reconciler - Log logr.Logger } // Reconcile is part of the main kubernetes reconciliation loop which aims to From 47b2653d47db41846d8ab876e8aaf8d5303036ab Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 11 Dec 2023 17:13:50 +0100 Subject: [PATCH 06/20] Prettier API --- Makefile | 7 +- README.md | 21 +++++- config/global.go | 3 +- mutators/mutators.go | 5 +- reconciler/pruner_test.go | 3 +- reconciler/reconciler.go | 112 +++++++++++++++++------------ reconciler/status.go | 10 +-- resource/create_or_update.go | 5 +- resource/template.go | 44 ++++++++---- test/suite_test.go | 3 +- test/test_controller.go | 38 +++++----- test/test_controller_suite_test.go | 10 +-- util/k8s.go | 5 -- 13 files changed, 157 insertions(+), 109 deletions(-) diff --git a/Makefile b/Makefile index 069efa8..c90ab2e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.24 +ENVTEST_K8S_VERSION = 1.27 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -45,7 +45,10 @@ KUBEBUILDER_ASSETS = "$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" TEST_PKG = ./... test: manifests generate fmt vet envtest ginkgo ## Run tests. - KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -v -r $(TEST_PKG) + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -r $(TEST_PKG) + +test-debug: manifests generate fmt vet envtest ginkgo ## Run tests. + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -v -r $(TEST_PKG) manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) crd paths="./test/..." output:crd:artifacts:config="./test/api/v1alpha1" diff --git a/README.md b/README.md index e6350eb..e4f5b27 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# basereconciler \ No newline at end of file +# basereconciler + +Basereconciler is an attempt to create a reconciler that can be imported an used in any controller-runtime based controller to perform the most common tasks a controller usually performs. It's a bunch of code that it's typically written again and again for every and each controller and that can be abstracted to work in a more generic way to avoid the repetition and improve code mantainability. +At the moment basereconciler can perform the following tasks: + +* Get the custom resource and perform some common tasks on it: + * Management of resource finalizer: some custom resources required more complex finalization logic. For this to happen a finalizer must be in place. Basereconciler can keep this finalizer in place and remove it when necessary during resource finalization. + * Management of finalization logic: it checks if the resource is being finalized and executed the finalization logic passed to it if that is the case. When all finalization logic is completed it removes the finalizer on the custom resource. +* Reconcile resources owned by the custom resource: basreconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future. +* Reconcile custom resource status: if the custom resource implements a certain interface, basereconciler can also be in charge of reconciling the status. +* Resource pruner: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no logner required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation. + +## Basic Usage + +The following example is a kubebuilder bootstrapped controller that uses basereconciler to reconcile several resources owned by a custom resource. Explanations inline in the code. + +```go + +} +``` \ No newline at end of file diff --git a/config/global.go b/config/global.go index d576f29..0b508b9 100644 --- a/config/global.go +++ b/config/global.go @@ -5,7 +5,6 @@ package config import ( "fmt" - "reflect" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -78,7 +77,7 @@ func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConf // If the passed GVK is an empty one ("schema.GroupVersionKind{}"), the function will set the wildcard instead, which // is a default set of basic reconclie rules that the reconciler will try to use when no other configuration is available. func SetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind, cfg ReconcileConfigForGVK) { - if reflect.DeepEqual(gvk, schema.GroupVersionKind{}) { + if gvk.Empty() { config.defaultResourceReconcileConfig["*"] = cfg } else { config.defaultResourceReconcileConfig[gvk.String()] = cfg diff --git a/mutators/mutators.go b/mutators/mutators.go index b92a0f2..840204c 100644 --- a/mutators/mutators.go +++ b/mutators/mutators.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/3scale-ops/basereconciler/resource" - "github.com/3scale-ops/basereconciler/util" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -35,7 +34,7 @@ func SetDeploymentReplicas(enforce bool) resource.TemplateMutationFunction { } live := &appsv1.Deployment{} - if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil { + if err := cl.Get(ctx, client.ObjectKeyFromObject(desired), live); err != nil { if errors.IsNotFound(err) { return nil } @@ -71,7 +70,7 @@ func SetServiceLiveValues() resource.TemplateMutationFunction { svc := desired.(*corev1.Service) live := &corev1.Service{} - if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil { + if err := cl.Get(ctx, client.ObjectKeyFromObject(desired), live); err != nil { if errors.IsNotFound(err) { return nil } diff --git a/reconciler/pruner_test.go b/reconciler/pruner_test.go index bcd8792..6a96f84 100644 --- a/reconciler/pruner_test.go +++ b/reconciler/pruner_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/3scale-ops/basereconciler/config" - "github.com/3scale-ops/basereconciler/util" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" @@ -177,7 +176,7 @@ func TestReconciler_pruneOrphaned(t *testing.T) { return } for _, check := range tt.want { - err := tt.fields.Client.Get(tt.args.ctx, util.ObjectKey(check.obj), check.obj) + err := tt.fields.Client.Get(tt.args.ctx, client.ObjectKeyFromObject(check.obj), check.obj) if (err != nil && errors.IsNotFound(err)) != check.absent { t.Errorf("Reconciler.pruneOrphaned() want %s to be absent=%v", check.obj.GetName(), check.absent) } diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 8795e5e..55bb1a4 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -20,11 +20,25 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +type ReconcileResult struct { + Requeue bool + Error error +} + +func (result ReconcileResult) IsReturnAndRequeue() bool { + return result.Requeue || result.Error != nil +} + +func (result ReconcileResult) Values() (ctrl.Result, error) { + return ctrl.Result{Requeue: result.Requeue}, result.Error +} + // Reconciler computes a list of resources that it needs to keep in place type Reconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + gvk schema.GroupVersionKind typeTracker typeTracker } @@ -33,23 +47,28 @@ func NewFromManager(mgr manager.Manager) *Reconciler { return &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logr.Discard()} } +func (r *Reconciler) WithGVK(apiVersion, kind string) *Reconciler { + r.gvk = schema.FromAPIVersionAndKind(apiVersion, kind) + return r +} + +// WithLogger sets the Reconciler logger func (r *Reconciler) WithLogger(logger logr.Logger) *Reconciler { r.Log = logger return r } -func (r *Reconciler) GetLogger(ctx context.Context) logr.Logger { - if logger, err := logr.FromContext(ctx); err != nil { - return r.Log +// Logger returns the Reconciler logger and a copy of the context that also includes the logger inside to pass it around easily. +func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) (context.Context, logr.Logger) { + var logger logr.Logger + if !r.Log.IsZero() { + // get the logger configured in the Reconciler + logger = r.Log.WithValues(keysAndValues...) } else { - return logger + // try to get a logger from the context + logger = logr.FromContextOrDiscard(ctx).WithValues(keysAndValues...) } -} - -func (r *Reconciler) SetLogger(ctx *context.Context, keysAndValues ...interface{}) logr.Logger { - logger := r.GetLogger(*ctx).WithValues(keysAndValues) - *ctx = logr.NewContext(*ctx, logger) - return logger + return logr.NewContext(ctx, logger), logger } // GetInstance tries to retrieve the custom resource instance and perform some standard @@ -60,64 +79,60 @@ func (r *Reconciler) SetLogger(ctx *context.Context, keysAndValues ...interface{ // - cleanupFns: variadic parameter that allows passing cleanup functions that will be // run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise // the custom resource will be immediately deleted and the functions won't run. -func (r *Reconciler) GetInstance(ctx context.Context, key types.NamespacedName, - instance client.Object, finalizer *string, cleanupFns ...func()) (*ctrl.Result, error) { - logger := logr.FromContextOrDiscard(ctx) +func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj client.Object, + finalizer *string, cleanupFns ...func()) ReconcileResult { - err := r.Client.Get(ctx, key, instance) + ctx, logger := r.Logger(ctx) + err := r.Client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, obj) if err != nil { if errors.IsNotFound(err) { // Return and don't requeue - return &ctrl.Result{}, nil + return ReconcileResult{Requeue: false, Error: nil} } - return &ctrl.Result{}, err + return ReconcileResult{Requeue: false, Error: err} } - if util.IsBeingDeleted(instance) { + if util.IsBeingDeleted(obj) { // finalizer logic is only triggered if the controller - // sets a finalizer, otherwise there's notihng to be done - if finalizer != nil { + // sets a finalizer and the finalizer is still present in the + // resource + if finalizer != nil && controllerutil.ContainsFinalizer(obj, *finalizer) { - if !controllerutil.ContainsFinalizer(instance, *finalizer) { - return &ctrl.Result{}, nil - } - err := r.ManageCleanupLogic(instance, cleanupFns, logger) + err := r.ManageCleanupLogic(obj, cleanupFns, logger) if err != nil { logger.Error(err, "unable to delete instance") - result, err := ctrl.Result{}, err - return &result, err + return ReconcileResult{Requeue: false, Error: err} } - controllerutil.RemoveFinalizer(instance, *finalizer) - err = r.Client.Update(ctx, instance) + controllerutil.RemoveFinalizer(obj, *finalizer) + err = r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to update instance") - result, err := ctrl.Result{}, err - return &result, err + return ReconcileResult{Requeue: false, Error: err} } } - return &ctrl.Result{}, nil + // no finalizer, just return without doing anything + return ReconcileResult{Requeue: false, Error: nil} } - if ok := r.IsInitialized(instance, finalizer); !ok { - err := r.Client.Update(ctx, instance) + if ok := r.IsInitialized(obj, finalizer); !ok { + err := r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to initialize instance") - result, err := ctrl.Result{}, err - return &result, err + return ReconcileResult{Requeue: false, Error: err} } - return &ctrl.Result{}, nil + return ReconcileResult{Requeue: true, Error: nil} } - return nil, nil + return ReconcileResult{Requeue: false, Error: nil} } // IsInitialized can be used to check if instance is correctly initialized. // Returns false if it isn't. -func (r *Reconciler) IsInitialized(instance client.Object, finalizer *string) bool { +func (r *Reconciler) IsInitialized(obj client.Object, finalizer *string) bool { ok := true - if finalizer != nil && !controllerutil.ContainsFinalizer(instance, *finalizer) { - controllerutil.AddFinalizer(instance, *finalizer) + if finalizer != nil && !controllerutil.ContainsFinalizer(obj, *finalizer) { + controllerutil.AddFinalizer(obj, *finalizer) ok = false } @@ -125,7 +140,7 @@ func (r *Reconciler) IsInitialized(instance client.Object, finalizer *string) bo } // ManageCleanupLogic contains finalization logic for the Reconciler -func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), log logr.Logger) error { +func (r *Reconciler) ManageCleanupLogic(obj client.Object, fns []func(), log logr.Logger) error { // Call any cleanup functions passed for _, fn := range fns { fn() @@ -143,13 +158,16 @@ func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), lo // - If the resource pruner is enabled any resource owned by the custom resource not present in the list of managed // resources is deleted. The resource pruner must be enabled in the global config (see package config) and also not // explicitely disabled in the resource by the '/prune: true/false' annotation. -func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) error { +func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) ReconcileResult { managedResources := []corev1.ObjectReference{} for _, template := range list { ref, err := resource.CreateOrUpdate(ctx, r.Client, r.Scheme, owner, template) if err != nil { - return fmt.Errorf("unable to CreateOrUpdate resource: %w", err) + return ReconcileResult{ + Requeue: false, + Error: fmt.Errorf("unable to CreateOrUpdate resource: %w", err), + } } if ref != nil { managedResources = append(managedResources, *ref) @@ -159,11 +177,15 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O if isPrunerEnabled(owner) { if err := r.pruneOrphaned(ctx, owner, managedResources); err != nil { - return fmt.Errorf("unable to prune orphaned resources: %w", err) + + return ReconcileResult{ + Requeue: false, + Error: fmt.Errorf("unable to prune orphaned resources: %w", err), + } } } - return nil + return ReconcileResult{Requeue: false, Error: nil} } // SecretEventHandler returns an EventHandler for the specific client.ObjectList @@ -186,7 +208,7 @@ func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger // TODO: pass a function that can decide if the event is of interest for a given resource req := make([]reconcile.Request, 0, len(items)) for _, item := range items { - req = append(req, reconcile.Request{NamespacedName: util.ObjectKey(item)}) + req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(item)}) } return req }, diff --git a/reconciler/status.go b/reconciler/status.go index 4d0a486..f7bb549 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -16,7 +16,7 @@ import ( // status of the custom resource. It also accepts functions with signature "func() bool" that can // reconcile the status of the custom resource and return whether update is required or not. func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithAppStatus, - deployments, statefulsets []types.NamespacedName, mutators ...func() bool) error { + deployments, statefulsets []types.NamespacedName, mutators ...func() bool) ReconcileResult { logger := logr.FromContextOrDiscard(ctx) update := false status := instance.GetStatus() @@ -27,7 +27,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp deployment := &appsv1.Deployment{} deploymentStatus := status.GetDeploymentStatus(key) if err := r.Client.Get(ctx, key, deployment); err != nil { - return err + return ReconcileResult{Requeue: false, Error: err} } if !equality.Semantic.DeepEqual(deploymentStatus, deployment.Status) { @@ -42,7 +42,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp sts := &appsv1.StatefulSet{} stsStatus := status.GetStatefulSetStatus(key) if err := r.Client.Get(ctx, key, sts); err != nil { - return err + return ReconcileResult{Requeue: false, Error: err} } if !equality.Semantic.DeepEqual(stsStatus, sts.Status) { @@ -63,11 +63,11 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp if update { if err := r.Client.Status().Update(ctx, instance); err != nil { logger.Error(err, "unable to update status") - return err + return ReconcileResult{Requeue: false, Error: err} } } - return nil + return ReconcileResult{Requeue: false, Error: nil} } // ObjectWithAppStatus is an interface that implements diff --git a/resource/create_or_update.go b/resource/create_or_update.go index db29933..13f79f7 100644 --- a/resource/create_or_update.go +++ b/resource/create_or_update.go @@ -22,7 +22,8 @@ import ( ) // CreateOrUpdate cretes or updates resources. The function receives several paremters: -// - ctx: the context +// - ctx: the context. The logger is expected to be within the context, otherwise the function won't +// produce any logs. // - cl: the kubernetes API client // - scheme: the kubernetes API scheme // - owner: the object that owns the resource. Used to set the OwnerReference in the resource @@ -37,7 +38,7 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem return nil, fmt.Errorf("unable to build template: %w", err) } - key := util.ObjectKey(desired) + key := client.ObjectKeyFromObject(desired) gvk, err := apiutil.GVKForObject(desired, scheme) if err != nil { return nil, err diff --git a/resource/template.go b/resource/template.go index 0733e34..55417f8 100644 --- a/resource/template.go +++ b/resource/template.go @@ -50,27 +50,19 @@ type Template[T client.Object] struct { } // NewTemplate returns a new Template struct using the passed parameters -func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], - enabled bool, mutations ...TemplateMutationFunction) *Template[T] { +func NewTemplate[T client.Object](tb TemplateBuilderFunction[T]) *Template[T] { return &Template[T]{ - TemplateBuilder: tb, - TemplateMutations: mutations, - IsEnabled: enabled, - EnsureProperties: []Property{}, - IgnoreProperties: []Property{}, + TemplateBuilder: tb, + IsEnabled: true, } } // NewTemplateFromObjectFunction returns a new Template using the given kubernetes // object as the base. -func NewTemplateFromObjectFunction[T client.Object](fn func() T, - enabled bool, mutations ...TemplateMutationFunction) *Template[T] { +func NewTemplateFromObjectFunction[T client.Object](fn func() T) *Template[T] { return &Template[T]{ - TemplateBuilder: func(client.Object) (T, error) { return fn(), nil }, - TemplateMutations: mutations, - IsEnabled: enabled, - EnsureProperties: []Property{}, - IgnoreProperties: []Property{}, + TemplateBuilder: func(client.Object) (T, error) { return fn(), nil }, + IsEnabled: true, } } @@ -104,6 +96,30 @@ func (t *Template[T]) GetIgnoreProperties() []Property { return t.IgnoreProperties } +func (t *Template[T]) WithMutation(fn TemplateMutationFunction) *Template[T] { + if t.TemplateMutations == nil { + t.TemplateMutations = []TemplateMutationFunction{fn} + } else { + t.TemplateMutations = append(t.TemplateMutations, fn) + } + return t +} + +func (t *Template[T]) WithEnabled(enabled bool) *Template[T] { + t.IsEnabled = enabled + return t +} + +func (t *Template[T]) WithEnsureProperties(ensure []Property) *Template[T] { + t.EnsureProperties = ensure + return t +} + +func (t *Template[T]) WithIgnoreProperties(ignore []Property) *Template[T] { + t.IgnoreProperties = ignore + return t +} + // Apply chains template functions to make them composable func (t *Template[T]) Apply(mutation TemplateBuilderFunction[T]) *Template[T] { diff --git a/test/suite_test.go b/test/suite_test.go index 97faed7..7a056cf 100644 --- a/test/suite_test.go +++ b/test/suite_test.go @@ -102,8 +102,7 @@ var _ = BeforeSuite(func() { // Add controllers for testing err = (&Reconciler{ - Reconciler: *reconciler.NewFromManager(mgr). - WithLogger(ctrl.Log.WithName("controllers").WithName("Test")), + Reconciler: reconciler.NewFromManager(mgr).WithLogger(ctrl.Log.WithName("controllers").WithName("Test")), }).SetupWithManager(mgr) Expect(err).ToNot(HaveOccurred()) diff --git a/test/test_controller.go b/test/test_controller.go index 4dd1f60..b0191c3 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -34,27 +34,24 @@ import ( "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/source" ) // Reconciler reconciles a Test object // +kubebuilder:object:generate=false type Reconciler struct { - reconciler.Reconciler + *reconciler.Reconciler } // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Log.WithValues("name", req.Name, "namespace", req.Namespace) - ctx = log.IntoContext(ctx, logger) - instance := &v1alpha1.Test{} - key := types.NamespacedName{Name: req.Name, Namespace: req.Namespace} - result, err := r.GetInstance(ctx, key, instance, nil, nil) - if result != nil || err != nil { - return *result, err + ctx, _ = r.Logger(ctx, "name", req.Name, "namespace", req.Namespace) + obj := &v1alpha1.Test{} + result := r.GetInstance(ctx, req, obj, nil, nil) + if result.IsReturnAndRequeue() { + return result.Values() } resources := []resource.TemplateInterface{ @@ -92,7 +89,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu &resource.Template[*autoscalingv2.HorizontalPodAutoscaler]{ TemplateBuilder: hpa(req.Namespace), - IsEnabled: instance.Spec.HPA != nil && *instance.Spec.HPA, + IsEnabled: obj.Spec.HPA != nil && *obj.Spec.HPA, EnsureProperties: []resource.Property{ "metadata.annotations", "metadata.labels", @@ -104,7 +101,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, &resource.Template[*policyv1.PodDisruptionBudget]{ TemplateBuilder: pdb(req.Namespace), - IsEnabled: instance.Spec.PDB != nil && *instance.Spec.PDB, + IsEnabled: obj.Spec.PDB != nil && *obj.Spec.PDB, EnsureProperties: []resource.Property{ "metadata.annotations", "metadata.labels", @@ -115,9 +112,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, } - if instance.Spec.PruneService == nil || !*instance.Spec.PruneService { + if obj.Spec.PruneService == nil || !*obj.Spec.PruneService { resources = append(resources, &resource.Template[*corev1.Service]{ - TemplateBuilder: service(req.Namespace, instance.Spec.ServiceAnnotations), + TemplateBuilder: service(req.Namespace, obj.Spec.ServiceAnnotations), IsEnabled: true, EnsureProperties: []resource.Property{ "metadata.annotations", @@ -133,17 +130,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }) } - err = r.ReconcileOwnedResources(ctx, instance, resources) - if err != nil { - logger.Error(err, "unable to reconcile owned resources") - return ctrl.Result{}, err + result = r.ReconcileOwnedResources(ctx, obj, resources) + if result.IsReturnAndRequeue() { + return result.Values() } // reconcile the status - err = r.ReconcileStatus(ctx, instance, - []types.NamespacedName{{Name: "deployment", Namespace: instance.GetNamespace()}}, nil) - if err != nil { - return ctrl.Result{}, err + result = r.ReconcileStatus(ctx, obj, + []types.NamespacedName{{Name: "deployment", Namespace: obj.GetNamespace()}}, nil) + if result.IsReturnAndRequeue() { + return result.Values() } return ctrl.Result{}, nil diff --git a/test/test_controller_suite_test.go b/test/test_controller_suite_test.go index 1b35f14..ad155c2 100644 --- a/test/test_controller_suite_test.go +++ b/test/test_controller_suite_test.go @@ -69,7 +69,7 @@ var _ = Describe("Test controller", func() { for _, res := range resources { Eventually(func() error { - return k8sClient.Get(context.Background(), util.ObjectKey(res), res) + return k8sClient.Get(context.Background(), client.ObjectKeyFromObject(res), res) }, timeout, poll).ShouldNot(HaveOccurred()) } }) @@ -138,11 +138,11 @@ var _ = Describe("Test controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(func() error { - return k8sClient.Get(context.Background(), util.ObjectKey(pdb), pdb) + return k8sClient.Get(context.Background(), client.ObjectKeyFromObject(pdb), pdb) }, timeout, poll).Should(HaveOccurred()) Eventually(func() error { - return k8sClient.Get(context.Background(), util.ObjectKey(hpa), hpa) + return k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hpa), hpa) }, timeout, poll).Should(HaveOccurred()) }) @@ -156,7 +156,7 @@ var _ = Describe("Test controller", func() { Expect(err).ToNot(HaveOccurred()) Eventually(func() bool { - if err := k8sClient.Get(context.Background(), util.ObjectKey(svc), svc); err != nil { + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(svc), svc); err != nil { return false } return svc.GetAnnotations()["key"] == "value" @@ -172,7 +172,7 @@ var _ = Describe("Test controller", func() { svc := resources[1].(*corev1.Service) Eventually(func() bool { - err := k8sClient.Get(context.Background(), util.ObjectKey(svc), svc) + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(svc), svc) if err != nil && errors.IsNotFound(err) { return true } diff --git a/util/k8s.go b/util/k8s.go index fd5d037..4fd71e0 100644 --- a/util/k8s.go +++ b/util/k8s.go @@ -8,14 +8,9 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) -func ObjectKey(o client.Object) types.NamespacedName { - return types.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()} -} - // this is an ugly function to retrieve the list of Items from a // client.ObjectList because the interface doesn't have a GetItems // method From 4ffecb62d6fa83da653e12405f45cc72d5ebfee5 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 12 Dec 2023 17:41:19 +0100 Subject: [PATCH 07/20] Add basic README.md --- README.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e4f5b27..3300889 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,152 @@ Basereconciler is an attempt to create a reconciler that can be imported an used in any controller-runtime based controller to perform the most common tasks a controller usually performs. It's a bunch of code that it's typically written again and again for every and each controller and that can be abstracted to work in a more generic way to avoid the repetition and improve code mantainability. At the moment basereconciler can perform the following tasks: -* Get the custom resource and perform some common tasks on it: +* **Get the custom resource and perform some common tasks on it**: * Management of resource finalizer: some custom resources required more complex finalization logic. For this to happen a finalizer must be in place. Basereconciler can keep this finalizer in place and remove it when necessary during resource finalization. * Management of finalization logic: it checks if the resource is being finalized and executed the finalization logic passed to it if that is the case. When all finalization logic is completed it removes the finalizer on the custom resource. -* Reconcile resources owned by the custom resource: basreconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future. -* Reconcile custom resource status: if the custom resource implements a certain interface, basereconciler can also be in charge of reconciling the status. -* Resource pruner: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no logner required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation. +* **Reconcile resources owned by the custom resource**: basreconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future. +* **Reconcile custom resource status**: if the custom resource implements a certain interface, basereconciler can also be in charge of reconciling the status. +* **Resource pruner**: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no logner required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation. ## Basic Usage The following example is a kubebuilder bootstrapped controller that uses basereconciler to reconcile several resources owned by a custom resource. Explanations inline in the code. ```go +package controllers +import ( + "context" + + "github.com/3scale-ops/basereconciler/config" + "github.com/3scale-ops/basereconciler/mutators" + "github.com/3scale-ops/basereconciler/reconciler" + "github.com/3scale-ops/basereconciler/resource" + "github.com/3scale-ops/basereconciler/util" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + webappv1 "my.domain/guestbook/api/v1" +) + +// Use the init function to configure the behavior of the controller. In this case we use +// "SetDefaultReconcileConfigForGVK" to specify the paths that need to be reconciled/ignored +// for each resource type. Check the "github.com/3scale-ops/basereconciler/config" for more +// configuration options +func init() { + config.SetDefaultReconcileConfigForGVK( + schema.FromAPIVersionAndKind("v1", "Service"), + config.ReconcileConfigForGVK{ + EnsureProperties: []string{ + "metadata.annotations", + "metadata.labels", + "spec", + }, + }) + config.SetDefaultReconcileConfigForGVK( + schema.FromAPIVersionAndKind("apps/v1", "Deployment"), + config.ReconcileConfigForGVK{ + EnsureProperties: []string{ + "metadata.annotations", + "metadata.labels", + "spec", + }, + IgnoreProperties: []string{ + "metadata.annotations['deployment.kubernetes.io/revision']", + }, + }) +} + +// GuestbookReconciler reconciles a Guestbook object +type GuestbookReconciler struct { + *reconciler.Reconciler } + +// +kubebuilder:rbac:groups=webapp.my.domain,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=webapp.my.domain,resources=guestbooks/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="core",namespace=placeholder,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="apps",namespace=placeholder,resources=deployments,verbs=get;list;watch;create;update;patch;delete + +func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // configure the logger for the controller. The function also returns a modified + // copy of the context that includes the logger so it's easily passed around to other functions. + ctx, logger := r.Logger(ctx, "guestbook", req.NamespacedName) + + // GetInstance will take care of retrieveing the custom resoure from the API. It is also in charge of the resource + // finalization logic if there is one. In this example, we are configuring a finalizer in our custom resource and passing + // a finalization function that will casuse a log line to show when the resource is being deleted. + guestbook := &webappv1.Guestbook{} + result := r.GetInstance(ctx, req, guestbook, util.Pointer("guestbook-finalizer"), func() { logger.Info("finalizing resource") }) + if result.IsReturnAndRequeue() { + return result.Values() + } + + // ReconcileOwnedResources creates/updates/deletes the resoures that our custom resource owns. + // It is a list of templates, in this case generated from the base of an object we provide. + // Modifiers can be added to the template to get live values from the k8s API, like in this example + // with the Service. Check the documentation of the "github.com/3scale-ops/basereconciler/resource" + // for more information on building templates. + result = r.ReconcileOwnedResources(ctx, guestbook, []resource.TemplateInterface{ + + resource.NewTemplateFromObjectFunction[*appsv1.Deployment]( + func() *appsv1.Deployment { + return &appsv1.Deployment{ + // define your object here + } + }), + + resource.NewTemplateFromObjectFunction[*corev1.Service]( + func() *corev1.Service { + return &corev1.Service{ + // define your object here + } + }). + // Retrieve the live values that kube-controller-manager sets + // in the Service spec to avoid overwrting them + WithMutation(mutators.SetServiceLiveValues()). + // There are some useful mutations in the "github.com/3scale-ops/basereconciler/mutators" + // package or you can pass your own mutation functions + WithMutation(func(ctx context.Context, cl client.Client, desired client.Object) error { + // your mutation logic here + return nil + }). + // The templates are reconciled using the global config defined in the init() function + // but in this case we are passing a custom config that will apply + // only to the reconciliation of this template + WithEnsureProperties([]resource.Property{"spec"}). + WithIgnoreProperties([]resource.Property{"spec.clusterIP", "spec.clusterIPs"}), + }) + + if result.IsReturnAndRequeue() { + return result.Values() + } + + return ctrl.Result{}, nil +} + +func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&webappv1.Guestbook{}). + // add the watches for the specific resource types that the + // custom resource owns to watch for changes on those + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Complete(r) +} +``` + +Then you just need to register the controller with the controller-runtime manager and you are all set! + +```go +[...] + if err = (&controllers.GuestbookReconciler{ + Reconciler: reconciler.NewFromManager(mgr).WithLogger(ctrl.Log.WithName("controllers").WithName("Guestbook")), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Guestbook") + os.Exit(1) + } +[...] ``` \ No newline at end of file From b4e057c89e35fb2e12d9f01fb203b410857ba77e Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 12 Dec 2023 22:31:31 +0100 Subject: [PATCH 08/20] Some small improvements --- reconciler/reconciler.go | 44 +++++++++++++++++++++++----------------- reconciler/status.go | 10 ++++----- resource/template.go | 13 ++++++++++-- util/k8s.go | 5 +++++ 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 55bb1a4..ef25970 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -3,6 +3,7 @@ package reconciler import ( "context" "fmt" + "time" "github.com/3scale-ops/basereconciler/resource" "github.com/3scale-ops/basereconciler/util" @@ -20,17 +21,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type ReconcileResult struct { - Requeue bool - Error error +type Result struct { + Requeue bool + RequeueAfter time.Duration + Error error } -func (result ReconcileResult) IsReturnAndRequeue() bool { +func (result Result) IsReturnAndRequeue() bool { return result.Requeue || result.Error != nil } -func (result ReconcileResult) Values() (ctrl.Result, error) { - return ctrl.Result{Requeue: result.Requeue}, result.Error +func (result Result) Values() (ctrl.Result, error) { + return ctrl.Result{ + Requeue: result.Requeue, + RequeueAfter: result.RequeueAfter, + }, + result.Error } // Reconciler computes a list of resources that it needs to keep in place @@ -80,16 +86,16 @@ func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) ( // run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise // the custom resource will be immediately deleted and the functions won't run. func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj client.Object, - finalizer *string, cleanupFns ...func()) ReconcileResult { + finalizer *string, cleanupFns ...func()) Result { ctx, logger := r.Logger(ctx) err := r.Client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, obj) if err != nil { if errors.IsNotFound(err) { // Return and don't requeue - return ReconcileResult{Requeue: false, Error: nil} + return Result{Requeue: false, Error: nil} } - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } if util.IsBeingDeleted(obj) { @@ -102,29 +108,29 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj err := r.ManageCleanupLogic(obj, cleanupFns, logger) if err != nil { logger.Error(err, "unable to delete instance") - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } controllerutil.RemoveFinalizer(obj, *finalizer) err = r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to update instance") - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } } // no finalizer, just return without doing anything - return ReconcileResult{Requeue: false, Error: nil} + return Result{Requeue: false, Error: nil} } if ok := r.IsInitialized(obj, finalizer); !ok { err := r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to initialize instance") - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } - return ReconcileResult{Requeue: true, Error: nil} + return Result{Requeue: true, Error: nil} } - return ReconcileResult{Requeue: false, Error: nil} + return Result{Requeue: false, Error: nil} } // IsInitialized can be used to check if instance is correctly initialized. @@ -158,13 +164,13 @@ func (r *Reconciler) ManageCleanupLogic(obj client.Object, fns []func(), log log // - If the resource pruner is enabled any resource owned by the custom resource not present in the list of managed // resources is deleted. The resource pruner must be enabled in the global config (see package config) and also not // explicitely disabled in the resource by the '/prune: true/false' annotation. -func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) ReconcileResult { +func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) Result { managedResources := []corev1.ObjectReference{} for _, template := range list { ref, err := resource.CreateOrUpdate(ctx, r.Client, r.Scheme, owner, template) if err != nil { - return ReconcileResult{ + return Result{ Requeue: false, Error: fmt.Errorf("unable to CreateOrUpdate resource: %w", err), } @@ -178,14 +184,14 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O if isPrunerEnabled(owner) { if err := r.pruneOrphaned(ctx, owner, managedResources); err != nil { - return ReconcileResult{ + return Result{ Requeue: false, Error: fmt.Errorf("unable to prune orphaned resources: %w", err), } } } - return ReconcileResult{Requeue: false, Error: nil} + return Result{Requeue: false, Error: nil} } // SecretEventHandler returns an EventHandler for the specific client.ObjectList diff --git a/reconciler/status.go b/reconciler/status.go index f7bb549..89fe1fd 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -16,7 +16,7 @@ import ( // status of the custom resource. It also accepts functions with signature "func() bool" that can // reconcile the status of the custom resource and return whether update is required or not. func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithAppStatus, - deployments, statefulsets []types.NamespacedName, mutators ...func() bool) ReconcileResult { + deployments, statefulsets []types.NamespacedName, mutators ...func() bool) Result { logger := logr.FromContextOrDiscard(ctx) update := false status := instance.GetStatus() @@ -27,7 +27,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp deployment := &appsv1.Deployment{} deploymentStatus := status.GetDeploymentStatus(key) if err := r.Client.Get(ctx, key, deployment); err != nil { - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } if !equality.Semantic.DeepEqual(deploymentStatus, deployment.Status) { @@ -42,7 +42,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp sts := &appsv1.StatefulSet{} stsStatus := status.GetStatefulSetStatus(key) if err := r.Client.Get(ctx, key, sts); err != nil { - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } if !equality.Semantic.DeepEqual(stsStatus, sts.Status) { @@ -63,11 +63,11 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp if update { if err := r.Client.Status().Update(ctx, instance); err != nil { logger.Error(err, "unable to update status") - return ReconcileResult{Requeue: false, Error: err} + return Result{Requeue: false, Error: err} } } - return ReconcileResult{Requeue: false, Error: nil} + return Result{Requeue: false, Error: nil} } // ObjectWithAppStatus is an interface that implements diff --git a/resource/template.go b/resource/template.go index 55417f8..8a41e87 100644 --- a/resource/template.go +++ b/resource/template.go @@ -53,7 +53,8 @@ type Template[T client.Object] struct { func NewTemplate[T client.Object](tb TemplateBuilderFunction[T]) *Template[T] { return &Template[T]{ TemplateBuilder: tb, - IsEnabled: true, + // default to true + IsEnabled: true, } } @@ -62,7 +63,8 @@ func NewTemplate[T client.Object](tb TemplateBuilderFunction[T]) *Template[T] { func NewTemplateFromObjectFunction[T client.Object](fn func() T) *Template[T] { return &Template[T]{ TemplateBuilder: func(client.Object) (T, error) { return fn(), nil }, - IsEnabled: true, + // default to true + IsEnabled: true, } } @@ -105,6 +107,13 @@ func (t *Template[T]) WithMutation(fn TemplateMutationFunction) *Template[T] { return t } +func (t *Template[T]) WithMutations(fns []TemplateMutationFunction) *Template[T] { + for _, fn := range fns { + t.WithMutation(fn) + } + return t +} + func (t *Template[T]) WithEnabled(enabled bool) *Template[T] { t.IsEnabled = enabled return t diff --git a/util/k8s.go b/util/k8s.go index 4fd71e0..fd5d037 100644 --- a/util/k8s.go +++ b/util/k8s.go @@ -8,9 +8,14 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) +func ObjectKey(o client.Object) types.NamespacedName { + return types.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()} +} + // this is an ugly function to retrieve the list of Items from a // client.ObjectList because the interface doesn't have a GetItems // method From 2fb41b738e41f6d0d1364fc680eaf2cc98846d3b Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Wed, 13 Dec 2023 19:04:19 +0100 Subject: [PATCH 09/20] Fix requeue/return logic --- reconciler/reconciler.go | 45 ++++++++++++----------- reconciler/reconciler_test.go | 67 +++++++++++++++++++++++++++++++++++ reconciler/status.go | 8 ++--- test/test_controller.go | 6 ++-- 4 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 reconciler/reconciler_test.go diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index ef25970..9738329 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -21,19 +21,28 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +type action int + +const ( + ContinueAction action = 0 + ReturnAction action = 1 + ReturnAndRequeueAction action = 2 +) + type Result struct { - Requeue bool + Action action RequeueAfter time.Duration Error error } -func (result Result) IsReturnAndRequeue() bool { - return result.Requeue || result.Error != nil +func (result Result) ShouldReturn() bool { + return result.Action == ReturnAction || result.Action == ReturnAndRequeueAction || result.Error != nil } func (result Result) Values() (ctrl.Result, error) { + return ctrl.Result{ - Requeue: result.Requeue, + Requeue: func() bool { return result.Action == ReturnAndRequeueAction }(), RequeueAfter: result.RequeueAfter, }, result.Error @@ -93,9 +102,9 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj if err != nil { if errors.IsNotFound(err) { // Return and don't requeue - return Result{Requeue: false, Error: nil} + return Result{Action: ReturnAction} } - return Result{Requeue: false, Error: err} + return Result{Error: err} } if util.IsBeingDeleted(obj) { @@ -108,29 +117,29 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj err := r.ManageCleanupLogic(obj, cleanupFns, logger) if err != nil { logger.Error(err, "unable to delete instance") - return Result{Requeue: false, Error: err} + return Result{Error: err} } controllerutil.RemoveFinalizer(obj, *finalizer) err = r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to update instance") - return Result{Requeue: false, Error: err} + return Result{Error: err} } } // no finalizer, just return without doing anything - return Result{Requeue: false, Error: nil} + return Result{Error: nil} } if ok := r.IsInitialized(obj, finalizer); !ok { err := r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to initialize instance") - return Result{Requeue: false, Error: err} + return Result{Error: err} } - return Result{Requeue: true, Error: nil} + return Result{Action: ReturnAndRequeueAction} } - return Result{Requeue: false, Error: nil} + return Result{Action: ContinueAction} } // IsInitialized can be used to check if instance is correctly initialized. @@ -170,10 +179,7 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O for _, template := range list { ref, err := resource.CreateOrUpdate(ctx, r.Client, r.Scheme, owner, template) if err != nil { - return Result{ - Requeue: false, - Error: fmt.Errorf("unable to CreateOrUpdate resource: %w", err), - } + return Result{Error: fmt.Errorf("unable to CreateOrUpdate resource: %w", err)} } if ref != nil { managedResources = append(managedResources, *ref) @@ -184,14 +190,11 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O if isPrunerEnabled(owner) { if err := r.pruneOrphaned(ctx, owner, managedResources); err != nil { - return Result{ - Requeue: false, - Error: fmt.Errorf("unable to prune orphaned resources: %w", err), - } + return Result{Error: fmt.Errorf("unable to prune orphaned resources: %w", err)} } } - return Result{Requeue: false, Error: nil} + return Result{Action: ContinueAction} } // SecretEventHandler returns an EventHandler for the specific client.ObjectList diff --git a/reconciler/reconciler_test.go b/reconciler/reconciler_test.go new file mode 100644 index 0000000..c2e593a --- /dev/null +++ b/reconciler/reconciler_test.go @@ -0,0 +1,67 @@ +package reconciler + +import ( + "fmt" + "testing" + "time" +) + +func TestResult_ShouldReturn(t *testing.T) { + type fields struct { + Action action + RequeueAfter time.Duration + Error error + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "Empty result returns false", + fields: fields{}, + want: false, + }, + { + name: "continueAction returns false", + fields: fields{ + Action: ContinueAction, + Error: nil, + }, + want: false, + }, + { + name: "returnAction returns true", + fields: fields{ + Action: ReturnAction, + }, + want: true, + }, + { + name: "Error returns true", + fields: fields{ + Error: fmt.Errorf("error"), + }, + want: true, + }, + { + name: "returnAndRequeueAction true returns true", + fields: fields{ + Action: ReturnAndRequeueAction, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Result{ + Action: tt.fields.Action, + RequeueAfter: tt.fields.RequeueAfter, + Error: tt.fields.Error, + } + if got := result.ShouldReturn(); got != tt.want { + t.Errorf("Result.ShouldReturn() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/reconciler/status.go b/reconciler/status.go index 89fe1fd..c2dd9aa 100644 --- a/reconciler/status.go +++ b/reconciler/status.go @@ -27,7 +27,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp deployment := &appsv1.Deployment{} deploymentStatus := status.GetDeploymentStatus(key) if err := r.Client.Get(ctx, key, deployment); err != nil { - return Result{Requeue: false, Error: err} + return Result{Error: err} } if !equality.Semantic.DeepEqual(deploymentStatus, deployment.Status) { @@ -42,7 +42,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp sts := &appsv1.StatefulSet{} stsStatus := status.GetStatefulSetStatus(key) if err := r.Client.Get(ctx, key, sts); err != nil { - return Result{Requeue: false, Error: err} + return Result{Error: err} } if !equality.Semantic.DeepEqual(stsStatus, sts.Status) { @@ -63,11 +63,11 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp if update { if err := r.Client.Status().Update(ctx, instance); err != nil { logger.Error(err, "unable to update status") - return Result{Requeue: false, Error: err} + return Result{Error: err} } } - return Result{Requeue: false, Error: nil} + return Result{Action: ContinueAction} } // ObjectWithAppStatus is an interface that implements diff --git a/test/test_controller.go b/test/test_controller.go index b0191c3..c3c00b6 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -50,7 +50,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu ctx, _ = r.Logger(ctx, "name", req.Name, "namespace", req.Namespace) obj := &v1alpha1.Test{} result := r.GetInstance(ctx, req, obj, nil, nil) - if result.IsReturnAndRequeue() { + if result.ShouldReturn() { return result.Values() } @@ -131,14 +131,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } result = r.ReconcileOwnedResources(ctx, obj, resources) - if result.IsReturnAndRequeue() { + if result.ShouldReturn() { return result.Values() } // reconcile the status result = r.ReconcileStatus(ctx, obj, []types.NamespacedName{{Name: "deployment", Namespace: obj.GetNamespace()}}, nil) - if result.IsReturnAndRequeue() { + if result.ShouldReturn() { return result.Values() } From b5a788cadc17fa72ede6ae7e4c7d3503ca67996c Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Thu, 14 Dec 2023 15:25:47 +0100 Subject: [PATCH 10/20] Separate resource diff code from update code --- reconciler/reconciler.go | 13 ++-- resource/create_or_update.go | 111 ++++++++++++++++++++++------------- resource/property.go | 9 +-- resource/property_test.go | 71 +++++++++------------- 4 files changed, 108 insertions(+), 96 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 9738329..374eeb7 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -21,12 +21,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type action int +type action string const ( - ContinueAction action = 0 - ReturnAction action = 1 - ReturnAndRequeueAction action = 2 + ContinueAction action = "Continue" + ReturnAction action = "Return" + ReturnAndRequeueAction action = "ReturnAndRequeue" ) type Result struct { @@ -127,8 +127,9 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj } } - // no finalizer, just return without doing anything - return Result{Error: nil} + // object being deleted, return without doing anything + // and stop the reconcile loop + return Result{Action: ReturnAction} } if ok := r.IsInitialized(obj, finalizer); !ok { diff --git a/resource/create_or_update.go b/resource/create_or_update.go index 13f79f7..26dafa8 100644 --- a/resource/create_or_update.go +++ b/resource/create_or_update.go @@ -9,6 +9,7 @@ import ( "github.com/3scale-ops/basereconciler/util" "github.com/go-logr/logr" "github.com/nsf/jsondiff" + "github.com/ohler55/ojg/jp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" @@ -85,59 +86,40 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem return nil, wrapError("unable to retrieve config for resource reconciler", key, gvk, err) } - // normalizedLive is a struct that will be populated with only the reconciled - // properties and their respective live values. It will be used to compare it with - // the desire and determine in an update is required. - normalizedLive, err := util.NewObjectFromGVK(gvk, scheme) + // normalize both live and desired for comparison + normalizedDesired, err := normalize(desired, ensure, ignore, gvk, scheme) if err != nil { - return nil, wrapError("unable to create object from GVK", key, gvk, err) + wrapError("unable to normalize desired", key, gvk, err) } - normalizedLive.SetName(desired.GetName()) - normalizedLive.SetNamespace(desired.GetNamespace()) - // convert to unstructured - u_desired, err := runtime.DefaultUnstructuredConverter.ToUnstructured(desired) + normalizedLive, err := normalize(live, ensure, ignore, gvk, scheme) if err != nil { - return nil, wrapError("unable to convert to unstructured", key, gvk, err) - + wrapError("unable to normalize live", key, gvk, err) } - u_live, err := runtime.DefaultUnstructuredConverter.ToUnstructured(live) - if err != nil { - return nil, wrapError("unable to convert to unstructured", key, gvk, err) - } + if !equality.Semantic.DeepEqual(normalizedLive, normalizedDesired) { + logger.V(1).Info("resource update required", "diff", printfDiff(normalizedLive, normalizedDesired)) - u_normalizedLive, err := runtime.DefaultUnstructuredConverter.ToUnstructured(normalizedLive) - if err != nil { - return nil, wrapError("unable to convert to unstructured", key, gvk, err) - } + // convert to unstructured + u_normalizedDesired, err := runtime.DefaultUnstructuredConverter.ToUnstructured(normalizedDesired) + if err != nil { + return nil, wrapError("unable to convert to unstructured", key, gvk, err) - // reconcile properties - for _, property := range ensure { - if err := property.reconcile(u_live, u_desired, u_normalizedLive, logger); err != nil { - return nil, wrapError(fmt.Sprintf("unable to reconcile property %s", property), key, gvk, err) } - } - // ignore properties - for _, property := range ignore { - for _, m := range []map[string]any{u_live, u_desired, u_normalizedLive} { - if err := property.ignore(m); err != nil { - return nil, wrapError(fmt.Sprintf("unable to ignore property %s", property), key, gvk, err) + u_live, err := runtime.DefaultUnstructuredConverter.ToUnstructured(live) + if err != nil { + return nil, wrapError("unable to convert to unstructured", key, gvk, err) + } + + // reconcile properties + for _, property := range ensure { + if err := property.reconcile(u_live, u_normalizedDesired, logger); err != nil { + return nil, wrapError(fmt.Sprintf("unable to reconcile property %s", property), key, gvk, err) } } - } - // do the comparison using structs so "equality.Semantic" can be used - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u_normalizedLive, normalizedLive); err != nil { - return nil, wrapError("unable to convert from unstructured", key, gvk, err) - } - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u_desired, desired); err != nil { - return nil, wrapError("unable to convert from unstructured", key, gvk, err) - } - if !equality.Semantic.DeepEqual(normalizedLive, desired) { - logger.V(1).Info("resource update required", "diff", printfDiff(normalizedLive, desired)) - err := cl.Update(ctx, client.Object(&unstructured.Unstructured{Object: u_live})) + err = cl.Update(ctx, client.Object(&unstructured.Unstructured{Object: u_live})) if err != nil { return nil, wrapError("unable to update resource", key, gvk, err) } @@ -147,6 +129,55 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem return util.ObjectReference(live, gvk), nil } +func normalize(o client.Object, ensure, ignore []Property, gvk schema.GroupVersionKind, s *runtime.Scheme) (client.Object, error) { + + in, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) + if err != nil { + return nil, err + } + u_normalized := map[string]any{} + + for _, p := range ensure { + expr, err := jp.ParseString(p.jsonPath()) + if err != nil { + return nil, fmt.Errorf("unable to parse JSONPath '%s': %w", p.jsonPath(), err) + } + val := expr.Get(in) + + switch len(val) { + case 0: + continue + case 1: + if err := expr.Set(u_normalized, val[0]); err != nil { + return nil, fmt.Errorf("usable to add value '%v' in JSONPath '%s'", val[0], p.jsonPath()) + } + default: + return nil, fmt.Errorf("multi-valued JSONPath (%s) not supported for 'ensure' properties", p.jsonPath()) + } + + } + + for _, p := range ignore { + expr, err := jp.ParseString(p.jsonPath()) + if err != nil { + return nil, fmt.Errorf("unable to parse JSONPath '%s': %w", p.jsonPath(), err) + } + if err = expr.Del(u_normalized); err != nil { + return nil, fmt.Errorf("unable to parse delete JSONPath '%s' from unstructured: %w", p.jsonPath(), err) + } + } + + normalized, err := util.NewObjectFromGVK(gvk, s) + if err != nil { + return nil, err + } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u_normalized, normalized); err != nil { + return nil, err + } + + return normalized, nil +} + func printfDiff(a, b client.Object) string { ajson, err := json.Marshal(a) if err != nil { diff --git a/resource/property.go b/resource/property.go index fcde261..d1e954b 100644 --- a/resource/property.go +++ b/resource/property.go @@ -24,7 +24,7 @@ type Property string func (p Property) jsonPath() string { return string(p) } -func (p Property) reconcile(u_live, u_desired, u_normalizedLive map[string]any, logger logr.Logger) error { +func (p Property) reconcile(u_live, u_desired map[string]any, logger logr.Logger) error { expr, err := jp.ParseString(p.jsonPath()) if err != nil { return fmt.Errorf("unable to parse JSONPath '%s': %w", p.jsonPath(), err) @@ -36,13 +36,6 @@ func (p Property) reconcile(u_live, u_desired, u_normalizedLive map[string]any, return fmt.Errorf("multi-valued JSONPath (%s) not supported when reconciling properties", p.jsonPath()) } - // store the live value for later comparison in u_normalizedLive - if len(liveVal) != 0 { - if err := expr.Set(u_normalizedLive, liveVal[0]); err != nil { - return fmt.Errorf("usable to add value '%v' in JSONPath '%s'", liveVal[0], p.jsonPath()) - } - } - switch delta(len(desiredVal), len(liveVal)) { case missingInBoth: diff --git a/resource/property_test.go b/resource/property_test.go index 23a25ac..975a207 100644 --- a/resource/property_test.go +++ b/resource/property_test.go @@ -10,75 +10,65 @@ import ( func TestProperty_Reconcile(t *testing.T) { type args struct { - u_live map[string]any - u_desired map[string]any - u_normalizedLive map[string]any - logger logr.Logger + u_live map[string]any + u_desired map[string]any + logger logr.Logger } tests := []struct { - name string - p Property - args args - wantErr bool - wantLive map[string]any - wantNormalizedLive map[string]any + name string + p Property + args args + wantErr bool + wantLive map[string]any }{ { name: "PresentInBoth", p: "a.b.c", args: args{ - u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, - u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, - u_normalizedLive: map[string]any{}, - logger: logr.Discard(), + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, + u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, + logger: logr.Discard(), }, - wantErr: false, - wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue", "d": 1}}}, - wantNormalizedLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value"}}}, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue", "d": 1}}}, }, { name: "MissingInBoth", p: "a.b.c", args: args{ - u_live: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, - u_desired: map[string]any{}, - u_normalizedLive: map[string]any{}, - logger: logr.Discard(), + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, + u_desired: map[string]any{}, + logger: logr.Discard(), }, - wantErr: false, - wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, - wantNormalizedLive: map[string]any{}, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, }, { name: "PresentInDesiredMissingFromLive", p: "a.b.c", args: args{ - u_live: map[string]any{"a": map[string]any{}}, - u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, - u_normalizedLive: map[string]any{}, - logger: logr.Discard(), + u_live: map[string]any{"a": map[string]any{}}, + u_desired: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, + logger: logr.Discard(), }, - wantErr: false, - wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, - wantNormalizedLive: map[string]any{}, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "newValue"}}}, }, { name: "MissingFromDesiredPresentInLive", p: "a.b.c", args: args{ - u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, - u_desired: map[string]any{"a": map[string]any{}}, - u_normalizedLive: map[string]any{}, - logger: logr.Discard(), + u_live: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value", "d": 1}}}, + u_desired: map[string]any{"a": map[string]any{}}, + logger: logr.Discard(), }, - wantErr: false, - wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, - wantNormalizedLive: map[string]any{"a": map[string]any{"b": map[string]any{"c": "value"}}}, + wantErr: false, + wantLive: map[string]any{"a": map[string]any{"b": map[string]any{"d": 1}}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.p.reconcile(tt.args.u_live, tt.args.u_desired, tt.args.u_normalizedLive, tt.args.logger) + err := tt.p.reconcile(tt.args.u_live, tt.args.u_desired, tt.args.logger) if (err != nil) != tt.wantErr { t.Errorf("Property.Reconcile() error = %v, wantErr %v", err, tt.wantErr) return @@ -86,9 +76,6 @@ func TestProperty_Reconcile(t *testing.T) { if diff := cmp.Diff(tt.args.u_live, tt.wantLive); len(diff) > 0 { t.Errorf("Property.Reconcile() diff in live %v", diff) } - if diff := cmp.Diff(tt.args.u_normalizedLive, tt.wantNormalizedLive); len(diff) > 0 { - t.Errorf("Property.Reconcile() diff in normalizedLive %v", diff) - } }) } } From 4c917d0337e4c733e353e7fa9ed4d1f6c1b62fc1 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Thu, 14 Dec 2023 16:27:00 +0100 Subject: [PATCH 11/20] Rename GetInstance to ManageResourceLifecyle and improve option semantics --- reconciler/reconciler.go | 64 ++++++++++++++++++++++++++++++++-------- test/test_controller.go | 2 +- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 374eeb7..4e0caee 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -48,6 +48,39 @@ func (result Result) Values() (ctrl.Result, error) { result.Error } +var options = struct { + finalizer *string + finalizationLogic []finalizationFunction +}{ + finalizer: nil, + finalizationLogic: []finalizationFunction{}, +} + +// LifecycleOption is an interface that defines options that can be passed to +// the reconciler's ManageResourceLifecycle() function +type LifecycleOption interface { + applyToLifecycleOptions() +} + +type finalizer string + +func (f finalizer) applyToLifecycleOptions() { + options.finalizer = util.Pointer(string(f)) +} +func WithFinalizer(f string) finalizer { + return finalizer(f) +} + +type finalizationFunction func(context.Context, client.Client) error + +func (fn finalizationFunction) applyToLifecycleOptions() { + options.finalizationLogic = append(options.finalizationLogic, fn) +} + +func WithFinalizationFunc(fn func(context.Context, client.Client) error) finalizationFunction { + return fn +} + // Reconciler computes a list of resources that it needs to keep in place type Reconciler struct { client.Client @@ -86,16 +119,20 @@ func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) ( return logr.NewContext(ctx, logger), logger } -// GetInstance tries to retrieve the custom resource instance and perform some standard -// tasks like initialization and cleanup. The behaviour can be modified depending on the -// parameters passed to the function: +// ManageResourceLifecycle manages the lifecycle of the resource, from initialization to +// finalization and deletion. +// The behaviour can be modified depending on the options passed to the function: // - finalizer: if a non-nil finalizer is passed to the function, it will ensure that the // custom resource has a finalizer in place, updasting it if required. // - cleanupFns: variadic parameter that allows passing cleanup functions that will be // run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise // the custom resource will be immediately deleted and the functions won't run. -func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj client.Object, - finalizer *string, cleanupFns ...func()) Result { +func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile.Request, obj client.Object, + opts ...LifecycleOption) Result { + + for _, o := range opts { + o.applyToLifecycleOptions() + } ctx, logger := r.Logger(ctx) err := r.Client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, obj) @@ -112,14 +149,14 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj // finalizer logic is only triggered if the controller // sets a finalizer and the finalizer is still present in the // resource - if finalizer != nil && controllerutil.ContainsFinalizer(obj, *finalizer) { + if options.finalizer != nil && controllerutil.ContainsFinalizer(obj, *options.finalizer) { - err := r.ManageCleanupLogic(obj, cleanupFns, logger) + err := r.Finalize(ctx, options.finalizationLogic, logger) if err != nil { logger.Error(err, "unable to delete instance") return Result{Error: err} } - controllerutil.RemoveFinalizer(obj, *finalizer) + controllerutil.RemoveFinalizer(obj, *options.finalizer) err = r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to update instance") @@ -132,7 +169,7 @@ func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj return Result{Action: ReturnAction} } - if ok := r.IsInitialized(obj, finalizer); !ok { + if ok := r.IsInitialized(obj, options.finalizer); !ok { err := r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to initialize instance") @@ -155,11 +192,14 @@ func (r *Reconciler) IsInitialized(obj client.Object, finalizer *string) bool { return ok } -// ManageCleanupLogic contains finalization logic for the Reconciler -func (r *Reconciler) ManageCleanupLogic(obj client.Object, fns []func(), log logr.Logger) error { +// Finalize contains finalization logic for the Reconciler +func (r *Reconciler) Finalize(ctx context.Context, fns []finalizationFunction, log logr.Logger) error { // Call any cleanup functions passed for _, fn := range fns { - fn() + err := fn(ctx, r.Client) + if err != nil { + return err + } } return nil } diff --git a/test/test_controller.go b/test/test_controller.go index c3c00b6..dbeb814 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -49,7 +49,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu ctx, _ = r.Logger(ctx, "name", req.Name, "namespace", req.Namespace) obj := &v1alpha1.Test{} - result := r.GetInstance(ctx, req, obj, nil, nil) + result := r.ManageResourceLifecycle(ctx, req, obj, nil, nil) if result.ShouldReturn() { return result.Values() } From 150eeccd10965fc3361963100bc525d4c341f04a Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Thu, 14 Dec 2023 17:41:21 +0100 Subject: [PATCH 12/20] Update README example with latest changes --- README.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3300889..b0981d6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ import ( "github.com/3scale-ops/basereconciler/mutators" "github.com/3scale-ops/basereconciler/reconciler" "github.com/3scale-ops/basereconciler/resource" - "github.com/3scale-ops/basereconciler/util" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,25 +39,29 @@ import ( // configuration options func init() { config.SetDefaultReconcileConfigForGVK( - schema.FromAPIVersionAndKind("v1", "Service"), + schema.FromAPIVersionAndKind("apps/v1", "Deployment"), config.ReconcileConfigForGVK{ EnsureProperties: []string{ "metadata.annotations", "metadata.labels", "spec", }, + IgnoreProperties: []string{ + "metadata.annotations['deployment.kubernetes.io/revision']", + }, }) config.SetDefaultReconcileConfigForGVK( - schema.FromAPIVersionAndKind("apps/v1", "Deployment"), + // specifying a config for an empty GVK will + // set a default fallback config for any gvk that is not + // explicitely declared in the configuration. Think of it + // as a wildcard. + schema.GroupVersionKind{}, config.ReconcileConfigForGVK{ EnsureProperties: []string{ "metadata.annotations", "metadata.labels", "spec", }, - IgnoreProperties: []string{ - "metadata.annotations['deployment.kubernetes.io/revision']", - }, }) } @@ -77,12 +80,19 @@ func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // copy of the context that includes the logger so it's easily passed around to other functions. ctx, logger := r.Logger(ctx, "guestbook", req.NamespacedName) - // GetInstance will take care of retrieveing the custom resoure from the API. It is also in charge of the resource - // finalization logic if there is one. In this example, we are configuring a finalizer in our custom resource and passing + // ManageResourceLifecycle will take care of retrieving the custom resoure from the API. It is also in charge of the resource + // lifecycle: initialization and finalization logic. In this example, we are configuring a finalizer in our custom resource and passing // a finalization function that will casuse a log line to show when the resource is being deleted. guestbook := &webappv1.Guestbook{} - result := r.GetInstance(ctx, req, guestbook, util.Pointer("guestbook-finalizer"), func() { logger.Info("finalizing resource") }) - if result.IsReturnAndRequeue() { + result := r.ManageResourceLifecycle(ctx, req, guestbook, + reconciler.WithFinalizer("guestbook-finalizer"), + reconciler.WithFinalizationFunc( + func(context.Context, client.Client) error { + logger.Info("finalizing resource") + return nil + }), + ) + if result.ShouldReturn() { return result.Values() } @@ -122,7 +132,7 @@ func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( WithIgnoreProperties([]resource.Property{"spec.clusterIP", "spec.clusterIPs"}), }) - if result.IsReturnAndRequeue() { + if result.ShouldReturn() { return result.Values() } From 23ee58ac624711003e76c5d520a5d57af3a90a72 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Fri, 15 Dec 2023 11:32:46 +0100 Subject: [PATCH 13/20] Fix bug in tests --- test/test_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_controller.go b/test/test_controller.go index dbeb814..9abab93 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -49,7 +49,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu ctx, _ = r.Logger(ctx, "name", req.Name, "namespace", req.Namespace) obj := &v1alpha1.Test{} - result := r.ManageResourceLifecycle(ctx, req, obj, nil, nil) + result := r.ManageResourceLifecycle(ctx, req, obj) if result.ShouldReturn() { return result.Values() } From 6a8129d6c0bad36c1f79dbe4f4a517fe943ec739 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Fri, 15 Dec 2023 15:55:07 +0100 Subject: [PATCH 14/20] Generalize SecretEventHandler, now called FilteredEventHandler --- reconciler/reconciler.go | 30 ++++++++++++++++-------------- test/test_controller.go | 7 ++++++- test/test_controller_suite_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 4e0caee..8dc27d2 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -86,7 +86,6 @@ type Reconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme - gvk schema.GroupVersionKind typeTracker typeTracker } @@ -95,11 +94,6 @@ func NewFromManager(mgr manager.Manager) *Reconciler { return &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logr.Discard()} } -func (r *Reconciler) WithGVK(apiVersion, kind string) *Reconciler { - r.gvk = schema.FromAPIVersionAndKind(apiVersion, kind) - return r -} - // WithLogger sets the Reconciler logger func (r *Reconciler) WithLogger(logger logr.Logger) *Reconciler { r.Log = logger @@ -238,12 +232,18 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O return Result{Action: ContinueAction} } -// SecretEventHandler returns an EventHandler for the specific client.ObjectList -// list object passed as parameter -// TODO: generalize this to watch any object type -func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger) handler.EventHandler { +// FilteredEventHandler returns an EventHandler for the specific client.ObjectList +// passed as parameter. It will produce reconcile requests for any client.Object of the +// given type that returns true when passed to the filter function. If the filter function +// is "nil" all the listed object will receive a reconcile request. +// The filter function receives both the object that generated the event and the object that +// might need to be reconciled in response to that event. Depending on whether it returns true +// or false the reconciler request will be generated or not. +func (r *Reconciler) FilteredEventHandler(ol client.ObjectList, + filter func(event client.Object, o client.Object) bool, logger logr.Logger) handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc( - func(o client.Object) []reconcile.Request { + func(event client.Object) []reconcile.Request { if err := r.Client.List(context.TODO(), ol); err != nil { logger.Error(err, "unable to retrieve the list of resources") return []reconcile.Request{} @@ -253,11 +253,13 @@ func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger return []reconcile.Request{} } - // This is a bit undiscriminate as we don't have a way to detect which - // resources are interested in the event, so we just wake them all up - // TODO: pass a function that can decide if the event is of interest for a given resource req := make([]reconcile.Request, 0, len(items)) for _, item := range items { + if filter != nil { + if !filter(event, item) { + continue + } + } req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(item)}) } return req diff --git a/test/test_controller.go b/test/test_controller.go index 9abab93..1c7d696 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -154,7 +154,12 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&policyv1.PodDisruptionBudget{}). Owns(&autoscalingv2.HorizontalPodAutoscaler{}). Watches(&source.Kind{Type: &corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: "Secret"}}}, - r.SecretEventHandler(&v1alpha1.TestList{}, r.Log)). + r.FilteredEventHandler( + &v1alpha1.TestList{}, + func(event, o client.Object) bool { + return event.GetName() == "secret" + }, + r.Log)). Complete(r) } diff --git a/test/test_controller_suite_test.go b/test/test_controller_suite_test.go index ad155c2..a83ba03 100644 --- a/test/test_controller_suite_test.go +++ b/test/test_controller_suite_test.go @@ -2,6 +2,7 @@ package test import ( "context" + "time" "github.com/3scale-ops/basereconciler/test/api/v1alpha1" "github.com/3scale-ops/basereconciler/util" @@ -126,6 +127,31 @@ var _ = Describe("Test controller", func() { }, timeout, poll).Should(BeTrue()) }) + It("Ignores changes in other secrets", func() { + + dep := resources[0].(*appsv1.Deployment) + // Annotations should be empty when Secret does not exists + resourceVersion := dep.GetResourceVersion() + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "some-other-secret", Namespace: namespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"KEY": []byte("value")}, + } + err := k8sClient.Create(context.Background(), secret) + Expect(err).ToNot(HaveOccurred()) + + time.Sleep(1 * time.Second) + err = k8sClient.Get( + context.Background(), + types.NamespacedName{Name: "deployment", Namespace: namespace}, + dep, + ) + Expect(err).ToNot(HaveOccurred()) + Expect(resourceVersion).To(Equal(dep.GetResourceVersion())) + + }) + It("deletes specific resources when disabled", func() { pdb := resources[2].(*policyv1.PodDisruptionBudget) hpa := resources[3].(*autoscalingv2.HorizontalPodAutoscaler) From 31587addcefc0d29ad87fa20f72e1f574b825c59 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Fri, 15 Dec 2023 15:55:18 +0100 Subject: [PATCH 15/20] Add more tests --- reconciler/reconciler_test.go | 357 ++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) diff --git a/reconciler/reconciler_test.go b/reconciler/reconciler_test.go index c2e593a..132d83e 100644 --- a/reconciler/reconciler_test.go +++ b/reconciler/reconciler_test.go @@ -1,9 +1,26 @@ package reconciler import ( + "context" + "errors" "fmt" + "reflect" "testing" "time" + + "github.com/3scale-ops/basereconciler/resource" + "github.com/3scale-ops/basereconciler/util" + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) func TestResult_ShouldReturn(t *testing.T) { @@ -65,3 +82,343 @@ func TestResult_ShouldReturn(t *testing.T) { }) } } + +func TestResult_Values(t *testing.T) { + type fields struct { + Action action + RequeueAfter time.Duration + Error error + } + tests := []struct { + name string + fields fields + want1 ctrl.Result + want2 error + }{ + { + name: "Returns expected results", + fields: fields{ + Action: "", + RequeueAfter: 0, + Error: errors.New("error"), + }, + want1: reconcile.Result{}, + want2: errors.New("error"), + }, + { + name: "Returns expected results, with 'RequeueAfter'", + fields: fields{ + Action: "", + RequeueAfter: 60 * time.Second, + Error: nil, + }, + want1: reconcile.Result{RequeueAfter: 60 * time.Second}, + want2: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Result{ + Action: tt.fields.Action, + RequeueAfter: tt.fields.RequeueAfter, + Error: tt.fields.Error, + } + got1, got2 := result.Values() + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("Result.Values() = %v, want %v", got1, tt.want1) + } + if tt.want2 != nil && (got2.Error() != tt.want2.Error()) { + t.Errorf("Result.Values() = %v, want %v", got2, tt.want2) + } + }) + } +} + +func TestReconciler_ManageResourceLifecycle(t *testing.T) { + type fields struct { + Client client.Client + Log logr.Logger + Scheme *runtime.Scheme + } + type args struct { + req reconcile.Request + obj client.Object + opts []LifecycleOption + } + tests := []struct { + name string + fields fields + args args + want Result + wantObject client.Object + }{ + { + name: "Gets the resource", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{}, + }, + want: Result{ + Action: ContinueAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: nil, + }, + { + name: "Resource not found", + fields: fields{ + Client: fake.NewClientBuilder().Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{}, + }, + want: Result{ + Action: ReturnAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: nil, + }, + { + name: "Resource being deleted", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{}, + }, + want: Result{ + Action: ReturnAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: nil, + }, + { + name: "Adds finalizer", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{WithFinalizer("finalizer")}, + }, + want: Result{ + Action: ReturnAndRequeueAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + Finalizers: []string{"finalizer"}, + }}, + }, + { + name: "Executes finalization logic", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + Finalizers: []string{"finalizer"}, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{ + WithFinalizer("finalizer"), + WithFinalizationFunc(func(ctx context.Context, c client.Client) error { + o := &corev1.ServiceAccount{} + o.SetName("test-finalization-logic") + o.SetNamespace("ns") + c.Create(ctx, o) + return nil + }), + }, + }, + want: Result{ + Action: ReturnAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + Name: "test-finalization-logic", Namespace: "ns", + }}, + }, + { + name: "Removes the finalizer", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + Finalizers: []string{"finalizer"}, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{ + WithFinalizer("finalizer"), + }, + }, + want: Result{ + Action: ReturnAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }}, + }, + { + name: "Adds the finalizer", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []LifecycleOption{ + WithFinalizer("finalizer"), + }, + }, + want: Result{ + Action: ReturnAndRequeueAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + Finalizers: []string{"finalizer"}, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Reconciler{ + Client: tt.fields.Client, + Log: tt.fields.Log, + Scheme: tt.fields.Scheme, + } + got := r.ManageResourceLifecycle(context.TODO(), tt.args.req, tt.args.obj, tt.args.opts...) + if diff := cmp.Diff(got, tt.want); len(diff) > 0 { + t.Errorf("Reconciler.ManageResourceLifecycle() diff = %v", diff) + } + if tt.wantObject != nil { + o := tt.wantObject.DeepCopyObject().(client.Object) + tt.fields.Client.Get(context.TODO(), types.NamespacedName{Name: tt.wantObject.GetName(), Namespace: tt.wantObject.GetNamespace()}, o) + if diff := cmp.Diff(o, tt.wantObject, + util.IgnoreProperty("ResourceVersion"), + util.IgnoreProperty("DeletionTimestamp"), + util.IgnoreProperty("Kind"), + util.IgnoreProperty("APIVersion")); len(diff) > 0 { + t.Errorf("Reconciler.ManageResourceLifecycle() diff = %v", diff) + } + } + }) + } +} + +func TestReconciler_ReconcileOwnedResources(t *testing.T) { + type fields struct { + Client client.Client + Log logr.Logger + Scheme *runtime.Scheme + } + type args struct { + owner client.Object + list []resource.TemplateInterface + } + tests := []struct { + name string + fields fields + args args + want Result + }{ + { + name: "Creates owned resources", + fields: fields{ + Client: fake.NewClientBuilder().Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + owner: &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "owner", Namespace: "ns"}}, + list: []resource.TemplateInterface{ + resource.NewTemplateFromObjectFunction[*corev1.Service]( + func() *corev1.Service { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "ns"}} + }), + resource.NewTemplateFromObjectFunction[*corev1.ConfigMap]( + func() *corev1.ConfigMap { + return &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: "ns"}} + }), + }, + }, + want: Result{ + Action: ContinueAction, + RequeueAfter: 0, + Error: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Reconciler{ + Client: tt.fields.Client, + Scheme: tt.fields.Scheme, + } + got := r.ReconcileOwnedResources(context.TODO(), tt.args.owner, tt.args.list) + if diff := cmp.Diff(got, tt.want); len(diff) > 0 { + t.Errorf("Reconciler.ReconcileOwnedResources() = %v, want %v", got, tt.want) + + } + }) + } +} From af6628cdb8a28971f9de4d4db05061c5b327765f Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Fri, 15 Dec 2023 15:59:52 +0100 Subject: [PATCH 16/20] Add example usage for FilteredEventHandler --- reconciler/reconciler.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 8dc27d2..b318c51 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -239,6 +239,23 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O // The filter function receives both the object that generated the event and the object that // might need to be reconciled in response to that event. Depending on whether it returns true // or false the reconciler request will be generated or not. +// +// In the following example, a watch for Secret resources which match the name "secret" is added +// to the reconciler. The watch will generate reconmcile requests for v1alpha1.Test resources +// any time a Secret with name "secret" is created/uddated/deleted +// +// func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +// return ctrl.NewControllerManagedBy(mgr). +// For(&v1alpha1.Test{}). +// Watches(&source.Kind{Type: &corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: "Secret"}}}, +// r.FilteredEventHandler( +// &v1alpha1.TestList{}, +// func(event, o client.Object) bool { +// return event.GetName() == "secret" +// }, +// r.Log)). +// Complete(r) +// } func (r *Reconciler) FilteredEventHandler(ol client.ObjectList, filter func(event client.Object, o client.Object) bool, logger logr.Logger) handler.EventHandler { From 7934340f8da2b3549fd35cbf0e4010d2dd1ba511 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 18 Dec 2023 14:06:54 +0100 Subject: [PATCH 17/20] Allow users to pass init logic to the reconciler --- reconciler/reconciler.go | 126 ++++++++++++++++++++++++++-------- reconciler/reconciler_test.go | 47 ++++++++++--- util/k8s.go | 14 ++++ util/k8s_test.go | 46 +++++++++++++ 4 files changed, 198 insertions(+), 35 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index b318c51..077e3f8 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -9,6 +9,7 @@ import ( "github.com/3scale-ops/basereconciler/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -48,39 +49,79 @@ func (result Result) Values() (ctrl.Result, error) { result.Error } -var options = struct { - finalizer *string - finalizationLogic []finalizationFunction -}{ - finalizer: nil, - finalizationLogic: []finalizationFunction{}, +type lifecycleOptions struct { + initializationLogic []initializationFunction + inMemoryinitializationLogic []inMemoryinitializationFunction + finalizer *string + finalizationLogic []finalizationFunction } -// LifecycleOption is an interface that defines options that can be passed to +func newLifecycleOptions() *lifecycleOptions { + return &lifecycleOptions{finalizationLogic: []finalizationFunction{}} +} + +// lifecycleOption is an interface that defines options that can be passed to // the reconciler's ManageResourceLifecycle() function -type LifecycleOption interface { - applyToLifecycleOptions() +type lifecycleOption interface { + applyToLifecycleOptions(*lifecycleOptions) } type finalizer string -func (f finalizer) applyToLifecycleOptions() { - options.finalizer = util.Pointer(string(f)) +func (f finalizer) applyToLifecycleOptions(opts *lifecycleOptions) { + opts.finalizer = util.Pointer(string(f)) + opts.initializationLogic = append(opts.initializationLogic, f.initFinalizer) } + +// WithFinalizer can be used to provide a finalizer string that the resource will be initialized with +// For finalization logic to be run before objet deletion, a finalizar must be passed. func WithFinalizer(f string) finalizer { return finalizer(f) } +func (f finalizer) initFinalizer(ctx context.Context, c client.Client, o client.Object) error { + if !controllerutil.ContainsFinalizer(o, string(f)) { + controllerutil.AddFinalizer(o, string(f)) + } + return nil +} + type finalizationFunction func(context.Context, client.Client) error -func (fn finalizationFunction) applyToLifecycleOptions() { - options.finalizationLogic = append(options.finalizationLogic, fn) +func (fn finalizationFunction) applyToLifecycleOptions(opts *lifecycleOptions) { + opts.finalizationLogic = append(opts.finalizationLogic, fn) } +// WithFinalizationFunc can be used to provide functions that will be run on object finalization. A Finalizer must be set for +// these functions to be called. func WithFinalizationFunc(fn func(context.Context, client.Client) error) finalizationFunction { return fn } +type initializationFunction func(context.Context, client.Client, client.Object) error + +func (fn initializationFunction) applyToLifecycleOptions(opts *lifecycleOptions) { + opts.initializationLogic = append(opts.initializationLogic, fn) +} + +// WithInitializationFunc can be used to provide functions that run resource initialization, like for example +// applying defaults or labels to the resource. +func WithInitializationFunc(fn func(context.Context, client.Client, client.Object) error) initializationFunction { + return fn +} + +type inMemoryinitializationFunction func(context.Context, client.Client, client.Object) error + +func (fn inMemoryinitializationFunction) applyToLifecycleOptions(opts *lifecycleOptions) { + opts.inMemoryinitializationLogic = append(opts.inMemoryinitializationLogic, fn) +} + +// WithInitializationFunc can be used to provide functions that run resource initialization, like for example +// applying defaults or labels to the resource. +func WithInMemoryInitializationFunc(fn func(context.Context, client.Client, client.Object) error) inMemoryinitializationFunction { + return fn +} + // Reconciler computes a list of resources that it needs to keep in place type Reconciler struct { client.Client @@ -122,10 +163,11 @@ func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) ( // run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise // the custom resource will be immediately deleted and the functions won't run. func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile.Request, obj client.Object, - opts ...LifecycleOption) Result { + opts ...lifecycleOption) Result { + options := newLifecycleOptions() for _, o := range opts { - o.applyToLifecycleOptions() + o.applyToLifecycleOptions(options) } ctx, logger := r.Logger(ctx) @@ -145,7 +187,7 @@ func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile. // resource if options.finalizer != nil && controllerutil.ContainsFinalizer(obj, *options.finalizer) { - err := r.Finalize(ctx, options.finalizationLogic, logger) + err := r.finalize(ctx, options.finalizationLogic, logger) if err != nil { logger.Error(err, "unable to delete instance") return Result{Error: err} @@ -163,7 +205,11 @@ func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile. return Result{Action: ReturnAction} } - if ok := r.IsInitialized(obj, options.finalizer); !ok { + ok, err := r.isInitialized(ctx, obj, options.initializationLogic) + if err != nil { + return Result{Error: err} + } + if !ok { err := r.Client.Update(ctx, obj) if err != nil { logger.Error(err, "unable to initialize instance") @@ -171,23 +217,49 @@ func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile. } return Result{Action: ReturnAndRequeueAction} } + + if err := r.inMemoryInitialization(ctx, obj, options.inMemoryinitializationLogic); err != nil { + return Result{Error: err} + } + return Result{Action: ContinueAction} } -// IsInitialized can be used to check if instance is correctly initialized. -// Returns false if it isn't. -func (r *Reconciler) IsInitialized(obj client.Object, finalizer *string) bool { - ok := true - if finalizer != nil && !controllerutil.ContainsFinalizer(obj, *finalizer) { - controllerutil.AddFinalizer(obj, *finalizer) - ok = false +// isInitialized can be used to check if instance is correctly initialized. +// Returns false if it isn't and an update is required. +func (r *Reconciler) isInitialized(ctx context.Context, obj client.Object, fns []initializationFunction) (bool, error) { + orig := obj.DeepCopyObject() + for _, fn := range fns { + err := fn(ctx, r.Client, obj) + if err != nil { + return false, err + } } - return ok + if !equality.Semantic.DeepEqual(orig, obj) { + return false, nil + } + + return true, nil +} + +// inMemoryInitialization can be used to perform initializarion on the resource that is not +// persisted in the API storage. This can be used to perform initialization on the resource without +// writing it to the API to avoid surfacing it uo to the user. This approach is a bit more +// gitops firendly as it avoids modifying the resource, but it doesn't provide any information +// to the user on the initialization being used for reconciliation. +func (r *Reconciler) inMemoryInitialization(ctx context.Context, obj client.Object, fns []inMemoryinitializationFunction) error { + for _, fn := range fns { + err := fn(ctx, r.Client, obj) + if err != nil { + return err + } + } + return nil } -// Finalize contains finalization logic for the Reconciler -func (r *Reconciler) Finalize(ctx context.Context, fns []finalizationFunction, log logr.Logger) error { +// finalize contains finalization logic for the Reconciler +func (r *Reconciler) finalize(ctx context.Context, fns []finalizationFunction, log logr.Logger) error { // Call any cleanup functions passed for _, fn := range fns { err := fn(ctx, r.Client) diff --git a/reconciler/reconciler_test.go b/reconciler/reconciler_test.go index 132d83e..3760300 100644 --- a/reconciler/reconciler_test.go +++ b/reconciler/reconciler_test.go @@ -143,7 +143,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { type args struct { req reconcile.Request obj client.Object - opts []LifecycleOption + opts []lifecycleOption } tests := []struct { name string @@ -166,7 +166,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{}, + opts: []lifecycleOption{}, }, want: Result{ Action: ContinueAction, @@ -185,7 +185,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{}, + opts: []lifecycleOption{}, }, want: Result{ Action: ReturnAction, @@ -209,7 +209,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{}, + opts: []lifecycleOption{}, }, want: Result{ Action: ReturnAction, @@ -232,7 +232,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{WithFinalizer("finalizer")}, + opts: []lifecycleOption{WithFinalizer("finalizer")}, }, want: Result{ Action: ReturnAndRequeueAction, @@ -260,7 +260,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{ + opts: []lifecycleOption{ WithFinalizer("finalizer"), WithFinalizationFunc(func(ctx context.Context, c client.Client) error { o := &corev1.ServiceAccount{} @@ -296,7 +296,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{ + opts: []lifecycleOption{ WithFinalizer("finalizer"), }, }, @@ -324,7 +324,7 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { args: args{ req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, obj: &corev1.Service{}, - opts: []LifecycleOption{ + opts: []lifecycleOption{ WithFinalizer("finalizer"), }, }, @@ -338,6 +338,37 @@ func TestReconciler_ManageResourceLifecycle(t *testing.T) { Finalizers: []string{"finalizer"}, }}, }, + { + name: "Runs initialization logic", + fields: fields{ + Client: fake.NewClientBuilder().WithObjects( + &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + }}, + ).Build(), + Log: logr.Discard(), + Scheme: scheme.Scheme, + }, + args: args{ + req: reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-resource", Namespace: "ns"}}, + obj: &corev1.Service{}, + opts: []lifecycleOption{ + WithInitializationFunc(func(ctx context.Context, c client.Client, o client.Object) error { + o.SetLabels(map[string]string{"initialized": "yes"}) + return nil + }), + }, + }, + want: Result{ + Action: ReturnAndRequeueAction, + RequeueAfter: 0, + Error: nil, + }, + wantObject: &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "my-resource", Namespace: "ns", + Labels: map[string]string{"initialized": "yes"}, + }}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/util/k8s.go b/util/k8s.go index fd5d037..8d58a93 100644 --- a/util/k8s.go +++ b/util/k8s.go @@ -1,6 +1,7 @@ package util import ( + "context" "fmt" "reflect" "strings" @@ -75,3 +76,16 @@ func ObjectReference(o client.Object, gvk schema.GroupVersionKind) *corev1.Objec ResourceVersion: o.GetResourceVersion(), } } + +// Defaulter defines functions for setting defaults on resources. +type Defaulter interface { + client.Object + Default() +} + +func ResourceDefaulter(o Defaulter) func(context.Context, client.Client, client.Object) error { + return func(_ context.Context, _ client.Client, o client.Object) error { + o.(Defaulter).Default() + return nil + } +} diff --git a/util/k8s_test.go b/util/k8s_test.go index 61360bd..f3d01dc 100644 --- a/util/k8s_test.go +++ b/util/k8s_test.go @@ -1,6 +1,7 @@ package util import ( + "context" "reflect" "testing" @@ -150,3 +151,48 @@ func TestNewObjectListFromGVK(t *testing.T) { }) } } + +type testResource struct { + *corev1.Service +} + +func (o *testResource) Default() { + o.SetLabels(map[string]string{"key": "value"}) +} +func TestResourceDefaulter(t *testing.T) { + type args struct { + o *testResource + } + tests := []struct { + name string + args args + check func(client.Object) bool + }{ + { + name: "Calling the ResourceDefaulter applies defaults", + args: args{ + o: &testResource{ + Service: &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "ns", + }}, + }, + }, + check: func(o client.Object) bool { + if v, ok := o.GetLabels()["key"]; ok { + if v == "value" { + return true + } + } + return false + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = ResourceDefaulter(tt.args.o)(context.TODO(), nil, tt.args.o) + if !tt.check(tt.args.o) { + t.Errorf("ResourceDefaulter() got %v", tt.args.o) + } + }) + } +} From 124455b0469d101e28e51410e94659b6ab137e81 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Mon, 18 Dec 2023 14:07:11 +0100 Subject: [PATCH 18/20] Add a couple of lifecycle management tests --- test/api/v1alpha1/test_types.go | 13 +++++++ test/test_controller.go | 17 ++++++++- test/test_controller_suite_test.go | 55 ++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/test/api/v1alpha1/test_types.go b/test/api/v1alpha1/test_types.go index 482e4c5..b086a7c 100644 --- a/test/api/v1alpha1/test_types.go +++ b/test/api/v1alpha1/test_types.go @@ -21,6 +21,7 @@ package v1alpha1 import ( "github.com/3scale-ops/basereconciler/reconciler" + "github.com/3scale-ops/basereconciler/util" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -79,6 +80,18 @@ type Test struct { Status TestStatus `json:"status,omitempty"` } +func (test *Test) Default() { + if test.Spec.HPA == nil { + test.Spec.HPA = util.Pointer(false) + } + if test.Spec.PDB == nil { + test.Spec.PDB = util.Pointer(false) + } + if test.Spec.PruneService == nil { + test.Spec.PruneService = util.Pointer(false) + } +} + var _ reconciler.ObjectWithAppStatus = &Test{} func (t *Test) GetStatus() reconciler.AppStatus { diff --git a/test/test_controller.go b/test/test_controller.go index 1c7d696..e8683cb 100644 --- a/test/test_controller.go +++ b/test/test_controller.go @@ -49,7 +49,22 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu ctx, _ = r.Logger(ctx, "name", req.Name, "namespace", req.Namespace) obj := &v1alpha1.Test{} - result := r.ManageResourceLifecycle(ctx, req, obj) + result := r.ManageResourceLifecycle(ctx, req, obj, + reconciler.WithInitializationFunc(util.ResourceDefaulter(obj)), + reconciler.WithFinalizer("finalizer"), + // create a configmap when the custom resource is deleted + reconciler.WithFinalizationFunc(func(context.Context, client.Client) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm", Namespace: req.Namespace}, + Data: map[string]string{"removed": "yes"}, + } + err := r.Client.Create(ctx, cm) + if err != nil { + return err + } + return nil + }), + ) if result.ShouldReturn() { return result.Values() } diff --git a/test/test_controller_suite_test.go b/test/test_controller_suite_test.go index a83ba03..f840eda 100644 --- a/test/test_controller_suite_test.go +++ b/test/test_controller_suite_test.go @@ -17,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var _ = Describe("Test controller", func() { @@ -43,6 +44,60 @@ var _ = Describe("Test controller", func() { Expect(err).ToNot(HaveOccurred()) }) + Context("Manages instance lifecycle", func() { + BeforeEach(func() { + By("creating a Test simple resource") + instance = &v1alpha1.Test{ + ObjectMeta: metav1.ObjectMeta{Name: "instance", Namespace: namespace}, + Spec: v1alpha1.TestSpec{}, + } + err := k8sClient.Create(context.Background(), instance) + Expect(err).ToNot(HaveOccurred()) + Eventually(func() error { + return k8sClient.Get(context.Background(), types.NamespacedName{Name: "instance", Namespace: namespace}, instance) + }, timeout, poll).ShouldNot(HaveOccurred()) + }) + + AfterEach(func() { + k8sClient.Delete(context.Background(), instance, client.PropagationPolicy(metav1.DeletePropagationForeground)) + }) + + It("initializes the custom resource", func() { + Eventually(func() bool { + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: "instance", Namespace: namespace}, instance) + if err != nil { + return false + } + return (instance.Spec.HPA != nil && !*instance.Spec.HPA) && + (instance.Spec.PDB != nil && !*instance.Spec.PDB) && + (instance.Spec.PruneService != nil && !*instance.Spec.PruneService) && + controllerutil.ContainsFinalizer(instance, "finalizer") + + }, timeout, poll).Should(BeTrue()) + }) + + When("the custom resource is deleted", func() { + BeforeEach(func() { + By("deleting the custom resource") + err := k8sClient.Delete(context.Background(), instance, client.PropagationPolicy(metav1.DeletePropagationForeground)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("executes finalization logic", func() { + Eventually(func() bool { + cm := &corev1.ConfigMap{} + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: "cm", Namespace: namespace}, cm) + if err != nil { + return false + } + _, ok := cm.Data["removed"] + return ok + + }, timeout, poll).Should(BeTrue()) + }) + }) + }) + Context("Creates resources", func() { BeforeEach(func() { From 533edabea4e98aa7e5627eb4439641002fb5f2c7 Mon Sep 17 00:00:00 2001 From: roi Date: Wed, 20 Dec 2023 10:16:45 +0100 Subject: [PATCH 19/20] Fix typos in doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sergio López <41513123+slopezz@users.noreply.github.com> --- README.md | 6 +++--- resource/create_or_update.go | 2 +- resource/template.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b0981d6..496b48e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # basereconciler -Basereconciler is an attempt to create a reconciler that can be imported an used in any controller-runtime based controller to perform the most common tasks a controller usually performs. It's a bunch of code that it's typically written again and again for every and each controller and that can be abstracted to work in a more generic way to avoid the repetition and improve code mantainability. +Basereconciler is an attempt to create a reconciler that can be imported and used in any controller-runtime based controller to perform the most common tasks a controller usually performs. It's a bunch of code that it's typically written again and again for every and each controller and that can be abstracted to work in a more generic way to avoid the repetition and improve code mantainability. At the moment basereconciler can perform the following tasks: * **Get the custom resource and perform some common tasks on it**: * Management of resource finalizer: some custom resources required more complex finalization logic. For this to happen a finalizer must be in place. Basereconciler can keep this finalizer in place and remove it when necessary during resource finalization. * Management of finalization logic: it checks if the resource is being finalized and executed the finalization logic passed to it if that is the case. When all finalization logic is completed it removes the finalizer on the custom resource. -* **Reconcile resources owned by the custom resource**: basreconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future. +* **Reconcile resources owned by the custom resource**: basereconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future. * **Reconcile custom resource status**: if the custom resource implements a certain interface, basereconciler can also be in charge of reconciling the status. -* **Resource pruner**: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no logner required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation. +* **Resource pruner**: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no longer required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation. ## Basic Usage diff --git a/resource/create_or_update.go b/resource/create_or_update.go index 26dafa8..3bac2dc 100644 --- a/resource/create_or_update.go +++ b/resource/create_or_update.go @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -// CreateOrUpdate cretes or updates resources. The function receives several paremters: +// CreateOrUpdate cretes or updates resources. The function receives several parameters: // - ctx: the context. The logger is expected to be within the context, otherwise the function won't // produce any logs. // - cl: the kubernetes API client diff --git a/resource/template.go b/resource/template.go index 8a41e87..1b720b3 100644 --- a/resource/template.go +++ b/resource/template.go @@ -34,7 +34,7 @@ type Template[T client.Object] struct { // object. TemplateBuilder TemplateBuilderFunction[T] // TemplateMutations are functions that are called during Build() after - // TemplateBuilder has ben invoked, to perform mutations on the object that require + // TemplateBuilder has been invoked, to perform mutations on the object that require // access to a kubernetes API server. TemplateMutations []TemplateMutationFunction // IsEnabled specifies whether the resourse described by this Template should From e9a8f344824575d87ecbb743ca1a2a2522e98254 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Wed, 20 Dec 2023 10:27:20 +0100 Subject: [PATCH 20/20] Update description on ManageResourceLifecycle() options --- reconciler/reconciler.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go index 077e3f8..20ff957 100644 --- a/reconciler/reconciler.go +++ b/reconciler/reconciler.go @@ -157,11 +157,17 @@ func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) ( // ManageResourceLifecycle manages the lifecycle of the resource, from initialization to // finalization and deletion. // The behaviour can be modified depending on the options passed to the function: -// - finalizer: if a non-nil finalizer is passed to the function, it will ensure that the -// custom resource has a finalizer in place, updasting it if required. -// - cleanupFns: variadic parameter that allows passing cleanup functions that will be -// run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise -// the custom resource will be immediately deleted and the functions won't run. +// - WithInitializationFunc(...): pass a function with initialization logic for the custom resource. +// The function will be executed and if changes to the custom resource are detected the resource will +// be updated. It can be used to set default values on the custom resource. Can be used more than once. +// - WithInMemoryInitializationFunc(...): pass a function with initialization logic to the custom resource. +// If the custom resource is modified in nay way, the changes won't be persisted in the API server and will +// only have effect within the reconcile loop. Can be used more than once. +// - WithFinalizer(...): passes a string that will be configured as a resource finalizar, ensuring that the +// custom resource has the finalizer in place, updating it if required. +// - WithFinalizationFunc(...): pass finalization functions that will be +// run when the custom resource is being deleted. Only works ifa finalizer is also passed, otherwise +// the custom resource will be immediately deleted and the functions won't run. Can be used more than once. func (r *Reconciler) ManageResourceLifecycle(ctx context.Context, req reconcile.Request, obj client.Object, opts ...lifecycleOption) Result {