diff --git a/fleetshard/pkg/central/reconciler/reconciler.go b/fleetshard/pkg/central/reconciler/reconciler.go index 73dad97ea2..9cf2ee4564 100644 --- a/fleetshard/pkg/central/reconciler/reconciler.go +++ b/fleetshard/pkg/central/reconciler/reconciler.go @@ -29,6 +29,7 @@ import ( centralConstants "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/converters" + "github.com/stackrox/acs-fleet-manager/pkg/client/fleetmanager" "github.com/stackrox/acs-fleet-manager/pkg/features" "github.com/stackrox/rox/operator/apis/platform/v1alpha1" "github.com/stackrox/rox/pkg/declarativeconfig" @@ -102,20 +103,21 @@ type CentralReconcilerOptions struct { // CentralReconciler is a reconciler tied to a one Central instance. It installs, updates and deletes Central instances // in its Reconcile function. type CentralReconciler struct { - client ctrlClient.Client - central private.ManagedCentral - status *int32 - lastCentralHash [16]byte - useRoutes bool - Resources bool - routeService *k8s.RouteService - secretBackup *k8s.SecretBackup - secretCipher cipher.Cipher - egressProxyImage string - telemetry config.Telemetry - clusterName string - environment string - auditLogging config.AuditLogging + client ctrlClient.Client + fleetmanagerClient *fleetmanager.Client + central private.ManagedCentral + status *int32 + lastCentralHash [16]byte + useRoutes bool + Resources bool + routeService *k8s.RouteService + secretBackup *k8s.SecretBackup + secretCipher cipher.Cipher + egressProxyImage string + telemetry config.Telemetry + clusterName string + environment string + auditLogging config.AuditLogging managedDBEnabled bool managedDBProvisioningClient cloudprovider.DBClient @@ -173,6 +175,11 @@ func (r *CentralReconciler) Reconcile(ctx context.Context, remoteCentral private return nil, errors.Wrapf(err, "unable to ensure that namespace %s exists", remoteCentralNamespace) } + err = r.restoreCentralSecrets(ctx, remoteCentral) + if err != nil { + return nil, err + } + if err := r.ensureChartResourcesExist(ctx, remoteCentral); err != nil { return nil, errors.Wrapf(err, "unable to install chart resource for central %s/%s", central.GetNamespace(), central.GetName()) } @@ -381,6 +388,50 @@ func (r *CentralReconciler) getInstanceConfig(remoteCentral *private.ManagedCent return central, nil } +func (r *CentralReconciler) restoreCentralSecrets(ctx context.Context, remoteCentral private.ManagedCentral) error { + restoreSecrets := []string{} + for _, secretName := range remoteCentral.Metadata.SecretsStored { // pragma: allowlist secret + exists, err := r.checkSecretExists(ctx, remoteCentral.Metadata.Namespace, secretName) + if err != nil { + return err + } + + if !exists { + restoreSecrets = append(restoreSecrets, secretName) + } + } + + if len(restoreSecrets) == 0 { + // nothing to restore + return nil + } + + glog.Info(fmt.Sprintf("Restore secret for tenant: %s/%s", remoteCentral.Id, r.central.Metadata.Namespace), restoreSecrets) + central, _, err := r.fleetmanagerClient.PrivateAPI().GetCentral(ctx, remoteCentral.Id) + if err != nil { + return fmt.Errorf("loading secrets for central %s: %w", remoteCentral.Id, err) + } + + decryptedSecrets, err := r.decryptSecrets(central.Metadata.Secrets) + if err != nil { + return fmt.Errorf("decrypting secrets for central %s: %w", central.Id, err) + } + + for _, secretName := range restoreSecrets { // pragma: allowlist secret + secretToRestore, secretFound := decryptedSecrets[secretName] + if !secretFound { + return fmt.Errorf("finding secret %s in decrypted secret map", secretName) + } + + if err := r.client.Create(ctx, secretToRestore); err != nil { + return fmt.Errorf("recreating secret %s for central %s: %w", secretName, central.Id, err) + } + + } + + return nil +} + func (r *CentralReconciler) reconcileAdminPasswordGeneration(central *v1alpha1.Central) error { if !r.wantsAuthProvider { central.Spec.Central.AdminPasswordGenerationDisabled = pointer.Bool(false) @@ -814,6 +865,31 @@ func (r *CentralReconciler) collectSecretsEncrypted(ctx context.Context, remoteC return encryptedSecrets, nil } +func (r *CentralReconciler) decryptSecrets(secrets map[string]string) (map[string]*corev1.Secret, error) { + decryptedSecrets := map[string]*corev1.Secret{} + + for secretName, ciphertext := range secrets { + decodedCipher, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return nil, fmt.Errorf("decoding secret %s: %w", secretName, err) + } + + plaintextSecret, err := r.secretCipher.Decrypt(decodedCipher) + if err != nil { + return nil, fmt.Errorf("decrypting secret %s: %w", secretName, err) + } + + var secret corev1.Secret + if err := json.Unmarshal(plaintextSecret, &secret); err != nil { + return nil, fmt.Errorf("unmarshaling secret %s: %w", secretName, err) + } + + decryptedSecrets[secretName] = &secret // pragma: allowlist secret + } + + return decryptedSecrets, nil +} + func (r *CentralReconciler) encryptSecrets(secrets map[string]*corev1.Secret) (map[string]string, error) { encryptedSecrets := map[string]string{} @@ -1479,25 +1555,26 @@ func (r *CentralReconciler) ensureSecretExists( } // NewCentralReconciler ... -func NewCentralReconciler(k8sClient ctrlClient.Client, central private.ManagedCentral, +func NewCentralReconciler(k8sClient ctrlClient.Client, fleetmanagerClient *fleetmanager.Client, central private.ManagedCentral, managedDBProvisioningClient cloudprovider.DBClient, managedDBInitFunc postgres.CentralDBInitFunc, secretCipher cipher.Cipher, opts CentralReconcilerOptions, ) *CentralReconciler { return &CentralReconciler{ - client: k8sClient, - central: central, - status: pointer.Int32(FreeStatus), - useRoutes: opts.UseRoutes, - wantsAuthProvider: opts.WantsAuthProvider, - routeService: k8s.NewRouteService(k8sClient), - secretBackup: k8s.NewSecretBackup(k8sClient), - secretCipher: secretCipher, // pragma: allowlist secret - egressProxyImage: opts.EgressProxyImage, - telemetry: opts.Telemetry, - clusterName: opts.ClusterName, - environment: opts.Environment, - auditLogging: opts.AuditLogging, + client: k8sClient, + fleetmanagerClient: fleetmanagerClient, + central: central, + status: pointer.Int32(FreeStatus), + useRoutes: opts.UseRoutes, + wantsAuthProvider: opts.WantsAuthProvider, + routeService: k8s.NewRouteService(k8sClient), + secretBackup: k8s.NewSecretBackup(k8sClient), + secretCipher: secretCipher, // pragma: allowlist secret + egressProxyImage: opts.EgressProxyImage, + telemetry: opts.Telemetry, + clusterName: opts.ClusterName, + environment: opts.Environment, + auditLogging: opts.AuditLogging, managedDBEnabled: opts.ManagedDBEnabled, managedDBProvisioningClient: managedDBProvisioningClient, diff --git a/fleetshard/pkg/central/reconciler/reconciler_test.go b/fleetshard/pkg/central/reconciler/reconciler_test.go index 11598abeee..7cc1ebf56f 100644 --- a/fleetshard/pkg/central/reconciler/reconciler_test.go +++ b/fleetshard/pkg/central/reconciler/reconciler_test.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "embed" + "encoding/base64" "fmt" + "net/http" "strings" "testing" "time" @@ -33,6 +35,7 @@ import ( "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/util" centralConstants "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" + "github.com/stackrox/acs-fleet-manager/pkg/client/fleetmanager" "github.com/stackrox/acs-fleet-manager/pkg/features" "github.com/stackrox/rox/operator/apis/platform/v1alpha1" "github.com/stretchr/testify/assert" @@ -138,6 +141,7 @@ func getClientTrackerAndReconciler( fakeClient, tracker := testutils.NewFakeClientWithTracker(t, k8sObjects...) reconciler := NewCentralReconciler( fakeClient, + fleetmanager.NewClientMock().Client(), centralConfig, managedDBClient, centralDBInitFunc, @@ -1698,3 +1702,130 @@ func TestReconcileDeclarativeConfigurationData(t *testing.T) { }) } } + +func TestRestoreCentralSecrets(t *testing.T) { + testCases := []struct { + name string + buildCentral func() private.ManagedCentral + mockObjects []client.Object + buildFMClient func() *fleetmanager.Client + expectedErrorMsgContains string + expectedObjects []client.Object + }{ + { + name: "no error for SecretsStored not set", + buildCentral: func() private.ManagedCentral { + return simpleManagedCentral + }, + }, + { + name: "no error for existing secrets in SecretsStored", + buildCentral: func() private.ManagedCentral { + newCentral := simpleManagedCentral + newCentral.Metadata.SecretsStored = []string{"central-tls", "central-db-password"} + return newCentral + }, + mockObjects: []client.Object{ + centralTLSSecretObject(), + centralDBPasswordSecretObject(), + }, + }, + { + name: "return errors from fleetmanager", + buildCentral: func() private.ManagedCentral { + newCentral := simpleManagedCentral + newCentral.Metadata.SecretsStored = []string{"central-tls", "central-db-password"} + return newCentral + }, + mockObjects: []client.Object{ + centralTLSSecretObject(), + }, + buildFMClient: func() *fleetmanager.Client { + mockClient := fleetmanager.NewClientMock() + mockClient.PrivateAPIMock.GetCentralFunc = func(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) { + return private.ManagedCentral{}, nil, errors.New("test error") + } + return mockClient.Client() + }, + expectedErrorMsgContains: "loading secrets for central cb45idheg5ip6dq1jo4g: test error", + }, + { + // force encrypt error by using non base64 value for central-db-password + name: "return errors from decryptSecrets", + buildCentral: func() private.ManagedCentral { + newCentral := simpleManagedCentral + newCentral.Metadata.SecretsStored = []string{"central-tls", "central-db-password"} + return newCentral + }, + mockObjects: []client.Object{ + centralTLSSecretObject(), + }, + buildFMClient: func() *fleetmanager.Client { + mockClient := fleetmanager.NewClientMock() + mockClient.PrivateAPIMock.GetCentralFunc = func(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) { + returnCentral := simpleManagedCentral + returnCentral.Metadata.Secrets = map[string]string{"central-db-password": "testpw"} + return returnCentral, nil, nil + } + return mockClient.Client() + }, + expectedErrorMsgContains: "decrypting secrets for central", + }, + { + name: "expect secrets to exist after secret restore", + buildCentral: func() private.ManagedCentral { + newCentral := simpleManagedCentral + newCentral.Metadata.SecretsStored = []string{"central-tls", "central-db-password"} + return newCentral + }, + buildFMClient: func() *fleetmanager.Client { + mockClient := fleetmanager.NewClientMock() + mockClient.PrivateAPIMock.GetCentralFunc = func(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) { + returnCentral := simpleManagedCentral + centralTLS := `{"metadata":{"name":"central-tls","namespace":"rhacs-cb45idheg5ip6dq1jo4g","creationTimestamp":null}}` + centralDBPW := `{"metadata":{"name":"central-db-password","namespace":"rhacs-cb45idheg5ip6dq1jo4g","creationTimestamp":null}}` + + encode := base64.StdEncoding.EncodeToString + // we need to encode twice, once for b64 test cipher used + // once for the b64 encoding done to transfer secret data via API + returnCentral.Metadata.Secrets = map[string]string{ + "central-tls": encode([]byte(encode([]byte(centralTLS)))), + "central-db-password": encode([]byte(encode([]byte(centralDBPW)))), + } + return returnCentral, nil, nil + } + return mockClient.Client() + }, + expectedObjects: []client.Object{ + centralTLSSecretObject(), + centralDBPasswordSecretObject(), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fakeClient, _, r := getClientTrackerAndReconciler(t, simpleManagedCentral, nil, defaultReconcilerOptions, tc.mockObjects...) + managedCentral := tc.buildCentral() + + if tc.buildFMClient != nil { + r.fleetmanagerClient = tc.buildFMClient() + } + + err := r.restoreCentralSecrets(context.Background(), managedCentral) + + if err != nil && tc.expectedErrorMsgContains != "" { + require.Contains(t, err.Error(), tc.expectedErrorMsgContains) + } else { + require.NoError(t, err) + } + + for _, obj := range tc.expectedObjects { + s := v1.Secret{} + err := fakeClient.Get(context.Background(), client.ObjectKey{Namespace: obj.GetNamespace(), Name: obj.GetName()}, &s) + require.NoErrorf(t, err, "finding expected object %s/%s", obj.GetNamespace(), obj.GetName()) + } + + }) + } +} diff --git a/fleetshard/pkg/runtime/runtime.go b/fleetshard/pkg/runtime/runtime.go index 42875c8eb6..f82725c85e 100644 --- a/fleetshard/pkg/runtime/runtime.go +++ b/fleetshard/pkg/runtime/runtime.go @@ -155,7 +155,7 @@ func (r *Runtime) Start() error { logger.InfoChangedInt32(&reconciledCentralCountCache, "Received central count changed: received %d centrals", reconciledCentralCountCache) for _, central := range list.Items { if _, ok := r.reconcilers[central.Id]; !ok { - r.reconcilers[central.Id] = centralReconciler.NewCentralReconciler(r.k8sClient, central, + r.reconcilers[central.Id] = centralReconciler.NewCentralReconciler(r.k8sClient, r.client, central, r.dbProvisionClient, postgres.InitializeDatabase, r.secretCipher, reconcilerOpts) } diff --git a/internal/dinosaur/pkg/api/private/api/openapi.yaml b/internal/dinosaur/pkg/api/private/api/openapi.yaml index bfb0ddeae2..bae6e67ddc 100644 --- a/internal/dinosaur/pkg/api/private/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/private/api/openapi.yaml @@ -142,6 +142,47 @@ paths: summary: Get the list of ManagedaCentrals for the specified agent cluster tags: - Agent Clusters + /api/rhacs/v1/agent-clusters/centrals/{id}: + get: + operationId: getCentral + parameters: + - description: The ID of record + in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ManagedCentral' + description: The ManagedCentrals with centralId for the specified agent + cluster + "400": + content: + application/json: + examples: + "400InvalidIdExample": + $ref: '#/components/examples/400InvalidIdExample' + schema: + $ref: '#/components/schemas/Error' + description: id value is not valid + "404": + content: + application/json: + examples: + "404Example": + $ref: '#/components/examples/404Example' + schema: + $ref: '#/components/schemas/Error' + description: Auth token is not valid. + security: + - Bearer: [] + summary: Get the ManagedaCentral for the specified agent cluster and centralId + tags: + - Agent Clusters /api/rhacs/v1/agent-clusters/{id}: get: operationId: getDataPlaneClusterAgentConfig @@ -421,6 +462,10 @@ components: items: type: string type: array + secrets: + additionalProperties: + type: string + type: object ManagedCentral_allOf_spec_auth: properties: clientSecret: diff --git a/internal/dinosaur/pkg/api/private/api_agent_clusters.go b/internal/dinosaur/pkg/api/private/api_agent_clusters.go index 1682c0b64d..bc937d1294 100644 --- a/internal/dinosaur/pkg/api/private/api_agent_clusters.go +++ b/internal/dinosaur/pkg/api/private/api_agent_clusters.go @@ -26,6 +26,103 @@ var ( // AgentClustersApiService AgentClustersApi service type AgentClustersApiService service +/* +GetCentral Get the ManagedaCentral for the specified agent cluster and centralId + - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + - @param id The ID of record + +@return ManagedCentral +*/ +func (a *AgentClustersApiService) GetCentral(ctx _context.Context, id string) (ManagedCentral, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue ManagedCentral + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/api/rhacs/v1/agent-clusters/centrals/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", _neturl.QueryEscape(parameterToString(id, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(r) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + /* GetCentrals Get the list of ManagedaCentrals for the specified agent cluster - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). diff --git a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go index f90ab28f7a..d50ae48c5e 100644 --- a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go +++ b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go @@ -18,4 +18,5 @@ type ManagedCentralAllOfMetadata struct { Annotations ManagedCentralAllOfMetadataAnnotations `json:"annotations,omitempty"` DeletionTimestamp string `json:"deletionTimestamp,omitempty"` SecretsStored []string `json:"secretsStored,omitempty"` + Secrets map[string]string `json:"secrets,omitempty"` } diff --git a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go index 26d73276c1..898425ab15 100644 --- a/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go +++ b/internal/dinosaur/pkg/handlers/data_plane_dinosaur.go @@ -81,3 +81,22 @@ func (h *dataPlaneDinosaurHandler) GetAll(w http.ResponseWriter, r *http.Request handlers.HandleGet(w, r, cfg) } + +// GetByID... +func (h *dataPlaneDinosaurHandler) GetByID(w http.ResponseWriter, r *http.Request) { + centralID := mux.Vars(r)["id"] + cfg := &handlers.HandlerConfig{ + Action: func() (interface{}, *errors.ServiceError) { + centralRequest, err := h.dinosaurService.GetByID(centralID) + if err != nil { + return nil, err + } + + converted := h.presenter.PresentManagedCentralWithSecrets(centralRequest) + + return converted, nil + }, + } + + handlers.HandleGet(w, r, cfg) +} diff --git a/internal/dinosaur/pkg/presenters/managedcentral.go b/internal/dinosaur/pkg/presenters/managedcentral.go index 52892066fc..0f065017c9 100644 --- a/internal/dinosaur/pkg/presenters/managedcentral.go +++ b/internal/dinosaur/pkg/presenters/managedcentral.go @@ -2,6 +2,7 @@ package presenters import ( "encoding/json" + "fmt" "time" "github.com/golang/glog" @@ -144,6 +145,25 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque return res } +// PresentManagedCentralWithSecrets return a private.ManagedCentral including secret data +func (c *ManagedCentralPresenter) PresentManagedCentralWithSecrets(from *dbapi.CentralRequest) private.ManagedCentral { + managedCentral := c.PresentManagedCentral(from) + secretInterfaceMap, err := from.Secrets.Object() + secretStringMap := make(map[string]string, len(secretInterfaceMap)) + + if err != nil { + glog.Errorf("Failed to get Secrets for central request as map %q/%s: %v", from.Name, from.ID, err) + return managedCentral + } + + for k, v := range secretInterfaceMap { + secretStringMap[k] = fmt.Sprintf("%v", v) + } + + managedCentral.Metadata.Secrets = secretStringMap // pragma: allowlist secret + return managedCentral +} + func orDefaultQty(qty resource.Quantity, def resource.Quantity) *resource.Quantity { if qty != (resource.Quantity{}) { return &qty diff --git a/internal/dinosaur/pkg/routes/route_loader.go b/internal/dinosaur/pkg/routes/route_loader.go index 6b69343cf5..aa2ae28b24 100644 --- a/internal/dinosaur/pkg/routes/route_loader.go +++ b/internal/dinosaur/pkg/routes/route_loader.go @@ -219,6 +219,13 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op apiV1DataPlaneRequestsRouter.HandleFunc("/{id}/centrals", dataPlaneCentralHandler.GetAll). Name(logger.NewLogEvent("list-dataplane-centrals", "list all dataplane centrals").ToString()). Methods(http.MethodGet) + + // /agent-clusters/ + // used for lazy loading additional data not added to the list requests e.g secrets + apiV1DataPlaneRequestsRouter.HandleFunc("/centrals/{id}", dataPlaneCentralHandler.GetByID). + Name(logger.NewLogEvent("get-dataplane-central-by-id", "get a single dataplane central").ToString()). + Methods(http.MethodGet) + // deliberately returns 404 here if the request doesn't have the required role, so that it will appear as if the endpoint doesn't exist auth.UseFleetShardAuthorizationMiddleware(apiV1DataPlaneRequestsRouter, s.IAMConfig.RedhatSSORealm.ValidIssuerURI, s.FleetShardAuthZConfig) diff --git a/openapi/fleet-manager-private.yaml b/openapi/fleet-manager-private.yaml index 19795a685b..cedd4fc701 100644 --- a/openapi/fleet-manager-private.yaml +++ b/openapi/fleet-manager-private.yaml @@ -139,6 +139,43 @@ paths: operationId: getCentrals summary: Get the list of ManagedaCentrals for the specified agent cluster + "/api/rhacs/v1/agent-clusters/centrals/{id}": + get: + tags: + - Agent Clusters + parameters: + - $ref: "fleet-manager.yaml#/components/parameters/id" + responses: + "200": + description: The ManagedCentrals with centralId for the specified agent cluster + content: + application/json: + schema: + $ref: "#/components/schemas/ManagedCentral" + "400": + content: + application/json: + schema: + $ref: "fleet-manager.yaml#/components/schemas/Error" + examples: + 400InvalidIdExample: + $ref: "#/components/examples/400InvalidIdExample" + description: id value is not valid + "404": + content: + application/json: + schema: + $ref: "fleet-manager.yaml#/components/schemas/Error" + examples: + 404Example: + $ref: "fleet-manager.yaml#/components/examples/404Example" + # This is deliberate to hide the endpoints for unauthorised users + description: Auth token is not valid. + security: + - Bearer: [] + operationId: getCentral + summary: Get the ManagedaCentral for the specified agent cluster and centralId + "/api/rhacs/v1/agent-clusters/{id}": get: tags: @@ -234,10 +271,17 @@ components: type: string deletionTimestamp: type: string + # Using lazy loading for secrets, secretsStored should always contain + # the names of all stored secrets. secrets should only contain secret data + # when the ManagedCentral is queried with the TODO: path endpoint secretsStored: type: array items: type: string + secrets: + type: object + additionalProperties: + type: string spec: type: object properties: diff --git a/pkg/client/fleetmanager/api_moq.go b/pkg/client/fleetmanager/api_moq.go index 875314748b..4ea2600e9a 100644 --- a/pkg/client/fleetmanager/api_moq.go +++ b/pkg/client/fleetmanager/api_moq.go @@ -256,6 +256,9 @@ var _ PrivateAPI = &PrivateAPIMock{} // // // make and configure a mocked PrivateAPI // mockedPrivateAPI := &PrivateAPIMock{ +// GetCentralFunc: func(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) { +// panic("mock out the GetCentral method") +// }, // GetCentralsFunc: func(ctx context.Context, id string) (private.ManagedCentralList, *http.Response, error) { // panic("mock out the GetCentrals method") // }, @@ -272,6 +275,9 @@ var _ PrivateAPI = &PrivateAPIMock{} // // } type PrivateAPIMock struct { + // GetCentralFunc mocks the GetCentral method. + GetCentralFunc func(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) + // GetCentralsFunc mocks the GetCentrals method. GetCentralsFunc func(ctx context.Context, id string) (private.ManagedCentralList, *http.Response, error) @@ -283,6 +289,13 @@ type PrivateAPIMock struct { // calls tracks calls to the methods. calls struct { + // GetCentral holds details about calls to the GetCentral method. + GetCentral []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // CentralID is the centralID argument value. + CentralID string + } // GetCentrals holds details about calls to the GetCentrals method. GetCentrals []struct { // Ctx is the ctx argument value. @@ -307,11 +320,48 @@ type PrivateAPIMock struct { RequestBody map[string]private.DataPlaneCentralStatus } } + lockGetCentral sync.RWMutex lockGetCentrals sync.RWMutex lockGetDataPlaneClusterAgentConfig sync.RWMutex lockUpdateCentralClusterStatus sync.RWMutex } +// GetCentral calls GetCentralFunc. +func (mock *PrivateAPIMock) GetCentral(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) { + if mock.GetCentralFunc == nil { + panic("PrivateAPIMock.GetCentralFunc: method is nil but PrivateAPI.GetCentral was just called") + } + callInfo := struct { + Ctx context.Context + CentralID string + }{ + Ctx: ctx, + CentralID: centralID, + } + mock.lockGetCentral.Lock() + mock.calls.GetCentral = append(mock.calls.GetCentral, callInfo) + mock.lockGetCentral.Unlock() + return mock.GetCentralFunc(ctx, centralID) +} + +// GetCentralCalls gets all the calls that were made to GetCentral. +// Check the length with: +// +// len(mockedPrivateAPI.GetCentralCalls()) +func (mock *PrivateAPIMock) GetCentralCalls() []struct { + Ctx context.Context + CentralID string +} { + var calls []struct { + Ctx context.Context + CentralID string + } + mock.lockGetCentral.RLock() + calls = mock.calls.GetCentral + mock.lockGetCentral.RUnlock() + return calls +} + // GetCentrals calls GetCentralsFunc. func (mock *PrivateAPIMock) GetCentrals(ctx context.Context, id string) (private.ManagedCentralList, *http.Response, error) { if mock.GetCentralsFunc == nil { diff --git a/pkg/client/fleetmanager/client.go b/pkg/client/fleetmanager/client.go index 5fea323532..b7bdbded10 100644 --- a/pkg/client/fleetmanager/client.go +++ b/pkg/client/fleetmanager/client.go @@ -24,6 +24,7 @@ type PublicAPI interface { // PrivateAPI is a wrapper interface for the fleetmanager client private API. type PrivateAPI interface { GetDataPlaneClusterAgentConfig(ctx context.Context, id string) (private.DataplaneClusterAgentConfig, *http.Response, error) + GetCentral(ctx context.Context, centralID string) (private.ManagedCentral, *http.Response, error) GetCentrals(ctx context.Context, id string) (private.ManagedCentralList, *http.Response, error) UpdateCentralClusterStatus(ctx context.Context, id string, requestBody map[string]private.DataPlaneCentralStatus) (*http.Response, error) }