diff --git a/Makefile b/Makefile index ff4d138f..cba3d768 100644 --- a/Makefile +++ b/Makefile @@ -217,7 +217,7 @@ kustomize: ## Download kustomize locally if necessary. @[ -f $(KUSTOMIZE) ] || GOBIN=$(shell pwd)/bin go install sigs.k8s.io/kustomize/kustomize/v4@v4.5.5 manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=$(MANAGER_ROLE) webhook paths="$(shell pwd)/pkg/api/..." output:crd:artifacts:config=$(shell pwd)/config/crd/bases output:rbac:artifacts:config=$(shell pwd)/config/rbac + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=$(MANAGER_ROLE) webhook paths="$(shell pwd)/pkg/..." output:crd:artifacts:config=$(shell pwd)/config/crd/bases output:rbac:artifacts:config=$(shell pwd)/config/rbac generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="$(shell pwd)/hack/boilerplate.go.txt" paths="$(shell pwd)/pkg/api/..." @@ -229,5 +229,5 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and SWAGGER_CLI = $(shell pwd)/bin/swag swagger: - @[ -f $(SWAGGER_CLI) ] || GOBIN=$(shell pwd)/bin go install github.com/swaggo/swag/cmd/swag@v1.8.12 + @[ -f $(SWAGGER_CLI) ] || GOBIN=$(shell pwd)/bin go install github.com/swaggo/swag/cmd/swag@v1.16.2 $(SWAGGER_CLI) init --parseDependency --parseInternal --parseDepth 2 -g cmd/apiserver/apiserver.go --output pkg/apiserver/docs/ diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index b4971239..5b736806 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -35,7 +35,7 @@ var Version = "dev" // @version 1.0 // @description Cluster Registry API -// @host http://127.0.0.1:8080 +// @host 127.0.0.1:8080 // @BasePath /api // @schemes http https diff --git a/cmd/client/client.go b/cmd/client/client.go index d531d698..57bf43c7 100644 --- a/cmd/client/client.go +++ b/cmd/client/client.go @@ -13,19 +13,17 @@ governing permissions and limitations under the License. package main import ( + "encoding/base64" "flag" - "k8s.io/client-go/tools/leaderelection/resourcelock" - "os" - "sigs.k8s.io/controller-runtime/pkg/cache" - + registryv1alpha1 "github.com/adobe/cluster-registry/pkg/api/registry/v1alpha1" "github.com/adobe/cluster-registry/pkg/client/controllers" "github.com/adobe/cluster-registry/pkg/config" monitoring "github.com/adobe/cluster-registry/pkg/monitoring/client" "github.com/adobe/cluster-registry/pkg/sqs" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "encoding/base64" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "os" configv1 "github.com/adobe/cluster-registry/pkg/api/config/v1" registryv1 "github.com/adobe/cluster-registry/pkg/api/registry/v1" @@ -48,10 +46,13 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(registryv1.AddToScheme(scheme)) + utilruntime.Must(registryv1alpha1.AddToScheme(scheme)) utilruntime.Must(configv1.AddToScheme(scheme)) } func main() { + ctx := ctrl.SetupSignalHandler() + var configFile string var metricsAddr string var enableLeaderElection bool @@ -80,18 +81,20 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) var err error - clientConfig := configv1.ClientConfig{ + var clientConfig configv1.ClientConfig + clientConfigDefaults := configv1.ClientConfig{ Namespace: namespace, AlertmanagerWebhook: configv1.AlertmanagerWebhookConfig{ BindAddress: alertmanagerWebhookAddr, AlertMap: []configv1.AlertRule{}, }, + ServiceMetadata: configv1.ServiceMetadataConfig{ + WatchedGVKs: []configv1.WatchedGVK{}, + ServiceIdAnnotation: "adobe.serviceid", + }, } options := ctrl.Options{ - Scheme: scheme, - Cache: cache.Options{ - Namespaces: []string{namespace}, - }, + Scheme: scheme, MetricsBindAddress: metricsAddr, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, @@ -100,7 +103,7 @@ func main() { } if configFile != "" { - options, clientConfig, err = apply(configFile) + options, clientConfig, err = apply(configFile, &clientConfigDefaults) if err != nil { setupLog.Error(err, "unable to load the config file") os.Exit(1) @@ -144,6 +147,27 @@ func main() { os.Exit(1) } + if err = (&controllers.ServiceMetadataWatcherReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("ServiceMetadataWatcher"), + Scheme: mgr.GetScheme(), + WatchedGVKs: func(cfg configv1.ClientConfig) []schema.GroupVersionKind { + var GVKs []schema.GroupVersionKind + for _, gvk := range cfg.ServiceMetadata.WatchedGVKs { + GVKs = append(GVKs, schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + }) + } + return GVKs + }(clientConfig), + ServiceIdAnnotation: clientConfig.ServiceMetadata.ServiceIdAnnotation, + }).SetupWithManager(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ServiceMetadataWatcher") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) @@ -174,14 +198,14 @@ func main() { }() setupLog.Info("starting cluster-registry-client") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running cluster-registry-client") os.Exit(1) } } -func apply(configFile string) (ctrl.Options, configv1.ClientConfig, error) { - options, cfg, err := configv1.Load(scheme, configFile) +func apply(configFile string, clientConfigDefaults *configv1.ClientConfig) (ctrl.Options, configv1.ClientConfig, error) { + options, cfg, err := configv1.Load(scheme, configFile, clientConfigDefaults) if err != nil { return options, cfg, err } diff --git a/config/crd/bases/registry.ethos.adobe.com_clusters.yaml b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml index 34d6dd94..461acc9a 100644 --- a/config/crd/bases/registry.ethos.adobe.com_clusters.yaml +++ b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml @@ -204,6 +204,15 @@ spec: registeredAt: description: Timestamp when cluster was registered in Cluster Registry type: string + services: + additionalProperties: + additionalProperties: + additionalProperties: + type: string + type: object + type: object + description: ServiceMetadata service specific metadata + type: object shortName: description: Cluster name, without dash maxLength: 64 diff --git a/config/crd/bases/registry.ethos.adobe.com_servicemetadatawatchers.yaml b/config/crd/bases/registry.ethos.adobe.com_servicemetadatawatchers.yaml new file mode 100644 index 00000000..6d45c7d2 --- /dev/null +++ b/config/crd/bases/registry.ethos.adobe.com_servicemetadatawatchers.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: servicemetadatawatchers.registry.ethos.adobe.com +spec: + group: registry.ethos.adobe.com + names: + kind: ServiceMetadataWatcher + listKind: ServiceMetadataWatcherList + plural: servicemetadatawatchers + singular: servicemetadatawatcher + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ServiceMetadataWatcher is the Schema for the servicemetadatawatchers + 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: ServiceMetadataWatcherSpec defines the desired state of ServiceMetadataWatcher + properties: + watchedServiceObjects: + items: + properties: + objectReference: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - apiVersion + - kind + - name + type: object + watchedFields: + items: + properties: + dst: + type: string + src: + type: string + required: + - dst + - src + type: object + type: array + required: + - objectReference + - watchedFields + type: object + type: array + required: + - watchedServiceObjects + type: object + status: + description: ServiceMetadataWatcherStatus defines the observed state of + ServiceMetadataWatcher + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index c9713abe..5b1ad8f5 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/registry.ethos.adobe.com_clusters.yaml +- bases/registry.ethos.adobe.com.servicemetadatawatchers.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_clusters.yaml +#- path: patches/webhook_in_servicemetadatawatchers.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_clusters.yaml +#- path: patches/cainjection_in_servicemetadatawatchers.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_servicemetadatawatchers.yaml b/config/crd/patches/cainjection_in_servicemetadatawatchers.yaml new file mode 100644 index 00000000..077cef4e --- /dev/null +++ b/config/crd/patches/cainjection_in_servicemetadatawatchers.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: servicemetadatawatchers.registry.ethos.adobe.com diff --git a/config/crd/patches/webhook_in_servicemetadatawatchers.yaml b/config/crd/patches/webhook_in_servicemetadatawatchers.yaml new file mode 100644 index 00000000..c38bdbe8 --- /dev/null +++ b/config/crd/patches/webhook_in_servicemetadatawatchers.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: servicemetadatawatchers.registry.ethos.adobe.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9c5b539c..66736c3d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -1,9 +1,7 @@ - --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: cluster-registry rules: - apiGroups: @@ -32,3 +30,29 @@ rules: - get - patch - update +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers/finalizers + verbs: + - update +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers/status + verbs: + - get + - patch + - update diff --git a/config/rbac/servicemetadatawatcher_editor_role.yaml b/config/rbac/servicemetadatawatcher_editor_role.yaml new file mode 100644 index 00000000..969c9eea --- /dev/null +++ b/config/rbac/servicemetadatawatcher_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit servicemetadatawatchers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: servicemetadatawatcher-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cluster-registry + app.kubernetes.io/part-of: cluster-registry + app.kubernetes.io/managed-by: kustomize + name: servicemetadatawatcher-editor-role +rules: +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers/status + verbs: + - get diff --git a/config/rbac/servicemetadatawatcher_viewer_role.yaml b/config/rbac/servicemetadatawatcher_viewer_role.yaml new file mode 100644 index 00000000..7bf85509 --- /dev/null +++ b/config/rbac/servicemetadatawatcher_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view servicemetadatawatchers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: servicemetadatawatcher-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cluster-registry + app.kubernetes.io/part-of: cluster-registry + app.kubernetes.io/managed-by: kustomize + name: servicemetadatawatcher-viewer-role +rules: +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers + verbs: + - get + - list + - watch +- apiGroups: + - registry.ethos.adobe.com + resources: + - servicemetadatawatchers/status + verbs: + - get diff --git a/go.mod b/go.mod index 02192831..4e935437 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 github.com/aws/aws-sdk-go v1.44.332 github.com/coreos/go-oidc/v3 v3.6.0 + github.com/evanphx/json-patch/v5 v5.6.0 github.com/go-logr/logr v1.2.4 github.com/google/uuid v1.3.0 github.com/gusaul/go-dynamock v0.0.0-20210107061312-3e989056e1e6 @@ -13,19 +14,20 @@ require ( github.com/labstack/echo/v4 v4.11.1 github.com/labstack/gommon v0.4.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.27.7 + github.com/onsi/gomega v1.27.10 github.com/prometheus/client_golang v1.15.1 github.com/stretchr/testify v1.8.4 github.com/swaggo/echo-swagger v1.4.0 github.com/swaggo/swag v1.16.1 github.com/testcontainers/testcontainers-go v0.23.0 - golang.org/x/net v0.12.0 + golang.org/x/net v0.14.0 gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.27.2 k8s.io/apimachinery v0.27.2 k8s.io/client-go v0.27.2 + k8s.io/component-base v0.27.2 k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.15.1 sigs.k8s.io/yaml v1.3.0 @@ -56,7 +58,6 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect @@ -108,18 +109,18 @@ require ( github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.11.0 // indirect + golang.org/x/crypto v0.12.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/mod v0.12.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.1 // indirect + golang.org/x/tools v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect @@ -130,7 +131,6 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.27.2 // indirect - k8s.io/component-base v0.27.2 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index 378cbd55..767b53e4 100644 --- a/go.sum +++ b/go.sum @@ -251,12 +251,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= 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.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 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/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= @@ -325,13 +325,15 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -342,8 +344,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh 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/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= @@ -355,8 +357,8 @@ 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.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/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= @@ -379,8 +381,8 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= @@ -392,8 +394,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.0.0-20220722155255-886fb9371eb4/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/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -433,16 +435,16 @@ golang.org/x/sys v0.11.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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/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.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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -456,8 +458,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f 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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/local/.env.local b/local/.env.local index b1a84849..eaeff638 100644 --- a/local/.env.local +++ b/local/.env.local @@ -14,7 +14,7 @@ export IMAGE_DB="amazon/dynamodb-local:1.16.0" export CONTAINER_SQS="sqs" export IMAGE_SQS="softwaremill/elasticmq-native:1.2.3" export KIND_CLUSTERNAME="k8s-cluster-registry" -export KIND_NODE_VERSION="v1.23.6" +export KIND_NODE_VERSION="v1.25.11" export NETWORK="cluster-registry-net" export CONTAINER_API="cluster-registry-api" export IMAGE_APISERVER="ghcr.io/adobe/cluster-registry-api" diff --git a/local/database/dummy-data.yaml b/local/database/dummy-data.yaml index 2f57e1ee..1184c6b1 100644 --- a/local/database/dummy-data.yaml +++ b/local/database/dummy-data.yaml @@ -93,6 +93,18 @@ tags: onboarding: "off" scaling: "off" + services: + 12345: + ns-team-abc: + key1: value1 + key2: value2 + ns-team-xyz: + key3: value3 + 98765: + ns-team-example: + some.key: some.value + ns-team-example2: + foo: bar - apiVersion: registry.ethos.adobe.com/v1 kind: Cluster metadata: @@ -165,6 +177,19 @@ tags: onboarding: "off" scaling: "on" + services: + 12345: + ns-team-abc: + keyA: valueA + keyB: valueB + 55555: + ns-team-foo: + foo: bar + 98765: + ns-team-example: + some.other.key: some.other.value + ns-team-foobar: + foo: bar - apiVersion: registry.ethos.adobe.com/v1 kind: Cluster metadata: diff --git a/local/database/reset.sh b/local/database/reset.sh index 516d1824..e70be06c 100755 --- a/local/database/reset.sh +++ b/local/database/reset.sh @@ -7,11 +7,11 @@ set -o pipefail ROOT_DIR="$(cd "$(dirname "$0")/.."; pwd)" echo 'Loading local environment variables...' -source ${ROOT_DIR}/local/.env.local +source ${ROOT_DIR}/.env.local echo 'Create dynamodb schema...' aws dynamodb delete-table --table-name ${DB_TABLE_NAME} --endpoint-url $DB_ENDPOINT > /dev/null 2>&1 || true -aws dynamodb create-table --cli-input-json file://${ROOT_DIR}/local/db/schema.json --endpoint-url $DB_ENDPOINT > /dev/null +aws dynamodb create-table --cli-input-json file://${ROOT_DIR}/database/schema.json --endpoint-url $DB_ENDPOINT > /dev/null echo 'Populate database with dummy data..' -go run ${ROOT_DIR}/local/db/import.go --input-file ${ROOT_DIR}/local/db/dummy-data.yaml +go run ${ROOT_DIR}/database/import.go --input-file ${ROOT_DIR}/database/dummy-data.yaml diff --git a/pkg/api/config/v1/clientconfig.go b/pkg/api/config/v1/clientconfig.go index 72594480..4119aa5d 100644 --- a/pkg/api/config/v1/clientconfig.go +++ b/pkg/api/config/v1/clientconfig.go @@ -133,21 +133,23 @@ func Encode(scheme *runtime.Scheme, cfg *ClientConfig) (string, error) { // Load returns a set of controller options and ClientConfig from the given file, if the config file path is empty // it uses the default values. -func Load(scheme *runtime.Scheme, configFile string) (ctrl.Options, ClientConfig, error) { +func Load(scheme *runtime.Scheme, configFile string, defaults *ClientConfig) (ctrl.Options, ClientConfig, error) { var err error options := ctrl.Options{ Scheme: scheme, } - - cfg := ClientConfig{} + cfg := &ClientConfig{} + if defaults != nil { + cfg = defaults.DeepCopy() + } if configFile == "" { - scheme.Default(&cfg) + scheme.Default(cfg) } else { - err := fromFile(configFile, scheme, &cfg) + err := fromFile(configFile, scheme, cfg) if err != nil { - return options, cfg, err + return options, *cfg, err } } - addTo(&options, &cfg) - return options, cfg, err + addTo(&options, cfg) + return options, *cfg, err } diff --git a/pkg/api/config/v1/clientconfig_types.go b/pkg/api/config/v1/clientconfig_types.go index 8e58e362..6b0a9a99 100644 --- a/pkg/api/config/v1/clientconfig_types.go +++ b/pkg/api/config/v1/clientconfig_types.go @@ -34,6 +34,8 @@ type ClientConfig struct { Namespace string `json:"namespace,omitempty"` AlertmanagerWebhook AlertmanagerWebhookConfig `json:"alertmanagerWebhook"` + + ServiceMetadata ServiceMetadataConfig `json:"serviceMetadata"` } // ControllerManager defines the desired state of GenericControllerManagerConfiguration. @@ -184,6 +186,17 @@ type AlertRule struct { OnResolved map[string]string `json:"onResolved"` } +type ServiceMetadataConfig struct { + WatchedGVKs []WatchedGVK `json:"watchedGVKs"` + ServiceIdAnnotation string `json:"serviceIdAnnotation"` +} + +type WatchedGVK struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} + func init() { SchemeBuilder.Register(&ClientConfig{}) } diff --git a/pkg/api/config/v1/zz_generated.deepcopy.go b/pkg/api/config/v1/zz_generated.deepcopy.go index 58a7517f..dfaa2cfb 100644 --- a/pkg/api/config/v1/zz_generated.deepcopy.go +++ b/pkg/api/config/v1/zz_generated.deepcopy.go @@ -80,6 +80,7 @@ func (in *ClientConfig) DeepCopyInto(out *ClientConfig) { out.TypeMeta = in.TypeMeta in.ControllerManager.DeepCopyInto(&out.ControllerManager) in.AlertmanagerWebhook.DeepCopyInto(&out.AlertmanagerWebhook) + in.ServiceMetadata.DeepCopyInto(&out.ServiceMetadata) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConfig. @@ -251,3 +252,38 @@ func (in *ControllerWebhook) DeepCopy() *ControllerWebhook { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceMetadataConfig) DeepCopyInto(out *ServiceMetadataConfig) { + *out = *in + if in.WatchedGVKs != nil { + in, out := &in.WatchedGVKs, &out.WatchedGVKs + *out = make([]WatchedGVK, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataConfig. +func (in *ServiceMetadataConfig) DeepCopy() *ServiceMetadataConfig { + if in == nil { + return nil + } + out := new(ServiceMetadataConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WatchedGVK) DeepCopyInto(out *WatchedGVK) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WatchedGVK. +func (in *WatchedGVK) DeepCopy() *WatchedGVK { + if in == nil { + return nil + } + out := new(WatchedGVK) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/registry/v1/cluster_types.go b/pkg/api/registry/v1/cluster_types.go index a25eb726..379c0c55 100644 --- a/pkg/api/registry/v1/cluster_types.go +++ b/pkg/api/registry/v1/cluster_types.go @@ -116,6 +116,9 @@ type ClusterSpec struct { // Capacity cluster information Capacity Capacity `json:"capacity,omitempty"` + + // ServiceMetadata service specific metadata + ServiceMetadata ServiceMetadata `json:"services,omitempty"` } // Offering the cluster is meant for @@ -243,6 +246,12 @@ type Capacity struct { ClusterProvisioning int `json:"clusterProvisioning"` } +type ServiceMetadata map[string]ServiceMetadataItem + +type ServiceMetadataItem map[string]ServiceMetadataMap + +type ServiceMetadataMap map[string]string + // ClusterStatus defines the observed state of Cluster type ClusterStatus struct { // Send/Receive Errors diff --git a/pkg/api/registry/v1/zz_generated.deepcopy.go b/pkg/api/registry/v1/zz_generated.deepcopy.go index cf5f878b..ed867961 100644 --- a/pkg/api/registry/v1/zz_generated.deepcopy.go +++ b/pkg/api/registry/v1/zz_generated.deepcopy.go @@ -185,6 +185,35 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { } } out.Capacity = in.Capacity + if in.ServiceMetadata != nil { + in, out := &in.ServiceMetadata, &out.ServiceMetadata + *out = make(ServiceMetadata, len(*in)) + for key, val := range *in { + var outVal map[string]ServiceMetadataMap + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(ServiceMetadataItem, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(ServiceMetadataMap, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } + (*out)[key] = outVal + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. @@ -296,6 +325,102 @@ func (in *PeerVirtualNetwork) DeepCopy() *PeerVirtualNetwork { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServiceMetadata) DeepCopyInto(out *ServiceMetadata) { + { + in := &in + *out = make(ServiceMetadata, len(*in)) + for key, val := range *in { + var outVal map[string]ServiceMetadataMap + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(ServiceMetadataItem, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(ServiceMetadataMap, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadata. +func (in ServiceMetadata) DeepCopy() ServiceMetadata { + if in == nil { + return nil + } + out := new(ServiceMetadata) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServiceMetadataItem) DeepCopyInto(out *ServiceMetadataItem) { + { + in := &in + *out = make(ServiceMetadataItem, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(ServiceMetadataMap, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataItem. +func (in ServiceMetadataItem) DeepCopy() ServiceMetadataItem { + if in == nil { + return nil + } + out := new(ServiceMetadataItem) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServiceMetadataMap) DeepCopyInto(out *ServiceMetadataMap) { + { + in := &in + *out = make(ServiceMetadataMap, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataMap. +func (in ServiceMetadataMap) DeepCopy() ServiceMetadataMap { + if in == nil { + return nil + } + out := new(ServiceMetadataMap) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Tier) DeepCopyInto(out *Tier) { *out = *in diff --git a/pkg/api/registry/v1alpha1/groupversion_info.go b/pkg/api/registry/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..358015e5 --- /dev/null +++ b/pkg/api/registry/v1alpha1/groupversion_info.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the registry.ethos.adobe.com v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=registry.ethos.adobe.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "registry.ethos.adobe.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/api/registry/v1alpha1/servicemetadatawatcher_types.go b/pkg/api/registry/v1alpha1/servicemetadatawatcher_types.go new file mode 100644 index 00000000..6d0b33d8 --- /dev/null +++ b/pkg/api/registry/v1alpha1/servicemetadatawatcher_types.go @@ -0,0 +1,78 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ServiceMetadataWatcherSpec defines the desired state of ServiceMetadataWatcher +type ServiceMetadataWatcherSpec struct { + WatchedServiceObjects []WatchedServiceObject `json:"watchedServiceObjects"` +} + +type WatchedServiceObject struct { + ObjectReference ObjectReference `json:"objectReference"` + WatchedFields []WatchedField `json:"watchedFields"` +} + +type ObjectReference struct { + Name string `json:"name"` + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} + +func (o *ObjectReference) String() string { + return o.Name + "/" + o.APIVersion + "/" + o.Kind +} + +type WatchedField struct { + Source string `json:"src"` + Destination string `json:"dst"` +} + +type WatchedServiceObjectStatus struct { + LastUpdated metav1.Time `json:"lastUpdated"` + ObjectReference ObjectReference `json:"objectReference"` + Errors []string `json:"errors"` +} + +// ServiceMetadataWatcherStatus defines the observed state of ServiceMetadataWatcher +type ServiceMetadataWatcherStatus struct { + WatchedServiceObjects []WatchedServiceObjectStatus `json:"watchedServiceObjects"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ServiceMetadataWatcher is the Schema for the servicemetadatawatchers API +type ServiceMetadataWatcher struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceMetadataWatcherSpec `json:"spec,omitempty"` + Status ServiceMetadataWatcherStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ServiceMetadataWatcherList contains a list of ServiceMetadataWatcher +type ServiceMetadataWatcherList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceMetadataWatcher `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServiceMetadataWatcher{}, &ServiceMetadataWatcherList{}) +} diff --git a/pkg/api/registry/v1alpha1/zz_generated.deepcopy.go b/pkg/api/registry/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..b7e97571 --- /dev/null +++ b/pkg/api/registry/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,168 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. +func (in *ObjectReference) DeepCopy() *ObjectReference { + if in == nil { + return nil + } + out := new(ObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceMetadataWatcher) DeepCopyInto(out *ServiceMetadataWatcher) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataWatcher. +func (in *ServiceMetadataWatcher) DeepCopy() *ServiceMetadataWatcher { + if in == nil { + return nil + } + out := new(ServiceMetadataWatcher) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceMetadataWatcher) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceMetadataWatcherList) DeepCopyInto(out *ServiceMetadataWatcherList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceMetadataWatcher, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataWatcherList. +func (in *ServiceMetadataWatcherList) DeepCopy() *ServiceMetadataWatcherList { + if in == nil { + return nil + } + out := new(ServiceMetadataWatcherList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceMetadataWatcherList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceMetadataWatcherSpec) DeepCopyInto(out *ServiceMetadataWatcherSpec) { + *out = *in + if in.WatchedServiceObjects != nil { + in, out := &in.WatchedServiceObjects, &out.WatchedServiceObjects + *out = make([]WatchedServiceObject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataWatcherSpec. +func (in *ServiceMetadataWatcherSpec) DeepCopy() *ServiceMetadataWatcherSpec { + if in == nil { + return nil + } + out := new(ServiceMetadataWatcherSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceMetadataWatcherStatus) DeepCopyInto(out *ServiceMetadataWatcherStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceMetadataWatcherStatus. +func (in *ServiceMetadataWatcherStatus) DeepCopy() *ServiceMetadataWatcherStatus { + if in == nil { + return nil + } + out := new(ServiceMetadataWatcherStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WatchedField) DeepCopyInto(out *WatchedField) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WatchedField. +func (in *WatchedField) DeepCopy() *WatchedField { + if in == nil { + return nil + } + out := new(WatchedField) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WatchedServiceObject) DeepCopyInto(out *WatchedServiceObject) { + *out = *in + out.ObjectReference = in.ObjectReference + if in.WatchedFields != nil { + in, out := &in.WatchedFields, &out.WatchedFields + *out = make([]WatchedField, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WatchedServiceObject. +func (in *WatchedServiceObject) DeepCopy() *WatchedServiceObject { + if in == nil { + return nil + } + out := new(WatchedServiceObject) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apiserver/docs/docs.go b/pkg/apiserver/docs/docs.go index c95d7ea7..c213c168 100644 --- a/pkg/apiserver/docs/docs.go +++ b/pkg/apiserver/docs/docs.go @@ -1,5 +1,4 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -208,7 +207,7 @@ const docTemplate = `{ "bearerAuth": [] } ], - "description": "Get an cluster. Auth is required", + "description": "Get a cluster. Auth is required", "consumes": [ "application/json" ], @@ -218,7 +217,7 @@ const docTemplate = `{ "tags": [ "cluster" ], - "summary": "Get an cluster", + "summary": "Get a cluster", "operationId": "v2-get-cluster", "parameters": [ { @@ -307,6 +306,129 @@ const docTemplate = `{ } } } + }, + "/v2/services/{serviceId}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "List all metadata for a service for all clusters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service" + ], + "summary": "Get service metadata", + "operationId": "v2-get-service-metadata", + "parameters": [ + { + "type": "string", + "description": "SNOW Service ID", + "name": "serviceId", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Filter conditions", + "name": "conditions", + "in": "query" + }, + { + "type": "integer", + "description": "Offset to start pagination search results (default is 0)", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "The number of results per page (default is 200)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/pkg_apiserver_web_handler_v2.clusterList" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + } + } + } + }, + "/v2/services/{serviceId}/cluster/{clusterName}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "Get metadata for a service for a specific cluster", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service" + ], + "summary": "Get service metadata for a specific cluster", + "operationId": "v2-get-service-metadata-for-cluster", + "parameters": [ + { + "type": "string", + "description": "SNOW Service ID", + "name": "serviceId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ClusterSpec" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + } + } + } } }, "definitions": { @@ -463,6 +585,14 @@ const docTemplate = `{ "description": "Timestamp when cluster was registered in Cluster Registry\n+kubebuilder:validation:Required", "type": "string" }, + "services": { + "description": "ServiceMetadata service specific metadata", + "allOf": [ + { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata" + } + ] + }, "shortName": { "description": "Cluster name, without dash\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", "type": "string" @@ -572,6 +702,24 @@ const docTemplate = `{ } } }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem" + } + }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap" + } + }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier": { "type": "object", "properties": { @@ -735,13 +883,15 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", - Host: "http://127.0.0.1:8080", + Host: "127.0.0.1:8080", BasePath: "/api", Schemes: []string{"http", "https"}, Title: "Cluster Registry API", Description: "Cluster Registry API", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/pkg/apiserver/docs/swagger.json b/pkg/apiserver/docs/swagger.json index b302b955..22fa0c82 100644 --- a/pkg/apiserver/docs/swagger.json +++ b/pkg/apiserver/docs/swagger.json @@ -13,7 +13,7 @@ "contact": {}, "version": "1.0" }, - "host": "http://127.0.0.1:8080", + "host": "127.0.0.1:8080", "basePath": "/api", "paths": { "/v1/clusters": { @@ -205,7 +205,7 @@ "bearerAuth": [] } ], - "description": "Get an cluster. Auth is required", + "description": "Get a cluster. Auth is required", "consumes": [ "application/json" ], @@ -215,7 +215,7 @@ "tags": [ "cluster" ], - "summary": "Get an cluster", + "summary": "Get a cluster", "operationId": "v2-get-cluster", "parameters": [ { @@ -304,6 +304,129 @@ } } } + }, + "/v2/services/{serviceId}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "List all metadata for a service for all clusters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service" + ], + "summary": "Get service metadata", + "operationId": "v2-get-service-metadata", + "parameters": [ + { + "type": "string", + "description": "SNOW Service ID", + "name": "serviceId", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Filter conditions", + "name": "conditions", + "in": "query" + }, + { + "type": "integer", + "description": "Offset to start pagination search results (default is 0)", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "The number of results per page (default is 200)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/pkg_apiserver_web_handler_v2.clusterList" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + } + } + } + }, + "/v2/services/{serviceId}/cluster/{clusterName}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "description": "Get metadata for a service for a specific cluster", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service" + ], + "summary": "Get service metadata for a specific cluster", + "operationId": "v2-get-service-metadata-for-cluster", + "parameters": [ + { + "type": "string", + "description": "SNOW Service ID", + "name": "serviceId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ClusterSpec" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error" + } + } + } + } } }, "definitions": { @@ -460,6 +583,14 @@ "description": "Timestamp when cluster was registered in Cluster Registry\n+kubebuilder:validation:Required", "type": "string" }, + "services": { + "description": "ServiceMetadata service specific metadata", + "allOf": [ + { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata" + } + ] + }, "shortName": { "description": "Cluster name, without dash\n+kubebuilder:validation:Required\n+kubebuilder:validation:MaxLength=64\n+kubebuilder:validation:MinLength=3", "type": "string" @@ -569,6 +700,24 @@ } } }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem" + } + }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap" + } + }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier": { "type": "object", "properties": { diff --git a/pkg/apiserver/docs/swagger.yaml b/pkg/apiserver/docs/swagger.yaml index 897069ab..24d9fc86 100644 --- a/pkg/apiserver/docs/swagger.yaml +++ b/pkg/apiserver/docs/swagger.yaml @@ -138,6 +138,10 @@ definitions: Timestamp when cluster was registered in Cluster Registry +kubebuilder:validation:Required type: string + services: + allOf: + - $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata' + description: ServiceMetadata service specific metadata shortName: description: |- Cluster name, without dash @@ -232,6 +236,18 @@ definitions: description: Cloud account of the owner type: string type: object + github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata: + additionalProperties: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem' + type: object + github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataItem: + additionalProperties: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap' + type: object + github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadataMap: + additionalProperties: + type: string + type: object github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier: properties: containerRuntime: @@ -351,7 +367,7 @@ definitions: offset: type: integer type: object -host: http://127.0.0.1:8080 +host: 127.0.0.1:8080 info: contact: {} description: Cluster Registry API @@ -481,7 +497,7 @@ paths: get: consumes: - application/json - description: Get an cluster. Auth is required + description: Get a cluster. Auth is required operationId: v2-get-cluster parameters: - description: Name of the cluster to get @@ -506,7 +522,7 @@ paths: $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error' security: - bearerAuth: [] - summary: Get an cluster + summary: Get a cluster tags: - cluster patch: @@ -546,6 +562,86 @@ paths: summary: Patch a cluster tags: - cluster + /v2/services/{serviceId}: + get: + consumes: + - application/json + description: List all metadata for a service for all clusters + operationId: v2-get-service-metadata + parameters: + - description: SNOW Service ID + in: path + name: serviceId + required: true + type: string + - collectionFormat: multi + description: Filter conditions + in: query + items: + type: string + name: conditions + type: array + - description: Offset to start pagination search results (default is 0) + in: query + name: offset + type: integer + - description: The number of results per page (default is 200) + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_apiserver_web_handler_v2.clusterList' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error' + security: + - bearerAuth: [] + summary: Get service metadata + tags: + - service + /v2/services/{serviceId}/cluster/{clusterName}: + get: + consumes: + - application/json + description: Get metadata for a service for a specific cluster + operationId: v2-get-service-metadata-for-cluster + parameters: + - description: SNOW Service ID + in: path + name: serviceId + required: true + type: string + - description: Name of the cluster + in: path + name: clusterName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_api_registry_v1.ClusterSpec' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_adobe_cluster-registry_pkg_apiserver_errors.Error' + security: + - bearerAuth: [] + summary: Get service metadata for a specific cluster + tags: + - service produces: - application/json schemes: diff --git a/pkg/apiserver/web/handler/v1/response.go b/pkg/apiserver/web/handler/v1/response.go index ef4e7053..7f81c9dd 100644 --- a/pkg/apiserver/web/handler/v1/response.go +++ b/pkg/apiserver/web/handler/v1/response.go @@ -27,6 +27,7 @@ type clusterList struct { func newClusterResponse(ctx echo.Context, c *registryv1.Cluster) *registryv1.ClusterSpec { cs := &c.Spec + cs.ServiceMetadata = nil return cs } @@ -36,6 +37,7 @@ func newClusterListResponse(clusters []registryv1.Cluster, count int, offset int for _, c := range clusters { cs := c.Spec + cs.ServiceMetadata = nil r.Items = append(r.Items, &cs) } diff --git a/pkg/apiserver/web/handler/v2/handler.go b/pkg/apiserver/web/handler/v2/handler.go index 8a1083b4..ef306b22 100644 --- a/pkg/apiserver/web/handler/v2/handler.go +++ b/pkg/apiserver/web/handler/v2/handler.go @@ -80,11 +80,15 @@ func (h *handler) Register(v2 *echo.Group) { clusters.GET("/:name", h.GetCluster) clusters.PATCH("/:name", h.PatchCluster, a.VerifyGroupAccess(h.appConfig.ApiAuthorizedGroupId)) clusters.GET("", h.ListClusters) + + services := v2.Group("/services", a.VerifyToken(), web.RateLimiter(h.appConfig)) + services.GET("/:serviceId", h.GetServiceMetadata) + services.GET("/:serviceId/cluster/:clusterName", h.GetServiceMetadataForCluster) } // GetCluster godoc -// @Summary Get an cluster -// @Description Get an cluster. Auth is required +// @Summary Get a cluster +// @Description Get a cluster. Auth is required // @ID v2-get-cluster // @Tags cluster // @Accept json @@ -107,7 +111,7 @@ func (h *handler) GetCluster(c echo.Context) error { return c.JSON(http.StatusNotFound, errors.NotFound()) } - return c.JSON(http.StatusOK, newClusterResponse(c, cluster)) + return c.JSON(http.StatusOK, newClusterResponse(cluster)) } // ListClusters @@ -200,7 +204,87 @@ func (h *handler) PatchCluster(c echo.Context) error { return c.JSON(http.StatusInternalServerError, errors.NewError(err)) } - return c.JSON(http.StatusOK, newClusterResponse(c, cluster)) + return c.JSON(http.StatusOK, newClusterResponse(cluster)) +} + +// GetServiceMetadata +// @Summary Get service metadata +// @Description List all metadata for a service for all clusters +// @ID v2-get-service-metadata +// @Tags service +// @Accept json +// @Produce json +// @Param serviceId path string true "SNOW Service ID" +// @Param conditions query []string false "Filter conditions" collectionFormat(multi) +// @Param offset query integer false "Offset to start pagination search results (default is 0)" +// @Param limit query integer false "The number of results per page (default is 200)" +// @Success 200 {object} clusterList +// @Failure 500 {object} errors.Error +// @Security bearerAuth +// @Router /v2/services/{serviceId} [get] +func (h *handler) GetServiceMetadata(c echo.Context) error { + var clusters []registryv1.Cluster + var count int + + serviceId := c.Param("serviceId") + + offset, err := strconv.Atoi(c.QueryParam("offset")) + if err != nil { + offset = 0 + } + + limit, err := strconv.Atoi(c.QueryParam("limit")) + if err != nil { + limit = 200 + } + + filter := database.NewDynamoDBFilter() + queryConditions := getQueryConditions(c) + + if len(queryConditions) == 0 { + clusters, count, more, _ := h.db.ListClustersWithService(serviceId, offset, limit, "", "", "", "") + return c.JSON(http.StatusOK, newServiceMetadataListResponse(clusters, count, offset, limit, more)) + } + + for _, qc := range queryConditions { + condition, err := models.NewFilterConditionFromQuery(qc) + if err != nil { + return c.JSON(http.StatusBadRequest, errors.NewError(err)) + } + filter.AddCondition(condition) + } + + clusters, count, more, _ := h.db.ListClustersWithServiceAndFilter(serviceId, offset, limit, filter) + return c.JSON(http.StatusOK, newServiceMetadataListResponse(clusters, count, offset, limit, more)) +} + +// GetServiceMetadataForCluster +// @Summary Get service metadata for a specific cluster +// @Description Get metadata for a service for a specific cluster +// @ID v2-get-service-metadata-for-cluster +// @Tags service +// @Accept json +// @Produce json +// @Param serviceId path string true "SNOW Service ID" +// @Param clusterName path string true "Name of the cluster" +// @Success 200 {object} registryv1.ClusterSpec +// @Failure 400 {object} errors.Error +// @Failure 500 {object} errors.Error +// @Security bearerAuth +// @Router /v2/services/{serviceId}/cluster/{clusterName} [get] +func (h *handler) GetServiceMetadataForCluster(c echo.Context) error { + serviceId := c.Param("serviceId") + clusterName := c.Param("clusterName") + cluster, err := h.db.GetClusterWithService(serviceId, clusterName) + if err != nil { + return c.JSON(http.StatusInternalServerError, errors.NewError(err)) + } + + if cluster == nil { + return c.JSON(http.StatusNotFound, errors.NotFound()) + } + + return c.JSON(http.StatusOK, newServiceMetadataResponse(cluster)) } // getCluster by standard name or short name diff --git a/pkg/apiserver/web/handler/v2/response.go b/pkg/apiserver/web/handler/v2/response.go index f6ff71ea..6a997110 100644 --- a/pkg/apiserver/web/handler/v2/response.go +++ b/pkg/apiserver/web/handler/v2/response.go @@ -14,7 +14,6 @@ package v2 import ( registryv1 "github.com/adobe/cluster-registry/pkg/api/registry/v1" - "github.com/labstack/echo/v4" ) type clusterList struct { @@ -25,8 +24,22 @@ type clusterList struct { More bool `json:"more"` } -func newClusterResponse(ctx echo.Context, c *registryv1.Cluster) *registryv1.ClusterSpec { - cs := &c.Spec +type serviceMetadataList struct { + Items []*ServiceMetadata `json:"items"` + ItemsCount int `json:"itemsCount"` + Offset int `json:"offset"` + Limit int `json:"limit"` + More bool `json:"more"` +} + +type ServiceMetadata struct { + Name string `json:"name"` + ServiceMetadata registryv1.ServiceMetadata `json:"services"` +} + +func newClusterResponse(cluster *registryv1.Cluster) *registryv1.ClusterSpec { + cs := &cluster.Spec + cs.ServiceMetadata = nil return cs } @@ -36,6 +49,7 @@ func newClusterListResponse(clusters []registryv1.Cluster, count int, offset int for _, c := range clusters { cs := c.Spec + cs.ServiceMetadata = nil r.Items = append(r.Items, &cs) } @@ -46,3 +60,30 @@ func newClusterListResponse(clusters []registryv1.Cluster, count int, offset int return r } + +func newServiceMetadataResponse(cluster *registryv1.Cluster) *ServiceMetadata { + return &ServiceMetadata{ + Name: cluster.Spec.Name, + ServiceMetadata: cluster.Spec.ServiceMetadata, + } +} + +func newServiceMetadataListResponse(clusters []registryv1.Cluster, count int, offset int, limit int, more bool) *serviceMetadataList { + r := new(serviceMetadataList) + r.Items = make([]*ServiceMetadata, 0) + + for _, c := range clusters { + sm := ServiceMetadata{ + Name: c.Spec.Name, + ServiceMetadata: c.Spec.ServiceMetadata, + } + r.Items = append(r.Items, &sm) + } + + r.ItemsCount = count + r.Offset = offset + r.Limit = limit + r.More = more + + return r +} diff --git a/pkg/client/controllers/cluster_controller.go b/pkg/client/controllers/cluster_controller.go index 1f4dfe50..261d8675 100644 --- a/pkg/client/controllers/cluster_controller.go +++ b/pkg/client/controllers/cluster_controller.go @@ -76,7 +76,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // ReconcileCreateUpdate ... func (r *ClusterReconciler) ReconcileCreateUpdate(instance *registryv1.Cluster, log logr.Logger) (ctrl.Result, error) { - hash := Hash(instance) + hash := hashCluster(instance) annotations := instance.GetAnnotations() if annotations == nil { @@ -91,19 +91,13 @@ func (r *ClusterReconciler) ReconcileCreateUpdate(instance *registryv1.Cluster, return ctrl.Result{}, err } - if err := r.Client.Update(context.TODO(), instance); err != nil { - log.Error(err, "Cannot update the object") - return ctrl.Result{}, err - } - log.V(1).Info("Object updated") - return ctrl.Result{}, nil } func (r *ClusterReconciler) hasDifferentHash(object runtime.Object) bool { instance := object.(*registryv1.Cluster) oldHash := instance.GetAnnotations()[HashAnnotation] - newHash := Hash(instance) + newHash := hashCluster(instance) if oldHash != newHash { r.Log.Info("Different hash found", "old", oldHash, "new", newHash, @@ -152,9 +146,9 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// Hash returns a SHA256 hash of the Cluster object, after removing the ResourceVersion, -// ManagedFields and Hash annotation -func Hash(instance *registryv1.Cluster) string { +// hashCluster returns a SHA256 hash of the Cluster object, after removing the ResourceVersion, +// ManagedFields and hashCluster annotation +func hashCluster(instance *registryv1.Cluster) string { clone := instance.DeepCopyObject().(*registryv1.Cluster) annotations := clone.GetAnnotations() diff --git a/pkg/client/controllers/cluster_controller_test.go b/pkg/client/controllers/cluster_controller_test.go index f530699e..dcba363c 100644 --- a/pkg/client/controllers/cluster_controller_test.go +++ b/pkg/client/controllers/cluster_controller_test.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -var _ = Describe("Client Controller", func() { +var _ = Describe("Cluster Controller", func() { const ( timeout = time.Second * 30 interval = time.Millisecond * 250 diff --git a/pkg/client/controllers/result.go b/pkg/client/controllers/result.go new file mode 100644 index 00000000..92684d9a --- /dev/null +++ b/pkg/client/controllers/result.go @@ -0,0 +1,36 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +func requeueIfError(err error) (ctrl.Result, error) { + return ctrl.Result{}, err +} + +func noRequeue() (ctrl.Result, error) { + return ctrl.Result{}, nil +} + +/* +func requeueAfter(interval time.Duration, err error) (ctrl.Result, error) { + return ctrl.Result{RequeueAfter: interval}, err +} + +func requeueImmediately() (ctrl.Result, error) { + return ctrl.Result{Requeue: true}, nil +} + +*/ diff --git a/pkg/client/controllers/servicemetadatawatcher_controller.go b/pkg/client/controllers/servicemetadatawatcher_controller.go new file mode 100644 index 00000000..97c7c25b --- /dev/null +++ b/pkg/client/controllers/servicemetadatawatcher_controller.go @@ -0,0 +1,314 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + registryv1 "github.com/adobe/cluster-registry/pkg/api/registry/v1" + registryv1alpha1 "github.com/adobe/cluster-registry/pkg/api/registry/v1alpha1" + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + crevent "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "strings" + "time" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ServiceMetadataWatcherReconciler reconciles a ServiceMetadataWatcher object +type ServiceMetadataWatcherReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + WatchedGVKs []schema.GroupVersionKind + ServiceIdAnnotation string +} + +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=servicemetadatawatchers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=servicemetadatawatchers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=registry.ethos.adobe.com,resources=servicemetadatawatchers/finalizers,verbs=update + +// 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 *ServiceMetadataWatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var start = time.Now() + + log := r.Log.WithValues("name", req.NamespacedName) + log.Info("start") + defer log.Info("end", "duration", time.Since(start)) + + instance := new(registryv1alpha1.ServiceMetadataWatcher) + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + log.Error(err, "unable to fetch object") + return requeueIfError(client.IgnoreNotFound(err)) + } + + if instance.ObjectMeta.DeletionTimestamp != nil { + return noRequeue() + } + + if len(instance.Spec.WatchedServiceObjects) == 0 { + log.Info("no watched objects") + // TODO: probably update status with error + return noRequeue() + } + + serviceId, err := r.getServiceIdFromNamespaceAnnotation(ctx, instance.GetNamespace()) + if err != nil { + log.Error(err, "cannot get serviceId from namespace annotation", "namespace", instance.GetNamespace()) + return noRequeue() + } + + var patches [][]byte + + for _, wso := range instance.Spec.WatchedServiceObjects { + gv, err := schema.ParseGroupVersion(wso.ObjectReference.APIVersion) + if err != nil { + log.Error(err, "cannot parse APIVersion", "apiVersion", wso.ObjectReference.APIVersion) + // TODO: update status with error + return requeueIfError(err) + } + + gvk := schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: wso.ObjectReference.Kind, + } + + // check if gvk is allowed + if !r.isAllowedGVK(gvk) { + log.Info("watched object GVK is not allowed, skipping", "gvk", gvk.String()) + // TODO: update status with error + return noRequeue() + } + + obj := new(unstructured.Unstructured) + obj.SetGroupVersionKind(gvk) + + if err := r.Client.Get(ctx, types.NamespacedName{ + Namespace: instance.Namespace, + Name: wso.ObjectReference.Name, + }, obj); err != nil { + log.Error(err, "cannot get object", + "name", wso.ObjectReference.Name, + "namespace", instance.Namespace, + "gvk", obj.GroupVersionKind().String()) + return requeueIfError(err) + } + + if len(wso.WatchedFields) == 0 { + // TODO: update status with error + return noRequeue() + } + + for _, field := range wso.WatchedFields { + // TODO: validate field Source & Destination somewhere + + value, found, err := unstructured.NestedString(obj.Object, strings.Split(field.Source, ".")...) + if err != nil { + // TODO: update status with error + log.Error(err, "cannot get field", "field", field.Source) + continue + } + + if !found { + log.Error(err, "field not found", "field", field.Source) + // TODO: update status with error + continue + } + + patch, err := createServiceMetadataPatch(serviceId, instance.Namespace, field.Destination, value) + if err != nil { + log.Error(err, "cannot create patch") + continue + } + patches = append(patches, patch) + } + } + + if len(patches) == 0 { + return noRequeue() + } + + if err := r.applyServiceMetadataPatches(ctx, patches); err != nil { + log.Error(err, "cannot update cluster service metadata") + return requeueIfError(err) + } + + return noRequeue() +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ServiceMetadataWatcherReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { + options := controller.Options{MaxConcurrentReconciles: 10} + b := ctrl.NewControllerManagedBy(mgr).For(®istryv1alpha1.ServiceMetadataWatcher{}, builder.WithPredicates(r.eventFilters())) + for _, gvk := range r.WatchedGVKs { + obj := new(unstructured.Unstructured) + obj.SetGroupVersionKind(gvk) + b.Watches(obj, handler.EnqueueRequestsFromMapFunc(r.enqueueRequestsFromMapFunc(gvk)), builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e crevent.CreateEvent) bool { + return true + }, + UpdateFunc: func(e crevent.UpdateEvent) bool { + return true + }, + DeleteFunc: func(e crevent.DeleteEvent) bool { + // not interested in ServiceObject delete events (for now) + return false + }, + })) + } + err := b.WithOptions(options).Complete(r) + return err +} + +func (r *ServiceMetadataWatcherReconciler) isAllowedGVK(gvk schema.GroupVersionKind) bool { + for _, watchedGVK := range r.WatchedGVKs { + if gvk.String() == watchedGVK.String() { + return true + } + } + return false +} + +func (r *ServiceMetadataWatcherReconciler) applyServiceMetadataPatches(ctx context.Context, patches [][]byte) error { + clusterList := ®istryv1.ClusterList{} + // TODO: get namespace from config + if err := r.Client.List(ctx, clusterList, &client.ListOptions{Namespace: "cluster-registry"}); err != nil { + return err + } + + for i := range clusterList.Items { + for _, patch := range patches { + // TODO: find a better way to do this (i.e. group patches) + if err := r.Client.Patch(ctx, &clusterList.Items[i], client.RawPatch(types.MergePatchType, patch)); err != nil { + r.Log.Info("cannot patch cluster", "name", clusterList.Items[i].Name, "namespace", clusterList.Items[i].Namespace, "error", err) + } + } + } + + return nil +} + +func (r *ServiceMetadataWatcherReconciler) eventFilters() predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e crevent.CreateEvent) bool { + r.Log.Info("New event", "type", "Create", "name", e.Object.GetName(), "namespace", e.Object.GetNamespace()) + return true + }, + UpdateFunc: func(e crevent.UpdateEvent) bool { + r.Log.Info("New event", "type", "Update", "name", e.ObjectNew.GetName(), "namespace", e.ObjectNew.GetNamespace()) + return true + }, + DeleteFunc: func(e crevent.DeleteEvent) bool { + r.Log.Info("New event", "type", "Delete", "name", e.Object.GetName(), "namespace", e.Object.GetNamespace()) + return !e.DeleteStateUnknown + }, + } +} + +// enqueueRequestsFromMapFunc enqueues reconcile requests for ServiceMetadataWatchers when watched objects are updated +func (r *ServiceMetadataWatcherReconciler) enqueueRequestsFromMapFunc(gvk schema.GroupVersionKind) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + var requests []reconcile.Request + + if obj.GetObjectKind().GroupVersionKind() != gvk { + // object gvk is of no interest, carry on + return requests + } + + // limit the search to the watcher's namespace + list := ®istryv1alpha1.ServiceMetadataWatcherList{} + if err := r.List(ctx, list, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil { + r.Log.Error(err, "failed to list ServiceMetadataWatchers", + "namespace", obj.GetNamespace()) + return requests + } + + for _, smw := range list.Items { + for _, wso := range smw.Spec.WatchedServiceObjects { + gv, err := schema.ParseGroupVersion(wso.ObjectReference.APIVersion) + if err != nil || gv != gvk.GroupVersion() || wso.ObjectReference.Kind != gvk.Kind { + continue + } + if smw.Namespace == obj.GetNamespace() && wso.ObjectReference.Name == obj.GetName() { + r.Log.Info("Watched object was updated, enqueueing watcher reconcile request", + "name", obj.GetName(), + "namespace", smw.Namespace, + "gvk", gvk.String(), + "watcher", smw.Name) + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: smw.Namespace, + Name: smw.Name, + }, + }) + } + } + } + + return requests + } +} + +func (r *ServiceMetadataWatcherReconciler) getServiceIdFromNamespaceAnnotation(ctx context.Context, namespace string) (string, error) { + ns := &corev1.Namespace{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil { + r.Log.Error(err, "cannot get namespace", "namespace", namespace) + return "", err + } + + serviceId, ok := ns.GetAnnotations()[r.ServiceIdAnnotation] + + if !ok { + return "", fmt.Errorf("namespace %s does not have annotation %s", namespace, r.ServiceIdAnnotation) + } + + return serviceId, nil +} + +func createServiceMetadataPatch(serviceId string, namespace string, field string, value string) ([]byte, error) { + oldCluster := ®istryv1.Cluster{} + oldClusterJSON, err := json.Marshal(oldCluster) + if err != nil { + return nil, err + } + + newCluster := oldCluster.DeepCopy() + newCluster.Spec.ServiceMetadata = registryv1.ServiceMetadata{ + serviceId: registryv1.ServiceMetadataItem{ + namespace: registryv1.ServiceMetadataMap{ + field: value, + }, + }, + } + newClusterJSON, err := json.Marshal(newCluster) + if err != nil { + return nil, err + } + + return jsonpatch.CreateMergePatch(oldClusterJSON, newClusterJSON) +} diff --git a/pkg/client/controllers/servicemetadatawatcher_controller_test.go b/pkg/client/controllers/servicemetadatawatcher_controller_test.go new file mode 100644 index 00000000..ee781b1d --- /dev/null +++ b/pkg/client/controllers/servicemetadatawatcher_controller_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +package controllers + +import ( + . "github.com/onsi/ginkgo" +) + +var _ = Describe("ServiceMetadataWatcher Controller", func() { + + BeforeEach(func() { + // + }) + + AfterEach(func() { + // + }) + + Context("...", func() { + + }) +}) diff --git a/pkg/database/database.go b/pkg/database/database.go index 06760c53..e17e062c 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -43,6 +43,9 @@ type Db interface { DeleteCluster(name string) error Status() error Mock() *dynamock.DynaMock + ListClustersWithService(serviceId string, offset int, limit int, environment string, region string, status string, lastUpdated string) ([]registryv1.Cluster, int, bool, error) + ListClustersWithServiceAndFilter(serviceId string, offset int, limit int, filter *DynamoDBFilter) ([]registryv1.Cluster, int, bool, error) + GetClusterWithService(serviceId string, clusterName string) (*registryv1.Cluster, error) } // db struct @@ -420,3 +423,95 @@ func (d *db) DeleteCluster(name string) error { return nil } + +// ListClustersWithService gets service metadata for a given serviceId on all clusters +func (d *db) ListClustersWithService(serviceId string, offset int, limit int, environment string, region string, status string, lastUpdated string) ([]registryv1.Cluster, int, bool, error) { + clusters, _, _, err := d.ListClusters(offset, limit, environment, region, status, lastUpdated) + + var clustersWithService []registryv1.Cluster + + for _, cluster := range clusters { + var serviceMetadata = registryv1.ServiceMetadata{} + for service := range cluster.Spec.ServiceMetadata { + if service == serviceId { + serviceMetadata[service] = cluster.Spec.ServiceMetadata[service] + cluster.Spec.ServiceMetadata = serviceMetadata + clustersWithService = append(clustersWithService, cluster) + } + } + } + + count := len(clustersWithService) + endIndex := offset + limit + more := false + + if endIndex > count { + endIndex = count + } + if endIndex < count { + more = true + } + + return clustersWithService, count, more, err +} + +// ListClustersWithServiceAndFilter gets service metadata for a given serviceId on all clusters with additional filtering options +func (d *db) ListClustersWithServiceAndFilter(serviceId string, offset int, limit int, filter *DynamoDBFilter) ([]registryv1.Cluster, int, bool, error) { + clusters, _, _, err := d.ListClustersWithFilter(offset, limit, filter) + + if err != nil { + msg := fmt.Sprintf("Failed to list clusters with filter: '%v'.", err) + log.Errorf(msg) + return nil, 0, false, fmt.Errorf(msg) + } + + var clustersWithService []registryv1.Cluster + + for _, cluster := range clusters { + var serviceMetadata = registryv1.ServiceMetadata{} + for service := range cluster.Spec.ServiceMetadata { + if service == serviceId { + serviceMetadata[service] = cluster.Spec.ServiceMetadata[service] + cluster.Spec.ServiceMetadata = serviceMetadata + clustersWithService = append(clustersWithService, cluster) + } + } + } + + count := len(clustersWithService) + endIndex := offset + limit + more := false + + if endIndex > count { + endIndex = count + } + if endIndex < count { + more = true + } + + return clustersWithService, count, more, err +} + +// GetClusterWithService gets service metadata for a given serviceId on a given cluster +func (d *db) GetClusterWithService(serviceId string, clusterName string) (*registryv1.Cluster, error) { + cluster, err := d.GetCluster(clusterName) + + if err != nil { + return nil, err + } + + if cluster == nil { + return nil, nil + } + + var serviceMetadata = registryv1.ServiceMetadata{} + for service := range cluster.Spec.ServiceMetadata { + if service == serviceId { + serviceMetadata[service] = cluster.Spec.ServiceMetadata[service] + cluster.Spec.ServiceMetadata = serviceMetadata + return cluster, nil + } + } + + return nil, err +}