From b93e0dfc7a2837c73deab8ecd2330a1d71f5da36 Mon Sep 17 00:00:00 2001 From: aalexandru Date: Wed, 26 Jul 2023 11:51:48 +0300 Subject: [PATCH] Add service metadata endpoints --- .../registry.ethos.adobe.com_clusters.yaml | 9 ++ local/database/dummy-data.yaml | 33 +++- local/database/reset.sh | 6 +- pkg/api/registry/v1/cluster_types.go | 9 ++ pkg/api/registry/v1/zz_generated.deepcopy.go | 145 ++++++++++++++++- pkg/apiserver/docs/docs.go | 153 +++++++++++++++++- pkg/apiserver/docs/swagger.json | 153 +++++++++++++++++- pkg/apiserver/docs/swagger.yaml | 102 +++++++++++- pkg/apiserver/web/handler/v1/response.go | 2 + pkg/apiserver/web/handler/v2/handler.go | 92 ++++++++++- pkg/apiserver/web/handler/v2/response.go | 47 +++++- pkg/database/database.go | 89 ++++++++++ 12 files changed, 806 insertions(+), 34 deletions(-) diff --git a/config/crd/bases/registry.ethos.adobe.com_clusters.yaml b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml index 902ed5bb..4dbb4e45 100644 --- a/config/crd/bases/registry.ethos.adobe.com_clusters.yaml +++ b/config/crd/bases/registry.ethos.adobe.com_clusters.yaml @@ -220,6 +220,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/local/database/dummy-data.yaml b/local/database/dummy-data.yaml index 7b4c2e98..5f288d58 100644 --- a/local/database/dummy-data.yaml +++ b/local/database/dummy-data.yaml @@ -96,18 +96,30 @@ 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: - name: cluster02-prod-euwest1 + name: cluster02-prod-euwest1 namespace: cluster-registry spec: - name: cluster02-prod-euwest1 - shortName: cluster02prodeuwest1 + name: cluster02-prod-euwest1 + shortName: cluster02prodeuwest1 apiServer: endpoint: https://cluster02-prod-euwest1.example.com certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0== - region: euwest1 + region: euwest1 cloudType: azure cloudProviderRegion: euwest1 environment: Prod @@ -171,6 +183,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/registry/v1/cluster_types.go b/pkg/api/registry/v1/cluster_types.go index 218b2040..14ea42d6 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 @@ -256,6 +259,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 5d4917f6..8273be12 100644 --- a/pkg/api/registry/v1/zz_generated.deepcopy.go +++ b/pkg/api/registry/v1/zz_generated.deepcopy.go @@ -61,6 +61,21 @@ func (in *AllowedOnboardingTeam) DeepCopy() *AllowedOnboardingTeam { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Capacity) DeepCopyInto(out *Capacity) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Capacity. +func (in *Capacity) DeepCopy() *Capacity { + if in == nil { + return nil + } + out := new(Capacity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in @@ -171,6 +186,34 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { (*out)[key] = val } } + 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 { + in, out := &val, &outVal + *out = make(ServiceMetadataItem, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &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. @@ -224,17 +267,16 @@ func (in *Extra) DeepCopyInto(out *Extra) { if in.EcrIamArns != nil { in, out := &in.EcrIamArns, &out.EcrIamArns *out = make(map[string][]string, len(*in)) - for i := range *in { + for key, val := range *in { var outVal []string - if (*in)[i] == nil { - (*out)[i] = nil + if val == nil { + (*out)[key] = nil } else { - val := (*in)[i] in, out := &val, &outVal *out = make([]string, len(*in)) copy(*out, *in) } - (*out)[i] = outVal + (*out)[key] = outVal } } if in.NFSInfo != nil { @@ -297,6 +339,99 @@ 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 { + in, out := &val, &outVal + *out = make(ServiceMetadataItem, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &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 { + in, out := &val, &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/apiserver/docs/docs.go b/pkg/apiserver/docs/docs.go index 27616b74..acd9f8fc 100644 --- a/pkg/apiserver/docs/docs.go +++ b/pkg/apiserver/docs/docs.go @@ -208,7 +208,7 @@ const docTemplate = `{ "bearerAuth": [] } ], - "description": "Get an cluster. Auth is required", + "description": "Get a cluster. Auth is required", "consumes": [ "application/json" ], @@ -218,7 +218,7 @@ const docTemplate = `{ "tags": [ "cluster" ], - "summary": "Get an cluster", + "summary": "Get a cluster", "operationId": "v2-get-cluster", "parameters": [ { @@ -278,13 +278,136 @@ const docTemplate = `{ }, { "description": "Request body", - "name": "clusterPatch", + "name": "clusterSpec", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/pkg_apiserver_web_handler_v2.ClusterPatch" + "$ref": "#/definitions/pkg_apiserver_web_handler_v2.ClusterSpec" + } + } + ], + "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" + } + } + } + } + }, + "/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": { @@ -467,6 +590,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" @@ -593,6 +724,18 @@ const docTemplate = `{ } } }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier": { "type": "object", "properties": { @@ -691,7 +834,7 @@ const docTemplate = `{ } } }, - "pkg_apiserver_web_handler_v2.ClusterPatch": { + "pkg_apiserver_web_handler_v2.ClusterSpec": { "type": "object", "properties": { "phase": { diff --git a/pkg/apiserver/docs/swagger.json b/pkg/apiserver/docs/swagger.json index e0edb5bd..87583c17 100644 --- a/pkg/apiserver/docs/swagger.json +++ b/pkg/apiserver/docs/swagger.json @@ -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": [ { @@ -275,13 +275,136 @@ }, { "description": "Request body", - "name": "clusterPatch", + "name": "clusterSpec", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/pkg_apiserver_web_handler_v2.ClusterPatch" + "$ref": "#/definitions/pkg_apiserver_web_handler_v2.ClusterSpec" + } + } + ], + "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" + } + } + } + } + }, + "/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": { @@ -464,6 +587,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" @@ -590,6 +721,18 @@ } } }, + "github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier": { "type": "object", "properties": { @@ -688,7 +831,7 @@ } } }, - "pkg_apiserver_web_handler_v2.ClusterPatch": { + "pkg_apiserver_web_handler_v2.ClusterSpec": { "type": "object", "properties": { "phase": { diff --git a/pkg/apiserver/docs/swagger.yaml b/pkg/apiserver/docs/swagger.yaml index f3e24e44..c599f918 100644 --- a/pkg/apiserver/docs/swagger.yaml +++ b/pkg/apiserver/docs/swagger.yaml @@ -139,6 +139,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 @@ -245,6 +249,14 @@ definitions: description: Cloud account of the owner type: string type: object + github_com_adobe_cluster-registry_pkg_api_registry_v1.ServiceMetadata: + additionalProperties: + additionalProperties: + additionalProperties: + type: string + type: object + type: object + type: object github_com_adobe_cluster-registry_pkg_api_registry_v1.Tier: properties: containerRuntime: @@ -328,7 +340,7 @@ definitions: offset: type: integer type: object - pkg_apiserver_web_handler_v2.ClusterPatch: + pkg_apiserver_web_handler_v2.ClusterSpec: properties: phase: enum: @@ -494,7 +506,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 @@ -519,7 +531,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: @@ -535,10 +547,10 @@ paths: type: string - description: Request body in: body - name: clusterPatch + name: clusterSpec required: true schema: - $ref: '#/definitions/pkg_apiserver_web_handler_v2.ClusterPatch' + $ref: '#/definitions/pkg_apiserver_web_handler_v2.ClusterSpec' produces: - application/json responses: @@ -559,6 +571,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/database/database.go b/pkg/database/database.go index 06760c53..9e65d402 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,89 @@ 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, count, more, err := d.ListClustersWithFilter(offset, limit, filter) + + 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 +}