diff --git a/.secrets.baseline b/.secrets.baseline index 8a85d8730..a1421d0a4 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -268,7 +268,7 @@ "filename": "internal/central/pkg/services/centralservice_moq.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 1112 + "line_number": 1180 } ], "pkg/client/fleetmanager/impl/testdata/token": [ @@ -391,5 +391,5 @@ } ] }, - "generated_at": "2025-05-12T10:17:39Z" + "generated_at": "2025-06-10T08:35:01Z" } diff --git a/internal/central/pkg/api/admin/private/api/openapi.yaml b/internal/central/pkg/api/admin/private/api/openapi.yaml index f862d7c62..11b2d59c4 100644 --- a/internal/central/pkg/api/admin/private/api/openapi.yaml +++ b/internal/central/pkg/api/admin/private/api/openapi.yaml @@ -566,6 +566,52 @@ paths: $ref: '#/components/schemas/Error' description: Unexpected error occurred summary: Change central billing parameters + /api/rhacs/v1/admin/centrals/{id}/subscription: + patch: + operationId: changeSubscriptionParameters + parameters: + - description: The ID of record + in: path + name: id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CentralSubscriptionChangeRequest' + description: Change subscription parameters + required: true + responses: + "200": + description: Subscription parameters changed + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: User is not authorised to access the service + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: No Central found with the specified ID or dynamic clients are + not configured + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: Unexpected error occurred + summary: Change central subscription and cloud account parameters /api/rhacs/v1/admin/centrals/db/{id}: delete: operationId: deleteDbCentralById @@ -905,6 +951,19 @@ components: - RHACS type: string type: object + CentralSubscriptionChangeRequest: + example: + cloud_account_id: cloud_account_id + subscription_id: subscription_id + cloud_provider: cloud_provider + properties: + cloud_account_id: + type: string + cloud_provider: + type: string + subscription_id: + type: string + type: object CentralAssignClusterRequest: example: cluster_id: cluster_id diff --git a/internal/central/pkg/api/admin/private/api_default.go b/internal/central/pkg/api/admin/private/api_default.go index e899ca2e7..3c73cd688 100644 --- a/internal/central/pkg/api/admin/private/api_default.go +++ b/internal/central/pkg/api/admin/private/api_default.go @@ -352,6 +352,114 @@ func (a *DefaultApiService) ChangeBillingParameters(ctx _context.Context, id str return localVarHTTPResponse, nil } +/* +ChangeSubscriptionParameters Change central subscription and cloud account parameters + - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + - @param id The ID of record + - @param centralSubscriptionChangeRequest Change subscription parameters +*/ +func (a *DefaultApiService) ChangeSubscriptionParameters(ctx _context.Context, id string, centralSubscriptionChangeRequest CentralSubscriptionChangeRequest) (*_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/api/rhacs/v1/admin/centrals/{id}/subscription" + 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{"application/json"} + + // 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 + } + // body params + localVarPostBody = ¢ralSubscriptionChangeRequest + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(r) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return 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 localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + /* CreateCentral Creates a Central request Creates a new Central that is owned by the user and organisation authenticated for the request. Each Central has a single owner organisation and a single owner user. This API allows providing custom resource settings for the new Central instance. diff --git a/internal/central/pkg/api/admin/private/model_central_subscription_change_request.go b/internal/central/pkg/api/admin/private/model_central_subscription_change_request.go new file mode 100644 index 000000000..dbd6a37c0 --- /dev/null +++ b/internal/central/pkg/api/admin/private/model_central_subscription_change_request.go @@ -0,0 +1,18 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager Admin API + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager Admin APIs that can be used by RHACS Managed Service Operations Team. + * + * API version: 0.0.3 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +package private + +// CentralSubscriptionChangeRequest struct for CentralSubscriptionChangeRequest +type CentralSubscriptionChangeRequest struct { + CloudAccountId string `json:"cloud_account_id,omitempty"` + CloudProvider string `json:"cloud_provider,omitempty"` + SubscriptionId string `json:"subscription_id,omitempty"` +} diff --git a/internal/central/pkg/handlers/admin_central.go b/internal/central/pkg/handlers/admin_central.go index 709c75139..86218326e 100644 --- a/internal/central/pkg/handlers/admin_central.go +++ b/internal/central/pkg/handlers/admin_central.go @@ -68,6 +68,9 @@ type AdminCentralHandler interface { // PatchBillingParameters changes the billing model of a central PatchBillingParameters(w http.ResponseWriter, r *http.Request) + + // PatchSubscriptionParameters changes the subscription and cloud account of a central + PatchSubscriptionParameters(w http.ResponseWriter, r *http.Request) } type adminCentralHandler struct { @@ -424,3 +427,15 @@ func (h adminCentralHandler) PatchBillingParameters(w http.ResponseWriter, r *ht } handlers.Handle(w, r, cfg, http.StatusOK) } + +func (h adminCentralHandler) PatchSubscriptionParameters(w http.ResponseWriter, r *http.Request) { + var request *private.CentralSubscriptionChangeRequest + cfg := &handlers.HandlerConfig{ + MarshalInto: &request, + Action: func() (i any, serviceError *errors.ServiceError) { + return nil, h.service.ChangeSubscription(r.Context(), mux.Vars(r)["id"], + request.CloudAccountId, request.CloudProvider, request.SubscriptionId) + }, + } + handlers.Handle(w, r, cfg, http.StatusOK) +} diff --git a/internal/central/pkg/routes/route_loader.go b/internal/central/pkg/routes/route_loader.go index 5e5934e9e..ce385faff 100644 --- a/internal/central/pkg/routes/route_loader.go +++ b/internal/central/pkg/routes/route_loader.go @@ -233,6 +233,9 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string) er adminCentralsRouter.HandleFunc("/{id}/billing", adminCentralHandler.PatchBillingParameters). Name(logger.NewLogEvent("admin-billing", "[admin] change central billing parameters").ToString()). Methods(http.MethodPatch) + adminCentralsRouter.HandleFunc("/{id}/subscription", adminCentralHandler.PatchSubscriptionParameters). + Name(logger.NewLogEvent("admin-subscription", "[admin] change central subscription parameters").ToString()). + Methods(http.MethodPatch) if features.ClusterMigration.Enabled() { adminCentralsRouter.HandleFunc("/{id}/assign-cluster", adminCentralHandler.AssignCluster). diff --git a/internal/central/pkg/services/central.go b/internal/central/pkg/services/central.go index 4b2aa0a42..197048cd8 100644 --- a/internal/central/pkg/services/central.go +++ b/internal/central/pkg/services/central.go @@ -118,6 +118,7 @@ type CentralService interface { ResetCentralSecretBackup(ctx context.Context, centralRequest *dbapi.CentralRequest) *errors.ServiceError ChangeBillingParameters(ctx context.Context, centralID string, billingModel string, cloudAccountID string, cloudProvider string, product string) *errors.ServiceError AssignCluster(ctx context.Context, centralID string, clusterID string) *errors.ServiceError + ChangeSubscription(ctx context.Context, centralID string, cloudAccountID string, cloudProvider string, subscriptionID string) *errors.ServiceError } var _ CentralService = ¢ralService{} @@ -1143,3 +1144,23 @@ func (k *centralService) ChangeBillingParameters(ctx context.Context, centralID } return nil } + +// ChangeSubscription implements CentralService. +func (k *centralService) ChangeSubscription(ctx context.Context, centralID string, cloudAccountID string, cloudProvider string, subscriptionID string) *errors.ServiceError { + centralRequest, svcErr := k.GetByID(centralID) + if svcErr != nil { + return svcErr + } + + centralRequest.CloudProvider = cloudProvider + centralRequest.CloudAccountID = cloudAccountID + centralRequest.SubscriptionID = subscriptionID + + if svcErr = k.UpdateIgnoreNils(centralRequest); svcErr != nil { + glog.Errorf("Failed to update central %q record with subscription_id %q and updated cloud account %q: %v", centralID, subscriptionID, cloudAccountID, svcErr) + return svcErr + } + + glog.Infof("Central %q cloud account parameters have been changed to %q with id %q", centralID, cloudProvider, cloudAccountID) + return nil +} diff --git a/internal/central/pkg/services/central_test.go b/internal/central/pkg/services/central_test.go index 729ecf7e0..a484c5f81 100644 --- a/internal/central/pkg/services/central_test.go +++ b/internal/central/pkg/services/central_test.go @@ -302,3 +302,33 @@ func Test_centralService_ChangeBillingParameters(t *testing.T) { deleteQuotaCalls := quotaService.DeleteQuotaCalls() require.Len(t, deleteQuotaCalls, 0) } + +func Test_centralService_ChangeSubscription(t *testing.T) { + service := ¢ralService{ + connectionFactory: db.NewMockConnectionFactory(nil), + } + central := buildCentralRequest(func(centralRequest *dbapi.CentralRequest) { + centralRequest.CloudProvider = "" + centralRequest.CloudAccountID = "" + centralRequest.SubscriptionID = "original_subscription_id" + }) + + catcher := mocket.Catcher.Reset() + q0 := catcher.NewMock().WithQuery(`SELECT * FROM "central_requests" `+ + `WHERE id = $1 AND "central_requests"."deleted_at" IS NULL `+ + `ORDER BY "central_requests"."id" LIMIT $2`). + OneTime().WithArgs(testID, int64(1)). + WithReply(converters.ConvertCentralRequest(central)) + q1 := catcher.NewMock().WithQuery(`UPDATE "central_requests" ` + + `SET "updated_at"=$1,"deleted_at"=$2,"region"=$3,"cluster_id"=$4,` + + `"cloud_provider"=$5,"cloud_account_id"=$6,"name"=$7,"subscription_id"=$8,"owner"=$9 ` + + `WHERE status not IN ($10,$11) AND "central_requests"."deleted_at" IS NULL AND "id" = $12`). + OneTime() + + svcErr := service.ChangeSubscription(context.Background(), central.ID, "aws_account_id", "aws", "new_subscription_id") + assert.Nil(t, svcErr) + + assert.True(t, q0.Triggered) + assert.True(t, q1.Triggered) + +} diff --git a/internal/central/pkg/services/centralservice_moq.go b/internal/central/pkg/services/centralservice_moq.go index 351f3126e..4df3881d2 100644 --- a/internal/central/pkg/services/centralservice_moq.go +++ b/internal/central/pkg/services/centralservice_moq.go @@ -37,6 +37,9 @@ var _ CentralService = &CentralServiceMock{} // ChangeCentralCNAMErecordsFunc: func(centralRequest *dbapi.CentralRequest, action CentralRoutesAction) (*route53.ChangeResourceRecordSetsOutput, *serviceError.ServiceError) { // panic("mock out the ChangeCentralCNAMErecords method") // }, +// ChangeSubscriptionFunc: func(ctx context.Context, centralID string, cloudAccountID string, cloudProvider string, subscriptionID string) *serviceError.ServiceError { +// panic("mock out the ChangeSubscription method") +// }, // CountByRegionAndInstanceTypeFunc: func() ([]CentralRegionCount, error) { // panic("mock out the CountByRegionAndInstanceType method") // }, @@ -128,6 +131,9 @@ type CentralServiceMock struct { // ChangeCentralCNAMErecordsFunc mocks the ChangeCentralCNAMErecords method. ChangeCentralCNAMErecordsFunc func(centralRequest *dbapi.CentralRequest, action CentralRoutesAction) (*route53.ChangeResourceRecordSetsOutput, *serviceError.ServiceError) + // ChangeSubscriptionFunc mocks the ChangeSubscription method. + ChangeSubscriptionFunc func(ctx context.Context, centralID string, cloudAccountID string, cloudProvider string, subscriptionID string) *serviceError.ServiceError + // CountByRegionAndInstanceTypeFunc mocks the CountByRegionAndInstanceType method. CountByRegionAndInstanceTypeFunc func() ([]CentralRegionCount, error) @@ -238,6 +244,19 @@ type CentralServiceMock struct { // Action is the action argument value. Action CentralRoutesAction } + // ChangeSubscription holds details about calls to the ChangeSubscription method. + ChangeSubscription []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // CentralID is the centralID argument value. + CentralID string + // CloudAccountID is the cloudAccountID argument value. + CloudAccountID string + // CloudProvider is the cloudProvider argument value. + CloudProvider string + // SubscriptionID is the subscriptionID argument value. + SubscriptionID string + } // CountByRegionAndInstanceType holds details about calls to the CountByRegionAndInstanceType method. CountByRegionAndInstanceType []struct { } @@ -377,6 +396,7 @@ type CentralServiceMock struct { lockAssignCluster sync.RWMutex lockChangeBillingParameters sync.RWMutex lockChangeCentralCNAMErecords sync.RWMutex + lockChangeSubscription sync.RWMutex lockCountByRegionAndInstanceType sync.RWMutex lockCountByStatus sync.RWMutex lockDelete sync.RWMutex @@ -563,6 +583,54 @@ func (mock *CentralServiceMock) ChangeCentralCNAMErecordsCalls() []struct { return calls } +// ChangeSubscription calls ChangeSubscriptionFunc. +func (mock *CentralServiceMock) ChangeSubscription(ctx context.Context, centralID string, cloudAccountID string, cloudProvider string, subscriptionID string) *serviceError.ServiceError { + if mock.ChangeSubscriptionFunc == nil { + panic("CentralServiceMock.ChangeSubscriptionFunc: method is nil but CentralService.ChangeSubscription was just called") + } + callInfo := struct { + Ctx context.Context + CentralID string + CloudAccountID string + CloudProvider string + SubscriptionID string + }{ + Ctx: ctx, + CentralID: centralID, + CloudAccountID: cloudAccountID, + CloudProvider: cloudProvider, + SubscriptionID: subscriptionID, + } + mock.lockChangeSubscription.Lock() + mock.calls.ChangeSubscription = append(mock.calls.ChangeSubscription, callInfo) + mock.lockChangeSubscription.Unlock() + return mock.ChangeSubscriptionFunc(ctx, centralID, cloudAccountID, cloudProvider, subscriptionID) +} + +// ChangeSubscriptionCalls gets all the calls that were made to ChangeSubscription. +// Check the length with: +// +// len(mockedCentralService.ChangeSubscriptionCalls()) +func (mock *CentralServiceMock) ChangeSubscriptionCalls() []struct { + Ctx context.Context + CentralID string + CloudAccountID string + CloudProvider string + SubscriptionID string +} { + var calls []struct { + Ctx context.Context + CentralID string + CloudAccountID string + CloudProvider string + SubscriptionID string + } + mock.lockChangeSubscription.RLock() + calls = mock.calls.ChangeSubscription + mock.lockChangeSubscription.RUnlock() + return calls +} + // CountByRegionAndInstanceType calls CountByRegionAndInstanceTypeFunc. func (mock *CentralServiceMock) CountByRegionAndInstanceType() ([]CentralRegionCount, error) { if mock.CountByRegionAndInstanceTypeFunc == nil { diff --git a/openapi/fleet-manager-private-admin.yaml b/openapi/fleet-manager-private-admin.yaml index 27f09ff37..6cb51b5e1 100644 --- a/openapi/fleet-manager-private-admin.yaml +++ b/openapi/fleet-manager-private-admin.yaml @@ -423,6 +423,46 @@ paths: application/json: schema: $ref: 'fleet-manager.yaml#/components/schemas/Error' + '/api/rhacs/v1/admin/centrals/{id}/subscription': + patch: + summary: Change central subscription and cloud account parameters + operationId: changeSubscriptionParameters + parameters: + - $ref: "fleet-manager.yaml#/components/parameters/id" + requestBody: + description: Change subscription parameters + content: + application/json: + schema: + $ref: "#/components/schemas/CentralSubscriptionChangeRequest" + required: true + responses: + "200": + description: Subscription parameters changed + "401": + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'fleet-manager.yaml#/components/schemas/Error' + "403": + description: User is not authorised to access the service + content: + application/json: + schema: + $ref: 'fleet-manager.yaml#/components/schemas/Error' + "404": + description: No Central found with the specified ID or dynamic clients are not configured + content: + application/json: + schema: + $ref: 'fleet-manager.yaml#/components/schemas/Error' + "500": + description: Unexpected error occurred + content: + application/json: + schema: + $ref: 'fleet-manager.yaml#/components/schemas/Error' '/api/rhacs/v1/admin/centrals/db/{id}': delete: summary: Delete a Central directly in the Database by ID @@ -762,6 +802,16 @@ components: enum: - RHACS + CentralSubscriptionChangeRequest: + type: object + properties: + cloud_account_id: + type: string + cloud_provider: + type: string + subscription_id: + type: string + CentralAssignClusterRequest: type: object properties: