diff --git a/pkg/armrpc/frontend/controller/controller.go b/pkg/armrpc/frontend/controller/controller.go index 983cecba13..4e89510120 100644 --- a/pkg/armrpc/frontend/controller/controller.go +++ b/pkg/armrpc/frontend/controller/controller.go @@ -8,6 +8,7 @@ package controller import ( "context" "net/http" + "time" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" sm "github.com/project-radius/radius/pkg/armrpc/asyncoperation/statusmanager" @@ -54,6 +55,9 @@ type ResourceOptions[T any] struct { // UpdateFilters is a slice of filters that execute prior to updating a resource. UpdateFilters []UpdateFilter[T] + + // AsyncOperationTimeout is the default timeout duration of async put operation. + AsyncOperationTimeout time.Duration } // TODO: Remove Controller when all controller uses Operation diff --git a/pkg/armrpc/frontend/controller/operation.go b/pkg/armrpc/frontend/controller/operation.go index 874b174b22..be772f7591 100644 --- a/pkg/armrpc/frontend/controller/operation.go +++ b/pkg/armrpc/frontend/controller/operation.go @@ -20,6 +20,11 @@ import ( "github.com/project-radius/radius/pkg/ucp/store" ) +const ( + // defaultAsyncPutTimeout is the default timeout duration of async put operation. + defaultAsyncPutTimeout = time.Duration(2) * time.Minute +) + // Operation is the base operation controller. type Operation[P interface { *T @@ -218,3 +223,11 @@ func (b *Operation[P, T]) DeleteFilters() []DeleteFilter[T] { func (b *Operation[P, T]) UpdateFilters() []UpdateFilter[T] { return b.resourceOptions.UpdateFilters } + +// AsyncOperationTimeout returns the timeput for the operation. +func (b *Operation[P, T]) AsyncOperationTimeout() time.Duration { + if b.resourceOptions.AsyncOperationTimeout == 0 { + return defaultAsyncPutTimeout + } + return b.resourceOptions.AsyncOperationTimeout +} diff --git a/pkg/armrpc/frontend/defaultoperation/defaultasyncdelete.go b/pkg/armrpc/frontend/defaultoperation/defaultasyncdelete.go index c2e4e88a11..9bc325d920 100644 --- a/pkg/armrpc/frontend/defaultoperation/defaultasyncdelete.go +++ b/pkg/armrpc/frontend/defaultoperation/defaultasyncdelete.go @@ -8,18 +8,12 @@ package defaultoperation import ( "context" "net/http" - "time" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" "github.com/project-radius/radius/pkg/armrpc/rest" ) -var ( - // defaultAsyncDeleteTimeout is the default timeout duration of async delete operation. - defaultAsyncDeleteTimeout = time.Duration(120) * time.Second -) - // DefaultAsyncDelete is the controller implementation to delete async resource. type DefaultAsyncDelete[P interface { *T @@ -58,7 +52,7 @@ func (e *DefaultAsyncDelete[P, T]) Run(ctx context.Context, w http.ResponseWrite } } - if r, err := e.PrepareAsyncOperation(ctx, old, v1.ProvisioningStateAccepted, defaultAsyncDeleteTimeout, &etag); r != nil || err != nil { + if r, err := e.PrepareAsyncOperation(ctx, old, v1.ProvisioningStateAccepted, e.AsyncOperationTimeout(), &etag); r != nil || err != nil { return r, err } diff --git a/pkg/armrpc/frontend/defaultoperation/defaultasyncput.go b/pkg/armrpc/frontend/defaultoperation/defaultasyncput.go index b76c01477e..1c9da6ee9e 100644 --- a/pkg/armrpc/frontend/defaultoperation/defaultasyncput.go +++ b/pkg/armrpc/frontend/defaultoperation/defaultasyncput.go @@ -8,18 +8,12 @@ package defaultoperation import ( "context" "net/http" - "time" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" "github.com/project-radius/radius/pkg/armrpc/rest" ) -var ( - // defaultAsyncPutTimeout is the default timeout duration of async put operation. - defaultAsyncPutTimeout = time.Duration(120) * time.Second -) - // DefaultAsyncPut is the controller implementation to create or update async resource. type DefaultAsyncPut[P interface { *T @@ -59,7 +53,7 @@ func (e *DefaultAsyncPut[P, T]) Run(ctx context.Context, w http.ResponseWriter, } } - if r, err := e.PrepareAsyncOperation(ctx, newResource, v1.ProvisioningStateAccepted, defaultAsyncPutTimeout, &etag); r != nil || err != nil { + if r, err := e.PrepareAsyncOperation(ctx, newResource, v1.ProvisioningStateAccepted, e.AsyncOperationTimeout(), &etag); r != nil || err != nil { return r, err } diff --git a/pkg/corerp/backend/controller/createorupdateresource.go b/pkg/corerp/backend/controller/createorupdateresource.go index ab5a14588d..fe68607623 100644 --- a/pkg/corerp/backend/controller/createorupdateresource.go +++ b/pkg/corerp/backend/controller/createorupdateresource.go @@ -100,8 +100,10 @@ func (c *CreateOrUpdateResource) Run(ctx context.Context, request *ctrl.Request) oldOutputResources := deploymentDataModel.OutputResources() - deploymentDataModel.ApplyDeploymentOutput(deploymentOutput) - + err = deploymentDataModel.ApplyDeploymentOutput(deploymentOutput) + if err != nil { + return ctrl.Result{}, err + } if !isNewResource { diff := rpv1.GetGCOutputResources(deploymentDataModel.OutputResources(), oldOutputResources) err = c.DeploymentProcessor().Delete(ctx, id, diff) diff --git a/pkg/corerp/backend/deployment/deploymentprocessor.go b/pkg/corerp/backend/deployment/deploymentprocessor.go index 735bb573b1..6da5375d67 100644 --- a/pkg/corerp/backend/deployment/deploymentprocessor.go +++ b/pkg/corerp/backend/deployment/deploymentprocessor.go @@ -73,8 +73,8 @@ type ResourceData struct { OutputResources []rpv1.OutputResource ComputedValues map[string]any SecretValues map[string]rpv1.SecretValueReference - AppID *resources.ID // Application ID for which the resource is created - RecipeData link_dm.RecipeData // Relevant only for links created with recipes to find relevant connections created by that recipe + AppID *resources.ID // Application ID for which the resource is created + RecipeData linkrp.RecipeData // Relevant only for links created with recipes to find relevant connections created by that recipe } func (dp *deploymentProcessor) Render(ctx context.Context, resourceID resources.ID, resource v1.DataModelInterface) (renderers.RendererOutput, error) { @@ -496,25 +496,25 @@ func (dp *deploymentProcessor) getResourceDataByID(ctx context.Context, resource if err = resource.As(obj); err != nil { return ResourceData{}, fmt.Errorf(errMsg, resourceID.String(), err) } - return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, link_dm.RecipeData{}) + return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, linkrp.RecipeData{}) case strings.ToLower(gateway.ResourceType): obj := &corerp_dm.Gateway{} if err = resource.As(obj); err != nil { return ResourceData{}, fmt.Errorf(errMsg, resourceID.String(), err) } - return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, link_dm.RecipeData{}) + return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, linkrp.RecipeData{}) case strings.ToLower(volume.ResourceType): obj := &corerp_dm.VolumeResource{} if err = resource.As(obj); err != nil { return ResourceData{}, fmt.Errorf(errMsg, resourceID.String(), err) } - return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, link_dm.RecipeData{}) + return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, linkrp.RecipeData{}) case strings.ToLower(httproute.ResourceType): obj := &corerp_dm.HTTPRoute{} if err = resource.As(obj); err != nil { return ResourceData{}, fmt.Errorf(errMsg, resourceID.String(), err) } - return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, link_dm.RecipeData{}) + return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, linkrp.RecipeData{}) case strings.ToLower(linkrp.MongoDatabasesResourceType): obj := &link_dm.MongoDatabase{} if err = resource.As(obj); err != nil { @@ -544,7 +544,7 @@ func (dp *deploymentProcessor) getResourceDataByID(ctx context.Context, resource if err = resource.As(obj); err != nil { return ResourceData{}, fmt.Errorf(errMsg, resourceID.String(), err) } - return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, link_dm.RecipeData{}) + return dp.buildResourceDependency(resourceID, obj.Properties.Application, obj, obj.Properties.Status.OutputResources, obj.ComputedValues, obj.SecretValues, linkrp.RecipeData{}) case strings.ToLower(linkrp.DaprStateStoresResourceType): obj := &link_dm.DaprStateStore{} if err = resource.As(obj); err != nil { @@ -574,7 +574,7 @@ func (dp *deploymentProcessor) getResourceDataByID(ctx context.Context, resource } } -func (dp *deploymentProcessor) buildResourceDependency(resourceID resources.ID, applicationID string, resource v1.DataModelInterface, outputResources []rpv1.OutputResource, computedValues map[string]any, secretValues map[string]rpv1.SecretValueReference, recipeData link_dm.RecipeData) (ResourceData, error) { +func (dp *deploymentProcessor) buildResourceDependency(resourceID resources.ID, applicationID string, resource v1.DataModelInterface, outputResources []rpv1.OutputResource, computedValues map[string]any, secretValues map[string]rpv1.SecretValueReference, recipeData linkrp.RecipeData) (ResourceData, error) { var appID *resources.ID if applicationID != "" { parsedID, err := resources.ParseResource(applicationID) diff --git a/pkg/corerp/backend/deployment/deploymentprocessor_test.go b/pkg/corerp/backend/deployment/deploymentprocessor_test.go index d72101a3a8..2ff5e4b0b3 100644 --- a/pkg/corerp/backend/deployment/deploymentprocessor_test.go +++ b/pkg/corerp/backend/deployment/deploymentprocessor_test.go @@ -20,6 +20,7 @@ import ( "github.com/project-radius/radius/pkg/corerp/model" "github.com/project-radius/radius/pkg/corerp/renderers" "github.com/project-radius/radius/pkg/corerp/renderers/container" + "github.com/project-radius/radius/pkg/linkrp" linkrp_dm "github.com/project-radius/radius/pkg/linkrp/datamodel" linkrp_renderers "github.com/project-radius/radius/pkg/linkrp/renderers" "github.com/project-radius/radius/pkg/linkrp/renderers/mongodatabases" @@ -182,9 +183,9 @@ func buildMongoDBLinkWithRecipe() linkrp_dm.MongoDatabase { Mode: linkrp_dm.LinkModeRecipe, }, LinkMetadata: linkrp_dm.LinkMetadata{ - RecipeData: linkrp_dm.RecipeData{ - RecipeProperties: linkrp_dm.RecipeProperties{ - LinkRecipe: linkrp_dm.LinkRecipe{ + RecipeData: linkrp.RecipeData{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: "mongoDB", Parameters: map[string]any{ "ResourceGroup": "testRG", diff --git a/pkg/corerp/datamodel/application.go b/pkg/corerp/datamodel/application.go index ae252a20c3..82185a51bd 100644 --- a/pkg/corerp/datamodel/application.go +++ b/pkg/corerp/datamodel/application.go @@ -26,8 +26,9 @@ func (e *Application) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (c *Application) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (c *Application) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { c.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. diff --git a/pkg/corerp/datamodel/container.go b/pkg/corerp/datamodel/container.go index 17ac7d7d85..99094b53c9 100644 --- a/pkg/corerp/datamodel/container.go +++ b/pkg/corerp/datamodel/container.go @@ -27,10 +27,11 @@ func (c ContainerResource) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (c *ContainerResource) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (c *ContainerResource) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { c.Properties.Status.OutputResources = do.DeployedOutputResources c.ComputedValues = do.ComputedValues c.SecretValues = do.SecretValues + return nil } // OutputResources returns the output resources array. diff --git a/pkg/corerp/datamodel/gateway.go b/pkg/corerp/datamodel/gateway.go index ce53c8f9c1..2945840fed 100644 --- a/pkg/corerp/datamodel/gateway.go +++ b/pkg/corerp/datamodel/gateway.go @@ -26,13 +26,14 @@ func (g *Gateway) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (g *Gateway) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (g *Gateway) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { g.Properties.Status.OutputResources = do.DeployedOutputResources g.ComputedValues = do.ComputedValues g.SecretValues = do.SecretValues if url, ok := do.ComputedValues["url"].(string); ok { g.Properties.URL = url } + return nil } // OutputResources returns the output resources array. diff --git a/pkg/corerp/datamodel/httproute.go b/pkg/corerp/datamodel/httproute.go index a7878664ae..197b543c29 100644 --- a/pkg/corerp/datamodel/httproute.go +++ b/pkg/corerp/datamodel/httproute.go @@ -26,7 +26,7 @@ func (h *HTTPRoute) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (h *HTTPRoute) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (h *HTTPRoute) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { if h.Properties != nil { h.Properties.Status.OutputResources = do.DeployedOutputResources } @@ -46,6 +46,7 @@ func (h *HTTPRoute) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { if url, ok := do.ComputedValues["url"].(string); ok { h.Properties.URL = url } + return nil } // OutputResources returns the output resources array. diff --git a/pkg/corerp/datamodel/volume.go b/pkg/corerp/datamodel/volume.go index b65a5dfab7..79f30c442d 100644 --- a/pkg/corerp/datamodel/volume.go +++ b/pkg/corerp/datamodel/volume.go @@ -32,10 +32,11 @@ func (h *VolumeResource) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (h *VolumeResource) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (h *VolumeResource) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { h.Properties.Status.OutputResources = do.DeployedOutputResources h.ComputedValues = do.ComputedValues h.SecretValues = do.SecretValues + return nil } // OutputResources returns the output resources array. diff --git a/pkg/linkrp/api/v20220315privatepreview/datamodel_util.go b/pkg/linkrp/api/v20220315privatepreview/datamodel_util.go index d1200e7210..760f91ade9 100644 --- a/pkg/linkrp/api/v20220315privatepreview/datamodel_util.go +++ b/pkg/linkrp/api/v20220315privatepreview/datamodel_util.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" autorestTo "github.com/Azure/go-autorest/autorest/to" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - "github.com/project-radius/radius/pkg/linkrp/datamodel" + "github.com/project-radius/radius/pkg/linkrp" ) func toProvisioningStateDataModel(state *ProvisioningState) v1.ProvisioningState { @@ -78,8 +78,8 @@ func fromSystemDataModel(s v1.SystemData) *SystemData { } } -func toRecipeDataModel(r *Recipe) datamodel.LinkRecipe { - recipe := datamodel.LinkRecipe{ +func toRecipeDataModel(r *Recipe) linkrp.LinkRecipe { + recipe := linkrp.LinkRecipe{ Name: autorestTo.String(r.Name), } @@ -89,7 +89,7 @@ func toRecipeDataModel(r *Recipe) datamodel.LinkRecipe { return recipe } -func fromRecipeDataModel(r datamodel.LinkRecipe) *Recipe { +func fromRecipeDataModel(r linkrp.LinkRecipe) *Recipe { return &Recipe{ Name: autorestTo.StringPtr(r.Name), Parameters: r.Parameters, diff --git a/pkg/linkrp/backend/controller/createorupdateresource.go b/pkg/linkrp/backend/controller/createorupdateresource.go new file mode 100644 index 0000000000..30c002d190 --- /dev/null +++ b/pkg/linkrp/backend/controller/createorupdateresource.go @@ -0,0 +1,103 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package controller + +import ( + "context" + "errors" + "net/http" + + v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" + ctrl "github.com/project-radius/radius/pkg/armrpc/asyncoperation/controller" + rpv1 "github.com/project-radius/radius/pkg/rp/v1" + "github.com/project-radius/radius/pkg/ucp/resources" + "github.com/project-radius/radius/pkg/ucp/store" +) + +var _ ctrl.Controller = (*CreateOrUpdateResource)(nil) + +// CreateOrUpdateResource is the async operation controller to create or update Applications.Link resources. +type CreateOrUpdateResource struct { + ctrl.BaseController +} + +// NewCreateOrUpdateResource creates the CreateOrUpdateResource controller instance. +func NewCreateOrUpdateResource(opts ctrl.Options) (ctrl.Controller, error) { + return &CreateOrUpdateResource{ctrl.NewBaseAsyncController(opts)}, nil +} + +func (c *CreateOrUpdateResource) Run(ctx context.Context, req *ctrl.Request) (ctrl.Result, error) { + obj, err := c.StorageClient().Get(ctx, req.ResourceID) + if err != nil && !errors.Is(&store.ErrNotFound{}, err) { + return ctrl.Result{}, err + } + + isNewResource := false + if errors.Is(&store.ErrNotFound{}, err) { + isNewResource = true + } + + opType, _ := v1.ParseOperationType(req.OperationType) + if opType.Method == http.MethodPatch && errors.Is(&store.ErrNotFound{}, err) { + return ctrl.Result{}, err + } + + // This code is general and we might be processing an async job for a resource or a scope, so using the general Parse function. + id, err := resources.Parse(req.ResourceID) + if err != nil { + return ctrl.Result{}, err + } + + dataModel, err := getDataModel(id) + if err != nil { + return ctrl.Result{}, err + } + + if err = obj.As(dataModel); err != nil { + return ctrl.Result{}, err + } + + rendererOutput, err := c.LinkDeploymentProcessor().Render(ctx, id, dataModel) + if err != nil { + return ctrl.Result{}, err + } + + deploymentOutput, err := c.LinkDeploymentProcessor().Deploy(ctx, id, rendererOutput) + if err != nil { + return ctrl.Result{}, err + } + + deploymentDataModel, ok := dataModel.(rpv1.DeploymentDataModel) + if !ok { + return ctrl.NewFailedResult(v1.ErrorDetails{Message: "deployment data model conversion error"}), err + } + + oldOutputResources := deploymentDataModel.OutputResources() + err = deploymentDataModel.ApplyDeploymentOutput(deploymentOutput) + if err != nil { + return ctrl.Result{}, err + } + + if !isNewResource { + diff := rpv1.GetGCOutputResources(deploymentDataModel.OutputResources(), oldOutputResources) + err = c.LinkDeploymentProcessor().Delete(ctx, id, diff) + if err != nil { + return ctrl.Result{}, err + } + } + nr := &store.Object{ + Metadata: store.Metadata{ + ID: req.ResourceID, + }, + Data: deploymentDataModel, + } + err = c.StorageClient().Save(ctx, nr, store.WithETag(obj.ETag)) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, err +} diff --git a/pkg/linkrp/backend/controller/createorupdateresource_test.go b/pkg/linkrp/backend/controller/createorupdateresource_test.go new file mode 100644 index 0000000000..81019268ce --- /dev/null +++ b/pkg/linkrp/backend/controller/createorupdateresource_test.go @@ -0,0 +1,382 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// ------------------------------------------------------------ + +package controller + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + ctrl "github.com/project-radius/radius/pkg/armrpc/asyncoperation/controller" + "github.com/project-radius/radius/pkg/linkrp" + "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" + "github.com/project-radius/radius/pkg/linkrp/renderers" + rpv1 "github.com/project-radius/radius/pkg/rp/v1" + "github.com/project-radius/radius/pkg/ucp/resources" + "github.com/project-radius/radius/pkg/ucp/store" +) + +func TestCreateOrUpdateResourceRun_20220315PrivatePreview(t *testing.T) { + setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *deployment.MockDeploymentProcessor) { + mctrl := gomock.NewController(t) + + msc := store.NewMockStorageClient(mctrl) + mdp := deployment.NewMockDeploymentProcessor(mctrl) + + return func(tb testing.TB) { + mctrl.Finish() + }, msc, mdp + } + + putCases := []struct { + desc string + rt string + opType string + rId string + getErr error + convErr bool + renderErr error + deployErr error + saveErr error + expErr error + }{ + { + "mongo-put-success", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo0", + nil, + false, + nil, + nil, + nil, + nil, + }, + { + "mongo-put-not-found", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo1", + &store.ErrNotFound{}, + false, + nil, + nil, + nil, + nil, + }, + { + "mongo-put-get-err", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo2", + errors.New("error getting object"), + false, + nil, + nil, + nil, + errors.New("error getting object"), + }, + { + "redis-put-success", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis0", + nil, + false, + nil, + nil, + nil, + nil, + }, + { + "redis-put-not-found", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis1", + &store.ErrNotFound{}, + false, + nil, + nil, + nil, + nil, + }, + { + "redis-put-get-err", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PUT", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis2", + errors.New("error getting object"), + false, + nil, + nil, + nil, + errors.New("error getting object"), + }, + } + + for _, tt := range putCases { + t.Run(tt.desc, func(t *testing.T) { + teardownTest, msc, mdp := setupTest(t) + defer teardownTest(t) + + req := &ctrl.Request{ + OperationID: uuid.New(), + OperationType: tt.opType, + ResourceID: tt.rId, + CorrelationID: uuid.NewString(), + OperationTimeout: &ctrl.DefaultAsyncOperationTimeout, + } + + // This code is general and we might be processing an async job for a resource or a scope, so using the general Parse function. + parsedID, err := resources.Parse(tt.rId) + require.NoError(t, err) + + getCall := msc.EXPECT(). + Get(gomock.Any(), gomock.Any()). + Return(&store.Object{ + Data: map[string]any{ + "name": "env0", + "properties": map[string]any{ + "provisioningState": "Accepted", + }, + }, + }, tt.getErr). + Times(1) + + if (tt.getErr == nil || errors.Is(&store.ErrNotFound{}, tt.getErr)) && !tt.convErr { + renderCall := mdp.EXPECT(). + Render(gomock.Any(), gomock.Any(), gomock.Any()). + Return(renderers.RendererOutput{}, tt.renderErr). + After(getCall). + Times(1) + + if tt.renderErr == nil { + deployCall := mdp.EXPECT(). + Deploy(gomock.Any(), gomock.Any(), gomock.Any()). + Return(rpv1.DeploymentOutput{}, tt.deployErr). + After(renderCall). + Times(1) + + if !errors.Is(&store.ErrNotFound{}, tt.getErr) { + mdp.EXPECT(). + Delete(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + After(deployCall). + Times(1) + } + + if tt.deployErr == nil { + msc.EXPECT(). + Save(gomock.Any(), gomock.Any(), gomock.Any()). + Return(tt.saveErr). + After(deployCall). + Times(1) + } + } + } + + opts := ctrl.Options{ + StorageClient: msc, + GetLinkDeploymentProcessor: func() deployment.DeploymentProcessor { + return mdp + }, + } + + genCtrl, err := NewCreateOrUpdateResource(opts) + require.NoError(t, err) + + res, err := genCtrl.Run(context.Background(), req) + + if tt.convErr { + tt.expErr = fmt.Errorf("invalid resource type: %q for dependent resource ID: %q", strings.ToLower(tt.rt), parsedID.String()) + } + + if tt.expErr != nil { + require.Error(t, err) + require.Equal(t, tt.expErr, err) + } else { + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + } + }) + } + patchCases := []struct { + desc string + rt string + opType string + rId string + getErr error + convErr bool + renderErr error + deployErr error + saveErr error + expErr error + }{ + { + "mongo-patch-success", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo0", + nil, + false, + nil, + nil, + nil, + nil, + }, + { + "mongo-patch-not-found", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo1", + &store.ErrNotFound{}, + false, + nil, + nil, + nil, + &store.ErrNotFound{}, + }, + { + "mongo-patch-get-err", + linkrp.MongoDatabasesResourceType, + "APPLICATIONS.LINK/MONGODATABASES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/mongoDatabases/mongo2", + errors.New("error getting object"), + false, + nil, + nil, + nil, + errors.New("error getting object"), + }, + { + "redis-patch-success", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis0", + nil, + false, + nil, + nil, + nil, + nil, + }, + { + "redis-patch-not-found", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis1", + &store.ErrNotFound{}, + false, + nil, + nil, + nil, + &store.ErrNotFound{}, + }, + { + "redis-patch-get-err", + linkrp.RedisCachesResourceType, + "APPLICATIONS.LINK/REDISCACHES|PATCH", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Link/redisCaches/redis2", + errors.New("error getting object"), + false, + nil, + nil, + nil, + errors.New("error getting object"), + }, + } + + for _, tt := range patchCases { + t.Run(tt.desc, func(t *testing.T) { + teardownTest, msc, mdp := setupTest(t) + defer teardownTest(t) + + req := &ctrl.Request{ + OperationID: uuid.New(), + OperationType: tt.opType, + ResourceID: tt.rId, + CorrelationID: uuid.NewString(), + OperationTimeout: &ctrl.DefaultAsyncOperationTimeout, + } + + // This code is general and we might be processing an async job for a resource or a scope, so using the general Parse function. + parsedID, err := resources.Parse(tt.rId) + require.NoError(t, err) + + getCall := msc.EXPECT(). + Get(gomock.Any(), gomock.Any()). + Return(&store.Object{ + Data: map[string]any{ + "name": "env0", + "properties": map[string]any{ + "provisioningState": "Accepted", + }, + }, + }, tt.getErr). + Times(1) + + if tt.getErr == nil && !tt.convErr { + renderCall := mdp.EXPECT(). + Render(gomock.Any(), gomock.Any(), gomock.Any()). + Return(renderers.RendererOutput{}, tt.renderErr). + After(getCall). + Times(1) + + if tt.renderErr == nil { + deployCall := mdp.EXPECT(). + Deploy(gomock.Any(), gomock.Any(), gomock.Any()). + Return(rpv1.DeploymentOutput{}, tt.deployErr). + After(renderCall). + Times(1) + + deleteCall := mdp.EXPECT(). + Delete(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + After(deployCall). + Times(1) + + if tt.deployErr == nil { + msc.EXPECT(). + Save(gomock.Any(), gomock.Any(), gomock.Any()). + Return(tt.saveErr). + After(deployCall). + After(deleteCall). + Times(1) + } + } + } + opts := ctrl.Options{ + StorageClient: msc, + GetLinkDeploymentProcessor: func() deployment.DeploymentProcessor { + return mdp + }, + } + + genCtrl, err := NewCreateOrUpdateResource(opts) + require.NoError(t, err) + + res, err := genCtrl.Run(context.Background(), req) + + if tt.convErr { + tt.expErr = fmt.Errorf("invalid resource type: %q for dependent resource ID: %q", strings.ToLower(tt.rt), parsedID.String()) + } + + if tt.expErr != nil { + require.Error(t, err) + require.Equal(t, tt.expErr, err) + } else { + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + } + }) + } +} diff --git a/pkg/linkrp/backend/controller/deleteresource.go b/pkg/linkrp/backend/controller/deleteresource.go index 90aaa1e303..de16c272ff 100644 --- a/pkg/linkrp/backend/controller/deleteresource.go +++ b/pkg/linkrp/backend/controller/deleteresource.go @@ -14,9 +14,8 @@ import ( ctrl "github.com/project-radius/radius/pkg/armrpc/asyncoperation/controller" "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/linkrp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" + rpv1 "github.com/project-radius/radius/pkg/rp/v1" "github.com/project-radius/radius/pkg/ucp/resources" - "github.com/project-radius/radius/pkg/ucp/store" ) var _ ctrl.Controller = (*DeleteResource)(nil) @@ -43,11 +42,21 @@ func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.R return ctrl.Result{}, err } - resourceData, err := getResourceData(id, obj) + dataModel, err := getDataModel(id) if err != nil { return ctrl.Result{}, err } - err = c.LinkDeploymentProcessor().Delete(ctx, resourceData) + + if err = obj.As(dataModel); err != nil { + return ctrl.Result{}, err + } + + deploymentDataModel, ok := dataModel.(rpv1.DeploymentDataModel) + if !ok { + return ctrl.NewFailedResult(v1.ErrorDetails{Message: "deployment data model conversion error"}), nil + } + + err = c.LinkDeploymentProcessor().Delete(ctx, id, deploymentDataModel.OutputResources()) if err != nil { return ctrl.Result{}, err } @@ -60,22 +69,16 @@ func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.R return ctrl.Result{}, err } -func getResourceData(id resources.ID, obj *store.Object) (deployment.ResourceData, error) { +func getDataModel(id resources.ID) (v1.ResourceDataModel, error) { resourceType := strings.ToLower(id.Type()) switch resourceType { case strings.ToLower(linkrp.MongoDatabasesResourceType): - d := &datamodel.MongoDatabase{} - if err := obj.As(d); err != nil { - return deployment.ResourceData{}, err - } - return deployment.ResourceData{ID: id, Resource: d, OutputResources: d.Properties.Status.OutputResources, ComputedValues: d.ComputedValues, SecretValues: d.SecretValues, RecipeData: d.RecipeData}, nil + return &datamodel.MongoDatabase{}, nil + case strings.ToLower(linkrp.RedisCachesResourceType): + return &datamodel.RedisCache{}, nil case strings.ToLower(linkrp.DaprStateStoresResourceType): - d := &datamodel.DaprStateStore{} - if err := obj.As(d); err != nil { - return deployment.ResourceData{}, err - } - return deployment.ResourceData{ID: id, Resource: d, OutputResources: d.Properties.Status.OutputResources, ComputedValues: d.ComputedValues, SecretValues: d.SecretValues, RecipeData: d.RecipeData}, nil + return &datamodel.DaprStateStore{}, nil default: - return deployment.ResourceData{}, fmt.Errorf("async delete operation unsupported on resource type: %q. Resource ID: %q", resourceType, id.String()) + return nil, fmt.Errorf("async delete operation unsupported on resource type: %q. Resource ID: %q", resourceType, id.String()) } } diff --git a/pkg/linkrp/backend/controller/deleteresource_test.go b/pkg/linkrp/backend/controller/deleteresource_test.go index 2120756c45..47a997c945 100644 --- a/pkg/linkrp/backend/controller/deleteresource_test.go +++ b/pkg/linkrp/backend/controller/deleteresource_test.go @@ -65,7 +65,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) { if tt.getErr == nil { mdp.EXPECT(). - Delete(gomock.Any(), gomock.Any()). + Delete(gomock.Any(), gomock.Any(), gomock.Any()). Return(tt.dpDelErr). Times(1) diff --git a/pkg/linkrp/backend/service.go b/pkg/linkrp/backend/service.go index f192de651e..1746e12dbc 100644 --- a/pkg/linkrp/backend/service.go +++ b/pkg/linkrp/backend/service.go @@ -27,6 +27,7 @@ var ( // We use this array to generate generic backend controller for each resource. ResourceTypeNames = []string{ linkrp.MongoDatabasesResourceType, + linkrp.RedisCachesResourceType, linkrp.DaprStateStoresResourceType, } ) @@ -72,6 +73,10 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { panic(err) } + err = s.Controllers.Register(ctx, rt, v1.OperationPut, backend_ctrl.NewCreateOrUpdateResource, opts) + if err != nil { + panic(err) + } } workerOpts := worker.Options{} if s.Options.Config.WorkerServer != nil { diff --git a/pkg/linkrp/datamodel/daprinvokehttproute.go b/pkg/linkrp/datamodel/daprinvokehttproute.go index 479f7a9d5d..0f07f1a176 100644 --- a/pkg/linkrp/datamodel/daprinvokehttproute.go +++ b/pkg/linkrp/datamodel/daprinvokehttproute.go @@ -23,8 +23,9 @@ type DaprInvokeHttpRoute struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *DaprInvokeHttpRoute) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *DaprInvokeHttpRoute) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -44,6 +45,6 @@ func (httpRoute *DaprInvokeHttpRoute) ResourceTypeName() string { // DaprInvokeHttpRouteProperties represents the properties of DaprInvokeHttpRoute resource. type DaprInvokeHttpRouteProperties struct { rpv1.BasicResourceProperties - Recipe LinkRecipe `json:"recipe,omitempty"` - AppId string `json:"appId"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` + AppId string `json:"appId"` } diff --git a/pkg/linkrp/datamodel/daprpubsubbroker.go b/pkg/linkrp/datamodel/daprpubsubbroker.go index becc11f4f7..34375cc9f9 100644 --- a/pkg/linkrp/datamodel/daprpubsubbroker.go +++ b/pkg/linkrp/datamodel/daprpubsubbroker.go @@ -23,8 +23,9 @@ type DaprPubSubBroker struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *DaprPubSubBroker) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *DaprPubSubBroker) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -45,11 +46,11 @@ func (daprPubSub *DaprPubSubBroker) ResourceTypeName() string { type DaprPubSubBrokerProperties struct { rpv1.BasicResourceProperties rpv1.BasicDaprResourceProperties - Topic string `json:"topic,omitempty"` // Topic name of the Azure ServiceBus resource. Provided by the user. - Mode LinkMode `json:"mode"` - Metadata map[string]any `json:"metadata,omitempty"` - Recipe LinkRecipe `json:"recipe"` - Resource string `json:"resource,omitempty"` - Type string `json:"type,omitempty"` - Version string `json:"version,omitempty"` + Topic string `json:"topic,omitempty"` // Topic name of the Azure ServiceBus resource. Provided by the user. + Mode LinkMode `json:"mode"` + Metadata map[string]any `json:"metadata,omitempty"` + Recipe linkrp.LinkRecipe `json:"recipe"` + Resource string `json:"resource,omitempty"` + Type string `json:"type,omitempty"` + Version string `json:"version,omitempty"` } diff --git a/pkg/linkrp/datamodel/daprsecretstore.go b/pkg/linkrp/datamodel/daprsecretstore.go index 45baa41311..3f3c92c45d 100644 --- a/pkg/linkrp/datamodel/daprsecretstore.go +++ b/pkg/linkrp/datamodel/daprsecretstore.go @@ -23,8 +23,9 @@ type DaprSecretStore struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *DaprSecretStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *DaprSecretStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -45,9 +46,9 @@ func (daprSecretStore *DaprSecretStore) ResourceTypeName() string { type DaprSecretStoreProperties struct { rpv1.BasicResourceProperties rpv1.BasicDaprResourceProperties - Mode LinkMode `json:"mode"` - Type string `json:"type"` - Version string `json:"version"` - Metadata map[string]any `json:"metadata"` - Recipe LinkRecipe `json:"recipe,omitempty"` + Mode LinkMode `json:"mode"` + Type string `json:"type"` + Version string `json:"version"` + Metadata map[string]any `json:"metadata"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` } diff --git a/pkg/linkrp/datamodel/daprstatestore.go b/pkg/linkrp/datamodel/daprstatestore.go index 4ad9beeaef..a2aba535ae 100644 --- a/pkg/linkrp/datamodel/daprstatestore.go +++ b/pkg/linkrp/datamodel/daprstatestore.go @@ -23,8 +23,9 @@ type DaprStateStore struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *DaprStateStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *DaprStateStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -45,10 +46,10 @@ func (daprStateStore *DaprStateStore) ResourceTypeName() string { type DaprStateStoreProperties struct { rpv1.BasicResourceProperties rpv1.BasicDaprResourceProperties - Mode LinkMode `json:"mode,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - Recipe LinkRecipe `json:"recipe,omitempty"` - Resource string `json:"resource,omitempty"` - Type string `json:"type,omitempty"` - Version string `json:"version,omitempty"` + Mode LinkMode `json:"mode,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` + Resource string `json:"resource,omitempty"` + Type string `json:"type,omitempty"` + Version string `json:"version,omitempty"` } diff --git a/pkg/linkrp/datamodel/extender.go b/pkg/linkrp/datamodel/extender.go index af6956dc45..0b0ac72417 100644 --- a/pkg/linkrp/datamodel/extender.go +++ b/pkg/linkrp/datamodel/extender.go @@ -23,8 +23,9 @@ type Extender struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *Extender) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *Extender) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. diff --git a/pkg/linkrp/datamodel/linkmetadata.go b/pkg/linkrp/datamodel/linkmetadata.go index 865d91897f..f290d8fa0c 100644 --- a/pkg/linkrp/datamodel/linkmetadata.go +++ b/pkg/linkrp/datamodel/linkmetadata.go @@ -6,6 +6,7 @@ package datamodel import ( + "github.com/project-radius/radius/pkg/linkrp" rpv1 "github.com/project-radius/radius/pkg/rp/v1" ) @@ -18,36 +19,7 @@ type LinkMetadata struct { // Stores action to retrieve secret values. For Azure, connectionstring is accessed through cosmos listConnectionString operation, if secrets are not provided as input SecretValues map[string]rpv1.SecretValueReference `json:"secretValues,omitempty"` - RecipeData RecipeData `json:"recipeData,omitempty"` -} - -type RecipeData struct { - RecipeProperties - - Provider string - - // API version to use to perform operations on resources supported by the link. - // For example for Azure resources, every service has different REST API version that must be specified in the request. - APIVersion string - - // Resource ids of the resources deployed by the recipe - Resources []string -} - -// RecipeProperties represents the information needed to deploy a recipe -type RecipeProperties struct { - LinkRecipe // LinkRecipe is the recipe of the resource to be deployed - LinkType string // LinkType represent the type of the link - TemplatePath string // TemplatePath represent the recipe location - EnvParameters map[string]any // EnvParameters represents the parameters set by the operator while linking the recipe to an environment -} - -// LinkRecipe is the recipe details used to automatically deploy underlying infrastructure for a link -type LinkRecipe struct { - // Name of the recipe within the environment to use - Name string `json:"name,omitempty"` - // Parameters are key/value parameters to pass into the recipe at deployment - Parameters map[string]any `json:"parameters,omitempty"` + RecipeData linkrp.RecipeData `json:"recipeData,omitempty"` } // LinkMode specifies how to build a Link. Options are to build automatically via ‘recipe’ or ‘resource’, or build manually via ‘values’. Selection determines which set of fields to additionally require. @@ -63,36 +35,3 @@ const ( // RecipeContextParameter is the parameter context for recipe deployment RecipeContextParameter string = "context" ) - -// RecipeContext Recipe template authors can leverage the RecipeContext parameter to access Link properties to -// generate name and properties that are unique for the Link calling the recipe. -type RecipeContext struct { - Resource Resource `json:"resource,omitempty"` - Application ResourceInfo `json:"application,omitempty"` - Environment ResourceInfo `json:"environment,omitempty"` - Runtime Runtime `json:"runtime,omitempty"` -} - -// Resource contains the information needed to deploy a recipe. -// In the case the resource is a Link, it represents the Link's id, name and type. -type Resource struct { - ResourceInfo - Type string `json:"type"` -} - -// ResourceInfo name and id of the resource -type ResourceInfo struct { - Name string `json:"name"` - ID string `json:"id"` -} - -type Runtime struct { - Kubernetes Kubernetes `json:"kubernetes,omitempty"` -} - -type Kubernetes struct { - // Namespace is set to the applicationNamespace when the Link is application-scoped, and set to the environmentNamespace when the Link is environment scoped - Namespace string `json:"namespace"` - // EnvironmentNamespace is set to environment namespace. - EnvironmentNamespace string `json:"environmentNamespace"` -} diff --git a/pkg/linkrp/datamodel/mongodatabase.go b/pkg/linkrp/datamodel/mongodatabase.go index 897227f4ea..3ce4024603 100644 --- a/pkg/linkrp/datamodel/mongodatabase.go +++ b/pkg/linkrp/datamodel/mongodatabase.go @@ -8,6 +8,7 @@ package datamodel import ( v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" "github.com/project-radius/radius/pkg/linkrp" + "github.com/project-radius/radius/pkg/linkrp/renderers" rpv1 "github.com/project-radius/radius/pkg/rp/v1" ) @@ -44,8 +45,15 @@ func (mongoSecrets MongoDatabaseSecrets) IsEmpty() bool { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *MongoDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *MongoDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + r.ComputedValues = do.ComputedValues + r.SecretValues = do.SecretValues + if database, ok := do.ComputedValues[renderers.DatabaseNameValue].(string); ok { + r.Properties.Database = database + } + r.RecipeData = do.RecipeData + return nil } // OutputResources returns the output resources array. @@ -77,5 +85,5 @@ type MongoDatabaseResourceProperties struct { } type MongoDatabaseRecipeProperties struct { - Recipe LinkRecipe `json:"recipe,omitempty"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` } diff --git a/pkg/linkrp/datamodel/rabbitmq.go b/pkg/linkrp/datamodel/rabbitmq.go index 7f5d06285e..f2076874ad 100644 --- a/pkg/linkrp/datamodel/rabbitmq.go +++ b/pkg/linkrp/datamodel/rabbitmq.go @@ -23,8 +23,9 @@ type RabbitMQMessageQueue struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *RabbitMQMessageQueue) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *RabbitMQMessageQueue) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -44,10 +45,10 @@ func (rabbitmq *RabbitMQMessageQueue) ResourceTypeName() string { // RabbitMQMessageQueueProperties represents the properties of RabbitMQMessageQueue response resource. type RabbitMQMessageQueueProperties struct { rpv1.BasicResourceProperties - Queue string `json:"queue"` - Recipe LinkRecipe `json:"recipe,omitempty"` - Secrets RabbitMQSecrets `json:"secrets,omitempty"` - Mode LinkMode `json:"mode,omitempty"` + Queue string `json:"queue"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` + Secrets RabbitMQSecrets `json:"secrets,omitempty"` + Mode LinkMode `json:"mode,omitempty"` } // Secrets values consisting of secrets provided for the resource diff --git a/pkg/linkrp/datamodel/rediscache.go b/pkg/linkrp/datamodel/rediscache.go index 55a6e1419c..ddcb78edd2 100644 --- a/pkg/linkrp/datamodel/rediscache.go +++ b/pkg/linkrp/datamodel/rediscache.go @@ -6,8 +6,12 @@ package datamodel import ( + "errors" + "strconv" + v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" "github.com/project-radius/radius/pkg/linkrp" + "github.com/project-radius/radius/pkg/linkrp/renderers" rpv1 "github.com/project-radius/radius/pkg/rp/v1" ) @@ -23,8 +27,35 @@ type RedisCache struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *RedisCache) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *RedisCache) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + r.ComputedValues = do.ComputedValues + r.SecretValues = do.SecretValues + if host, ok := do.ComputedValues[renderers.Host].(string); ok { + r.Properties.Host = host + } + if port, ok := do.ComputedValues[renderers.Port]; ok { + if port != nil { + switch p := port.(type) { + case float64: + r.Properties.Port = int32(p) + case int32: + r.Properties.Port = p + case string: + converted, err := strconv.Atoi(p) + if err != nil { + return err + } + r.Properties.Port = int32(converted) + default: + return errors.New("unhandled type for the property port") + } + } + } + if username, ok := do.ComputedValues[renderers.UsernameStringValue].(string); ok { + r.Properties.Username = username + } + return nil } // OutputResources returns the output resources array. @@ -56,7 +87,7 @@ type RedisResourceProperties struct { } type RedisRecipeProperties struct { - Recipe LinkRecipe `json:"recipe,omitempty"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` } type RedisCacheProperties struct { rpv1.BasicResourceProperties diff --git a/pkg/linkrp/datamodel/sqldatabase.go b/pkg/linkrp/datamodel/sqldatabase.go index d7f56a93e0..c3a3763698 100644 --- a/pkg/linkrp/datamodel/sqldatabase.go +++ b/pkg/linkrp/datamodel/sqldatabase.go @@ -23,8 +23,9 @@ type SqlDatabase struct { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (r *SqlDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (r *SqlDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { r.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. @@ -44,9 +45,9 @@ func (sql *SqlDatabase) ResourceTypeName() string { // SqlDatabaseProperties represents the properties of SqlDatabase resource. type SqlDatabaseProperties struct { rpv1.BasicResourceProperties - Recipe LinkRecipe `json:"recipe,omitempty"` - Resource string `json:"resource,omitempty"` - Database string `json:"database,omitempty"` - Server string `json:"server,omitempty"` - Mode LinkMode `json:"mode,omitempty"` + Recipe linkrp.LinkRecipe `json:"recipe,omitempty"` + Resource string `json:"resource,omitempty"` + Database string `json:"database,omitempty"` + Server string `json:"server,omitempty"` + Mode LinkMode `json:"mode,omitempty"` } diff --git a/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute.go b/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute.go index d93a6e73c9..54e541b214 100644 --- a/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute.go +++ b/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute.go @@ -84,7 +84,7 @@ func (daprHttpRoute *CreateOrUpdateDaprInvokeHttpRoute) Run(ctx context.Context, return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if appId, ok := deploymentOutput.ComputedValues[daprinvokehttproutes.AppIDKey].(string); ok { @@ -93,7 +93,7 @@ func (daprHttpRoute *CreateOrUpdateDaprInvokeHttpRoute) Run(ctx context.Context, if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = daprHttpRoute.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = daprHttpRoute.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute_test.go b/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute_test.go index 3e3d243bcb..1424f083b3 100644 --- a/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute_test.go +++ b/pkg/linkrp/frontend/controller/daprinvokehttproutes/createorupdatedaprinvokehttproute_test.go @@ -28,7 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ ComputedValues: map[string]renderers.ComputedValueReference{ "appId": { @@ -37,8 +37,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{}, + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{}, } return rendererOutput, deploymentOutput @@ -196,7 +196,7 @@ func TestCreateOrUpdateDaprInvokeHttpRoute_20220315PrivatePreview(t *testing.T) if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute.go b/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute.go index 4910c3a2bb..3ecf9c7fa3 100644 --- a/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute.go +++ b/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute.go @@ -61,7 +61,7 @@ func (daprHttpRoute *DeleteDaprInvokeHttpRoute) Run(ctx context.Context, w http. return r, err } - err = daprHttpRoute.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = daprHttpRoute.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute_test.go b/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute_test.go index 052b39fc17..2258a7fd5f 100644 --- a/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute_test.go +++ b/pkg/linkrp/frontend/controller/daprinvokehttproutes/deletedaprinvokehttproute_test.go @@ -107,7 +107,7 @@ func TestDeleteDaprInvokeHttpRoute_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). Delete(gomock.Any(), gomock.Any()). diff --git a/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker.go b/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker.go index 81656534de..0468cdfcb8 100644 --- a/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker.go +++ b/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker.go @@ -85,7 +85,7 @@ func (daprPubSubBroker *CreateOrUpdateDaprPubSubBroker) Run(ctx context.Context, return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if topic, ok := deploymentOutput.ComputedValues[daprpubsubbrokers.TopicNameKey].(string); ok { @@ -98,7 +98,7 @@ func (daprPubSubBroker *CreateOrUpdateDaprPubSubBroker) Run(ctx context.Context, if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = daprPubSubBroker.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = daprPubSubBroker.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker_test.go b/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker_test.go index 9187185a94..581bb57ce3 100644 --- a/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker_test.go +++ b/pkg/linkrp/frontend/controller/daprpubsubbrokers/createorupdatedaprpubsubbroker_test.go @@ -32,7 +32,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { output := rpv1.OutputResource{ LocalID: rpv1.LocalIDAzureServiceBusNamespace, ResourceType: resourcemodel.ResourceType{ @@ -73,8 +73,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo ComputedValues: values, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{ { LocalID: rpv1.LocalIDAzureServiceBusNamespace, ResourceType: resourcemodel.ResourceType{ @@ -244,7 +244,7 @@ func TestCreateOrUpdateDaprPubSubBroker_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker.go b/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker.go index f7873bf1f0..d25877e56a 100644 --- a/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker.go +++ b/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker.go @@ -61,7 +61,7 @@ func (daprPubSubBroker *DeleteDaprPubSubBroker) Run(ctx context.Context, w http. return r, err } - err = daprPubSubBroker.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = daprPubSubBroker.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker_test.go b/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker_test.go index cb3ebc93fb..ed7a48fde5 100644 --- a/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker_test.go +++ b/pkg/linkrp/frontend/controller/daprpubsubbrokers/deletedaprpubsubbroker_test.go @@ -108,7 +108,7 @@ func TestDeleteDaprPubSubBroker_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). Delete(gomock.Any(), gomock.Any()). @@ -166,7 +166,7 @@ func TestDeleteDaprPubSubBroker_20220315PrivatePreview(t *testing.T) { Data: daprPubSubDataModel, }, nil }) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) opts := frontend_ctrl.Options{ Options: ctrl.Options{ diff --git a/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore.go b/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore.go index 3b2dc73415..b94c82db43 100644 --- a/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore.go +++ b/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore.go @@ -84,7 +84,7 @@ func (daprSecretStore *CreateOrUpdateDaprSecretStore) Run(ctx context.Context, w return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if componentName, ok := deploymentOutput.ComputedValues[renderers.ComponentNameKey].(string); ok { @@ -93,7 +93,7 @@ func (daprSecretStore *CreateOrUpdateDaprSecretStore) Run(ctx context.Context, w if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = daprSecretStore.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = daprSecretStore.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore_test.go b/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore_test.go index c5725174fb..288f79c542 100644 --- a/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore_test.go +++ b/pkg/linkrp/frontend/controller/daprsecretstores/createorupdatedaprsecretstore_test.go @@ -30,7 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ Resources: []rpv1.OutputResource{ { @@ -50,8 +50,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{ { LocalID: rpv1.LocalIDDaprComponent, ResourceType: resourcemodel.ResourceType{ @@ -220,7 +220,7 @@ func TestCreateOrUpdateDaprSecretStore_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore.go b/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore.go index 76d7a2d4ca..fa1beefc05 100644 --- a/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore.go +++ b/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore.go @@ -61,7 +61,7 @@ func (daprSecretStore *DeleteDaprSecretStore) Run(ctx context.Context, w http.Re return r, err } - err = daprSecretStore.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = daprSecretStore.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore_test.go b/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore_test.go index 8d23a395c1..d8d6fcbda3 100644 --- a/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore_test.go +++ b/pkg/linkrp/frontend/controller/daprsecretstores/deletedaprsecretstore_test.go @@ -108,7 +108,7 @@ func TestDeleteDaprSecretStore_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). Delete(gomock.Any(), gomock.Any()). @@ -166,7 +166,7 @@ func TestDeleteDaprSecretStore_20220315PrivatePreview(t *testing.T) { Data: daprSecretStoreDataModel, }, nil }) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) opts := frontend_ctrl.Options{ Options: ctrl.Options{ diff --git a/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore.go b/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore.go index 24970e40be..a2f9ec1fc8 100644 --- a/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore.go +++ b/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore.go @@ -84,7 +84,7 @@ func (daprStateStore *CreateOrUpdateDaprStateStore) Run(ctx context.Context, w h return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues newResource.RecipeData = deploymentOutput.RecipeData @@ -95,7 +95,7 @@ func (daprStateStore *CreateOrUpdateDaprStateStore) Run(ctx context.Context, w h if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = daprStateStore.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = daprStateStore.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore_test.go b/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore_test.go index 74578bbd5a..378c4a8950 100644 --- a/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore_test.go +++ b/pkg/linkrp/frontend/controller/daprstatestores/createorupdatedaprstatestore_test.go @@ -183,7 +183,7 @@ func TestCreateOrUpdateDaprStateStore_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). @@ -239,7 +239,7 @@ func TestCreateOrUpdateDaprStateStore_20220315PrivatePreview(t *testing.T) { } } -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ Resources: []rpv1.OutputResource{ { @@ -259,8 +259,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{ { LocalID: rpv1.LocalIDDaprComponent, ResourceType: resourcemodel.ResourceType{ diff --git a/pkg/linkrp/frontend/controller/extenders/createorupdateextender.go b/pkg/linkrp/frontend/controller/extenders/createorupdateextender.go index 464e44f634..03f2f9197d 100644 --- a/pkg/linkrp/frontend/controller/extenders/createorupdateextender.go +++ b/pkg/linkrp/frontend/controller/extenders/createorupdateextender.go @@ -74,13 +74,13 @@ func (extender *CreateOrUpdateExtender) Run(ctx context.Context, w http.Response return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = extender.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = extender.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/extenders/createorupdateextender_test.go b/pkg/linkrp/frontend/controller/extenders/createorupdateextender_test.go index f7a7e5ba8d..6022ad0758 100644 --- a/pkg/linkrp/frontend/controller/extenders/createorupdateextender_test.go +++ b/pkg/linkrp/frontend/controller/extenders/createorupdateextender_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/require" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ SecretValues: map[string]rpv1.SecretValueReference{ "secretname": { @@ -39,15 +39,15 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{}, + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{}, } return rendererOutput, deploymentOutput } func TestCreateOrUpdateExtender_20220315PrivatePreview(t *testing.T) { - setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *deployment.MockDeploymentProcessor, renderers.RendererOutput, deployment.DeploymentOutput) { + setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *deployment.MockDeploymentProcessor, renderers.RendererOutput, rpv1.DeploymentOutput) { mctrl := gomock.NewController(t) mDeploymentProcessor := deployment.NewMockDeploymentProcessor(mctrl) rendererOutput, deploymentOutput := getDeploymentProcessorOutputs() @@ -178,7 +178,7 @@ func TestCreateOrUpdateExtender_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mds. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/extenders/deleteextender.go b/pkg/linkrp/frontend/controller/extenders/deleteextender.go index 06177602cb..1cb8814f2e 100644 --- a/pkg/linkrp/frontend/controller/extenders/deleteextender.go +++ b/pkg/linkrp/frontend/controller/extenders/deleteextender.go @@ -61,7 +61,7 @@ func (extender *DeleteExtender) Run(ctx context.Context, w http.ResponseWriter, return r, err } - err = extender.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = extender.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/extenders/deleteextender_test.go b/pkg/linkrp/frontend/controller/extenders/deleteextender_test.go index 01d954de93..494694f7c8 100644 --- a/pkg/linkrp/frontend/controller/extenders/deleteextender_test.go +++ b/pkg/linkrp/frontend/controller/extenders/deleteextender_test.go @@ -113,7 +113,7 @@ func TestDeleteExtender_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mds. EXPECT(). Delete(gomock.Any(), gomock.Any()). diff --git a/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase.go b/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase.go deleted file mode 100644 index 528703d79b..0000000000 --- a/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase.go +++ /dev/null @@ -1,102 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package mongodatabases - -import ( - "context" - "net/http" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/armrpc/rest" - "github.com/project-radius/radius/pkg/linkrp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/datamodel/converter" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/linkrp/renderers" - rp_frontend "github.com/project-radius/radius/pkg/rp/frontend" - rpv1 "github.com/project-radius/radius/pkg/rp/v1" -) - -var _ ctrl.Controller = (*CreateOrUpdateMongoDatabase)(nil) - -// CreateOrUpdateMongoDatabase is the controller implementation to create or update MongoDatabase link resource. -type CreateOrUpdateMongoDatabase struct { - ctrl.Operation[*datamodel.MongoDatabase, datamodel.MongoDatabase] - dp deployment.DeploymentProcessor -} - -// NewCreateOrUpdateMongoDatabase creates a new instance of CreateOrUpdateMongoDatabase. -func NewCreateOrUpdateMongoDatabase(opts frontend_ctrl.Options) (ctrl.Controller, error) { - return &CreateOrUpdateMongoDatabase{ - Operation: ctrl.NewOperation(opts.Options, - ctrl.ResourceOptions[datamodel.MongoDatabase]{ - RequestConverter: converter.MongoDatabaseDataModelFromVersioned, - ResponseConverter: converter.MongoDatabaseDataModelToVersioned, - }), - dp: opts.DeployProcessor, - }, nil -} - -// Run executes CreateOrUpdateMongoDatabase operation. -func (mongoDatabase *CreateOrUpdateMongoDatabase) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { - serviceCtx := v1.ARMRequestContextFromContext(ctx) - - newResource, err := mongoDatabase.GetResourceFromRequest(ctx, req) - if err != nil { - return nil, err - } - - old, etag, err := mongoDatabase.GetResource(ctx, serviceCtx.ResourceID) - if err != nil { - return nil, err - } - - r, err := mongoDatabase.PrepareResource(ctx, req, newResource, old, etag) - if r != nil || err != nil { - return r, err - } - - r, err = rp_frontend.PrepareRadiusResource(ctx, newResource, old, mongoDatabase.Options()) - if r != nil || err != nil { - return r, err - } - - rendererOutput, err := mongoDatabase.dp.Render(ctx, serviceCtx.ResourceID, newResource) - if err != nil { - return nil, err - } - - deploymentOutput, err := mongoDatabase.dp.Deploy(ctx, serviceCtx.ResourceID, rendererOutput) - if err != nil { - return nil, err - } - - newResource.Properties.Status.OutputResources = deploymentOutput.Resources - newResource.ComputedValues = deploymentOutput.ComputedValues - newResource.SecretValues = deploymentOutput.SecretValues - newResource.RecipeData = deploymentOutput.RecipeData - - if database, ok := deploymentOutput.ComputedValues[renderers.DatabaseNameValue].(string); ok { - newResource.Properties.Database = database - } - - if old != nil { - diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = mongoDatabase.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) - if err != nil { - return nil, err - } - } - - newResource.SetProvisioningState(v1.ProvisioningStateSucceeded) - newEtag, err := mongoDatabase.SaveResource(ctx, serviceCtx.ResourceID.String(), newResource, etag) - if err != nil { - return nil, err - } - - return mongoDatabase.ConstructSyncResponse(ctx, req.Method, newEtag, newResource) -} diff --git a/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase_test.go b/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase_test.go deleted file mode 100644 index 55cfc29b4c..0000000000 --- a/pkg/linkrp/frontend/controller/mongodatabases/createorupdatemongodatabase_test.go +++ /dev/null @@ -1,232 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package mongodatabases - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/api/v20220315privatepreview" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/linkrp/renderers" - "github.com/project-radius/radius/pkg/resourcekinds" - "github.com/project-radius/radius/pkg/resourcemodel" - rpv1 "github.com/project-radius/radius/pkg/rp/v1" - "github.com/project-radius/radius/pkg/ucp/store" - "github.com/project-radius/radius/test/testutil" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestCreateOrUpdateMongoDatabase_20220315PrivatePreview(t *testing.T) { - mctrl := gomock.NewController(t) - defer mctrl.Finish() - - mStorageClient := store.NewMockStorageClient(mctrl) - mDeploymentProcessor := deployment.NewMockDeploymentProcessor(mctrl) - ctx := context.Background() - - createNewResourceTestCases := []struct { - desc string - headerKey string - headerValue string - resourceETag string - expectedStatusCode int - shouldFail bool - }{ - {"create-new-resource-no-if-match", "If-Match", "", "", http.StatusOK, false}, - {"create-new-resource-*-if-match", "If-Match", "*", "", http.StatusPreconditionFailed, true}, - {"create-new-resource-etag-if-match", "If-Match", "random-etag", "", http.StatusPreconditionFailed, true}, - {"create-new-resource-*-if-none-match", "If-None-Match", "*", "", http.StatusOK, false}, - } - - for _, testcase := range createNewResourceTestCases { - t.Run(testcase.desc, func(t *testing.T) { - input, dataModel, expectedOutput := getTestModelsForGetAndListApis20220315privatepreview() - rendererOutput, deploymentOutput := getDeploymentProcessorOutputs() - w := httptest.NewRecorder() - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodGet, testHeaderfile, input) - req.Header.Set(testcase.headerKey, testcase.headerValue) - ctx := testutil.ARMTestContextFromRequest(req) - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return nil, &store.ErrNotFound{} - }) - - expectedOutput.SystemData.CreatedAt = expectedOutput.SystemData.LastModifiedAt - expectedOutput.SystemData.CreatedBy = expectedOutput.SystemData.LastModifiedBy - expectedOutput.SystemData.CreatedByType = expectedOutput.SystemData.LastModifiedByType - - if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) - mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - - mStorageClient. - EXPECT(). - Save(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, obj *store.Object, opts ...store.SaveOptions) error { - // First time created objects should have the same lastModifiedAt and createdAt - dataModel.SystemData.CreatedAt = dataModel.SystemData.LastModifiedAt - obj.ETag = "new-resource-etag" - obj.Data = dataModel - return nil - }) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewCreateOrUpdateMongoDatabase(opts) - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - require.NoError(t, err) - _ = resp.Apply(ctx, w, req) - require.Equal(t, testcase.expectedStatusCode, w.Result().StatusCode) - - if !testcase.shouldFail { - actualOutput := &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(w.Body.Bytes(), actualOutput) - require.Equal(t, expectedOutput, actualOutput) - - require.Equal(t, "new-resource-etag", w.Header().Get("ETag")) - } - }) - } - - updateExistingResourceTestCases := []struct { - desc string - headerKey string - headerValue string - inputFile string - resourceETag string - expectedStatusCode int - shouldFail bool - }{ - {"update-resource-no-if-match", "If-Match", "", "", "resource-etag", http.StatusOK, false}, - {"update-resource-with-diff-app", "If-Match", "", "20220315privatepreview_input_diff_app.json", "resource-etag", http.StatusBadRequest, true}, - {"update-resource-*-if-match", "If-Match", "*", "", "resource-etag", http.StatusOK, false}, - {"update-resource-matching-if-match", "If-Match", "matching-etag", "", "matching-etag", http.StatusOK, false}, - {"update-resource-not-matching-if-match", "If-Match", "not-matching-etag", "", "another-etag", http.StatusPreconditionFailed, true}, - {"update-resource-*-if-none-match", "If-None-Match", "*", "", "another-etag", http.StatusPreconditionFailed, true}, - } - - for _, testcase := range updateExistingResourceTestCases { - t.Run(testcase.desc, func(t *testing.T) { - input, dataModel, expectedOutput := getTestModelsForGetAndListApis20220315privatepreview() - if testcase.inputFile != "" { - input = &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(testutil.ReadFixture(testcase.inputFile), input) - } - rendererOutput, deploymentOutput := getDeploymentProcessorOutputs() - w := httptest.NewRecorder() - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodGet, testHeaderfile, input) - req.Header.Set(testcase.headerKey, testcase.headerValue) - ctx := testutil.ARMTestContextFromRequest(req) - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return &store.Object{ - Metadata: store.Metadata{ID: id, ETag: testcase.resourceETag}, - Data: dataModel, - }, nil - }) - - if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) - mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) - - mStorageClient. - EXPECT(). - Save(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, obj *store.Object, opts ...store.SaveOptions) error { - obj.ETag = "updated-resource-etag" - obj.Data = dataModel - return nil - }) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewCreateOrUpdateMongoDatabase(opts) - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - _ = resp.Apply(ctx, w, req) - require.NoError(t, err) - require.Equal(t, testcase.expectedStatusCode, w.Result().StatusCode) - - if !testcase.shouldFail { - actualOutput := &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(w.Body.Bytes(), actualOutput) - require.Equal(t, expectedOutput, actualOutput) - - require.Equal(t, "updated-resource-etag", w.Header().Get("ETag")) - } - }) - } -} - -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { - rendererOutput := renderers.RendererOutput{ - Resources: []rpv1.OutputResource{ - { - LocalID: rpv1.LocalIDAzureCosmosAccount, - ResourceType: resourcemodel.ResourceType{ - Type: resourcekinds.AzureCosmosAccount, - Provider: resourcemodel.ProviderAzure, - }, - Identity: resourcemodel.ResourceIdentity{}, - }, - }, - SecretValues: map[string]rpv1.SecretValueReference{ - renderers.UsernameStringValue: {Value: "testUser"}, - renderers.PasswordStringHolder: {Value: "testPassword"}, - renderers.ConnectionStringValue: {Value: "mongodb://testUser:testPassword@testAccount1.mongo.cosmos.azure.com:10255"}, - }, - ComputedValues: map[string]renderers.ComputedValueReference{ - renderers.DatabaseNameValue: { - Value: "db", - }, - }, - } - - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ - { - LocalID: rpv1.LocalIDAzureCosmosAccount, - ResourceType: resourcemodel.ResourceType{ - Type: resourcekinds.AzureCosmosAccount, - Provider: resourcemodel.ProviderAzure, - }, - }, - }, - ComputedValues: map[string]any{ - "database": rendererOutput.ComputedValues["database"].Value, - }, - } - - return rendererOutput, deploymentOutput -} diff --git a/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase.go b/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase.go deleted file mode 100644 index 1966cceaa1..0000000000 --- a/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase.go +++ /dev/null @@ -1,71 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package mongodatabases - -import ( - "context" - "net/http" - "time" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/armrpc/rest" - "github.com/project-radius/radius/pkg/linkrp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/datamodel/converter" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" -) - -var _ ctrl.Controller = (*DeleteMongoDatabase)(nil) - -const ( - // AsyncDeleteMongoDatabaseOperationTimeout is the default timeout duration of async delete mongodatabase operation. - // MongoDatabase deletion take 8-10 mins. - AsyncDeleteMongoDatabaseOperationTimeout = time.Duration(900) * time.Second -) - -// DeleteMongoDatabase is the controller implementation to delete mongoDatabase link resource. -type DeleteMongoDatabase struct { - ctrl.Operation[*datamodel.MongoDatabase, datamodel.MongoDatabase] -} - -// NewDeleteMongoDatabase creates a new instance DeleteMongoDatabase. -func NewDeleteMongoDatabase(opts frontend_ctrl.Options) (ctrl.Controller, error) { - return &DeleteMongoDatabase{ - Operation: ctrl.NewOperation(opts.Options, - ctrl.ResourceOptions[datamodel.MongoDatabase]{ - RequestConverter: converter.MongoDatabaseDataModelFromVersioned, - ResponseConverter: converter.MongoDatabaseDataModelToVersioned, - }), - }, nil -} - -func (mongoDatabase *DeleteMongoDatabase) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { - serviceCtx := v1.ARMRequestContextFromContext(ctx) - - old, etag, err := mongoDatabase.GetResource(ctx, serviceCtx.ResourceID) - if err != nil { - return nil, err - } - - if old == nil { - return rest.NewNoContentResponse(), nil - } - - if etag == "" { - return rest.NewNoContentResponse(), nil - } - - r, err := mongoDatabase.PrepareResource(ctx, req, nil, old, etag) - if r != nil || err != nil { - return r, err - } - - if r, err := mongoDatabase.PrepareAsyncOperation(ctx, old, v1.ProvisioningStateAccepted, AsyncDeleteMongoDatabaseOperationTimeout, &etag); r != nil || err != nil { - return r, err - } - - return mongoDatabase.ConstructAsyncResponse(ctx, req.Method, etag, old) -} diff --git a/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase_test.go b/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase_test.go deleted file mode 100644 index 51fa97c7e0..0000000000 --- a/pkg/linkrp/frontend/controller/mongodatabases/deletemongodatabase_test.go +++ /dev/null @@ -1,113 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package mongodatabases - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - "github.com/project-radius/radius/pkg/armrpc/asyncoperation/statusmanager" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/api/v20220315privatepreview" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/ucp/store" - "github.com/project-radius/radius/test/testutil" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestDeleteMongoDatabase_20220315PrivatePreview(t *testing.T) { - setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *statusmanager.MockStatusManager) { - mctrl := gomock.NewController(t) - mds := store.NewMockStorageClient(mctrl) - msm := statusmanager.NewMockStatusManager(mctrl) - - return func(tb testing.TB) { - mctrl.Finish() - }, mds, msm - } - - t.Parallel() - - deleteCases := []struct { - desc string - etag string - curState v1.ProvisioningState - getErr error - qErr error - saveErr error - code int - }{ - {"async-delete-non-existing-resource-no-etag", "", v1.ProvisioningStateNone, &store.ErrNotFound{}, nil, nil, http.StatusNoContent}, - {"async-delete-existing-resource-not-in-terminal-state", "random-etag", v1.ProvisioningStateUpdating, nil, nil, nil, http.StatusConflict}, - {"async-delete-existing-resource-success", "random-etag", v1.ProvisioningStateSucceeded, nil, nil, nil, http.StatusAccepted}, - } - - for _, tt := range deleteCases { - t.Run(tt.desc, func(t *testing.T) { - teardownTest, mds, msm := setupTest(t) - defer teardownTest(t) - - w := httptest.NewRecorder() - - req, _ := testutil.GetARMTestHTTPRequest(context.Background(), http.MethodDelete, testHeaderfile, nil) - req.Header.Set("If-Match", tt.etag) - - ctx := testutil.ARMTestContextFromRequest(req) - _, appDataModel, _ := getTestModels20220315privatepreview() - - appDataModel.InternalMetadata.AsyncProvisioningState = tt.curState - - mds.EXPECT(). - Get(gomock.Any(), gomock.Any()). - Return(&store.Object{ - Metadata: store.Metadata{ID: appDataModel.ID, ETag: tt.etag}, - Data: appDataModel, - }, tt.getErr). - Times(1) - - if tt.getErr == nil && appDataModel.InternalMetadata.AsyncProvisioningState.IsTerminal() { - msm.EXPECT().QueueAsyncOperation(gomock.Any(), gomock.Any(), gomock.Any()). - Return(tt.qErr). - Times(1) - - mds.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()). - Return(tt.saveErr). - Times(1) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mds, - StatusManager: msm, - }, - } - - ctl, err := NewDeleteMongoDatabase(opts) - require.NoError(t, err) - - resp, err := ctl.Run(ctx, w, req) - require.NoError(t, err) - - err = resp.Apply(ctx, w, req) - require.NoError(t, err) - - result := w.Result() - require.Equal(t, tt.code, result.StatusCode) - - // If happy path, expect that the returned object has Deleting state - if tt.code == http.StatusAccepted { - actualOutput := &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(w.Body.Bytes(), actualOutput) - } - }) - } -} diff --git a/pkg/linkrp/frontend/controller/mongodatabases/v20220315privatepreview_test.go b/pkg/linkrp/frontend/controller/mongodatabases/v20220315privatepreview_test.go index 4f7d1b1043..a337a6600d 100644 --- a/pkg/linkrp/frontend/controller/mongodatabases/v20220315privatepreview_test.go +++ b/pkg/linkrp/frontend/controller/mongodatabases/v20220315privatepreview_test.go @@ -30,19 +30,3 @@ func getTestModels20220315privatepreview() (input *v20220315privatepreview.Mongo return input, dataModel, output } - -func getTestModelsForGetAndListApis20220315privatepreview() (input *v20220315privatepreview.MongoDatabaseResource, dataModel *datamodel.MongoDatabase, output *v20220315privatepreview.MongoDatabaseResource) { - rawInput := testutil.ReadFixture("20220315privatepreview_input.json") - input = &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(rawInput, input) - - rawDataModel := testutil.ReadFixture("20220315privatepreview_datamodel.json") - dataModel = &datamodel.MongoDatabase{} - _ = json.Unmarshal(rawDataModel, dataModel) - - rawExpectedOutput := testutil.ReadFixture("20220315privatepreview_output.json") - output = &v20220315privatepreview.MongoDatabaseResource{} - _ = json.Unmarshal(rawExpectedOutput, output) - - return input, dataModel, output -} diff --git a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq.go b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq.go index bf2db51207..ad0ed2ff0a 100644 --- a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq.go +++ b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq.go @@ -75,7 +75,7 @@ func (rabbitmq *CreateOrUpdateRabbitMQMessageQueue) Run(ctx context.Context, w h return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if queue, ok := deploymentOutput.ComputedValues[rabbitmqmessagequeues.QueueNameKey].(string); ok { @@ -84,7 +84,7 @@ func (rabbitmq *CreateOrUpdateRabbitMQMessageQueue) Run(ctx context.Context, w h if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = rabbitmq.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = rabbitmq.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq_test.go b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq_test.go index 56b14cc5c5..e13c3c3ed9 100644 --- a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq_test.go +++ b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/createorupdaterabbitmq_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/require" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ SecretValues: map[string]rpv1.SecretValueReference{ renderers.ConnectionStringValue: { @@ -39,8 +39,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{}, + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{}, } return rendererOutput, deploymentOutput @@ -170,7 +170,7 @@ func TestCreateOrUpdateRabbitMQ_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq.go b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq.go index 581d51adb5..fa4c40d30f 100644 --- a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq.go +++ b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq.go @@ -61,7 +61,7 @@ func (rabbitmq *DeleteRabbitMQMessageQueue) Run(ctx context.Context, w http.Resp return r, err } - err = rabbitmq.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = rabbitmq.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq_test.go b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq_test.go index f15b019eb5..0cb68f5209 100644 --- a/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq_test.go +++ b/pkg/linkrp/frontend/controller/rabbitmqmessagequeues/deleterabbitmq_test.go @@ -108,7 +108,7 @@ func TestDeleteRabbitMQMessageQueue_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). Delete(gomock.Any(), gomock.Any()). @@ -165,7 +165,7 @@ func TestDeleteRabbitMQMessageQueue_20220315PrivatePreview(t *testing.T) { Data: rabbitMQDataModel, }, nil }) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) opts := frontend_ctrl.Options{ Options: ctrl.Options{ diff --git a/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache.go b/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache.go deleted file mode 100644 index 7acca0ff33..0000000000 --- a/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache.go +++ /dev/null @@ -1,123 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package rediscaches - -import ( - "context" - "errors" - "net/http" - "strconv" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/armrpc/rest" - "github.com/project-radius/radius/pkg/linkrp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/datamodel/converter" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/linkrp/renderers" - rp_frontend "github.com/project-radius/radius/pkg/rp/frontend" - rpv1 "github.com/project-radius/radius/pkg/rp/v1" -) - -var _ ctrl.Controller = (*CreateOrUpdateRedisCache)(nil) - -// CreateOrUpdateRedisCache is the controller implementation to create or update RedisCache link resource. -type CreateOrUpdateRedisCache struct { - ctrl.Operation[*datamodel.RedisCache, datamodel.RedisCache] - dp deployment.DeploymentProcessor -} - -// NewCreateOrUpdateRedisCache creates a new instance of CreateOrUpdateRedisCache. -func NewCreateOrUpdateRedisCache(opts frontend_ctrl.Options) (ctrl.Controller, error) { - return &CreateOrUpdateRedisCache{ - Operation: ctrl.NewOperation(opts.Options, - ctrl.ResourceOptions[datamodel.RedisCache]{ - RequestConverter: converter.RedisCacheDataModelFromVersioned, - ResponseConverter: converter.RedisCacheDataModelToVersioned, - }), - dp: opts.DeployProcessor, - }, nil -} - -// Run executes CreateOrUpdateRedisCache operation. -func (redisCache *CreateOrUpdateRedisCache) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { - serviceCtx := v1.ARMRequestContextFromContext(ctx) - - newResource, err := redisCache.GetResourceFromRequest(ctx, req) - if err != nil { - return nil, err - } - - old, etag, err := redisCache.GetResource(ctx, serviceCtx.ResourceID) - if err != nil { - return nil, err - } - - r, err := redisCache.PrepareResource(ctx, req, newResource, old, etag) - if r != nil || err != nil { - return r, err - } - - r, err = rp_frontend.PrepareRadiusResource(ctx, newResource, old, redisCache.Options()) - if r != nil || err != nil { - return r, err - } - - rendererOutput, err := redisCache.dp.Render(ctx, serviceCtx.ResourceID, newResource) - if err != nil { - return nil, err - } - - deploymentOutput, err := redisCache.dp.Deploy(ctx, serviceCtx.ResourceID, rendererOutput) - if err != nil { - return nil, err - } - - newResource.Properties.Status.OutputResources = deploymentOutput.Resources - newResource.ComputedValues = deploymentOutput.ComputedValues - newResource.SecretValues = deploymentOutput.SecretValues - if host, ok := deploymentOutput.ComputedValues[renderers.Host].(string); ok { - newResource.Properties.Host = host - } - if port, ok := deploymentOutput.ComputedValues[renderers.Port]; ok { - if port != nil { - switch p := port.(type) { - case float64: - newResource.Properties.Port = int32(p) - case int32: - newResource.Properties.Port = p - case string: - converted, err := strconv.Atoi(p) - if err != nil { - return nil, err - } - newResource.Properties.Port = int32(converted) - default: - return nil, errors.New("unhandled type for the property port") - } - } - } - if username, ok := deploymentOutput.ComputedValues[renderers.UsernameStringValue].(string); ok { - newResource.Properties.Username = username - } - - if old != nil { - diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = redisCache.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) - if err != nil { - return nil, err - } - } - - newResource.SetProvisioningState(v1.ProvisioningStateSucceeded) - newEtag, err := redisCache.SaveResource(ctx, serviceCtx.ResourceID.String(), newResource, etag) - if err != nil { - return nil, err - } - - return redisCache.ConstructSyncResponse(ctx, req.Method, newEtag, newResource) -} diff --git a/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache_test.go b/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache_test.go deleted file mode 100644 index 94d73e9db5..0000000000 --- a/pkg/linkrp/frontend/controller/rediscaches/createorupdaterediscache_test.go +++ /dev/null @@ -1,267 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package rediscaches - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/api/v20220315privatepreview" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/linkrp/renderers" - "github.com/project-radius/radius/pkg/resourcekinds" - "github.com/project-radius/radius/pkg/resourcemodel" - rpv1 "github.com/project-radius/radius/pkg/rp/v1" - "github.com/project-radius/radius/pkg/ucp/store" - "github.com/project-radius/radius/test/testutil" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func getDeploymentProcessorOutputs(buildComputedValueReferences bool) (renderers.RendererOutput, deployment.DeploymentOutput) { - var computedValues map[string]renderers.ComputedValueReference - var portValue any - if buildComputedValueReferences { - computedValues = map[string]renderers.ComputedValueReference{ - renderers.Host: { - LocalID: rpv1.LocalIDAzureRedis, - JSONPointer: "/properties/hostName", - }, - renderers.Port: { - LocalID: rpv1.LocalIDAzureRedis, - JSONPointer: "/properties/sslPort", - }, - renderers.UsernameStringValue: { - Value: "redisusername", - }, - } - - portValue = "10255" - } else { - portValue = float64(10255) - - computedValues = map[string]renderers.ComputedValueReference{ - renderers.Host: { - Value: "myrediscache.redis.cache.windows.net", - }, - renderers.Port: { - Value: portValue, - }, - renderers.UsernameStringValue: { - Value: "redisusername", - }, - } - } - - rendererOutput := renderers.RendererOutput{ - Resources: []rpv1.OutputResource{ - { - LocalID: rpv1.LocalIDAzureRedis, - ResourceType: resourcemodel.ResourceType{ - Type: resourcekinds.AzureRedis, - Provider: resourcemodel.ProviderAzure, - }, - Identity: resourcemodel.ResourceIdentity{}, - }, - }, - SecretValues: map[string]rpv1.SecretValueReference{ - renderers.ConnectionStringValue: {Value: "test-connection-string"}, - renderers.PasswordStringHolder: {Value: "testpassword"}, - renderers.UsernameStringValue: {Value: "redisusername"}, - }, - ComputedValues: computedValues, - } - - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ - { - LocalID: rpv1.LocalIDAzureRedis, - ResourceType: resourcemodel.ResourceType{ - Type: resourcekinds.AzureRedis, - Provider: resourcemodel.ProviderAzure, - }, - }, - }, - ComputedValues: map[string]any{ - renderers.Host: "myrediscache.redis.cache.windows.net", - renderers.Port: portValue, - renderers.UsernameStringValue: "redisusername", - }, - } - - return rendererOutput, deploymentOutput -} - -func TestCreateOrUpdateRedisCache_20220315PrivatePreview(t *testing.T) { - mctrl := gomock.NewController(t) - defer mctrl.Finish() - - mStorageClient := store.NewMockStorageClient(mctrl) - mDeploymentProcessor := deployment.NewMockDeploymentProcessor(mctrl) - rendererOutput, deploymentOutput := getDeploymentProcessorOutputs(false) - ctx := context.Background() - - createNewResourceTestCases := []struct { - desc string - headerKey string - headerValue string - resourceETag string - expectedStatusCode int - shouldFail bool - azureResource bool - }{ - {"create-new-resource-no-if-match", "If-Match", "", "", http.StatusOK, false, false}, - {"create-new-resource-*-if-match", "If-Match", "*", "", http.StatusPreconditionFailed, true, false}, - {"create-new-resource-etag-if-match", "If-Match", "random-etag", "", http.StatusPreconditionFailed, true, false}, - {"create-new-resource-*-if-none-match", "If-None-Match", "*", "", http.StatusOK, false, true}, - } - - for _, testcase := range createNewResourceTestCases { - t.Run(testcase.desc, func(t *testing.T) { - if testcase.azureResource { - rendererOutput, deploymentOutput = getDeploymentProcessorOutputs(true) - } - - input, dataModel, expectedOutput := getTestModelsForGetAndListApis20220315privatepreview() - w := httptest.NewRecorder() - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodGet, testHeaderfile, input) - req.Header.Set(testcase.headerKey, testcase.headerValue) - ctx := testutil.ARMTestContextFromRequest(req) - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return nil, &store.ErrNotFound{} - }) - - expectedOutput.SystemData.CreatedAt = expectedOutput.SystemData.LastModifiedAt - expectedOutput.SystemData.CreatedBy = expectedOutput.SystemData.LastModifiedBy - expectedOutput.SystemData.CreatedByType = expectedOutput.SystemData.LastModifiedByType - - if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) - mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - - mStorageClient. - EXPECT(). - Save(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, obj *store.Object, opts ...store.SaveOptions) error { - // First time created objects should have the same lastModifiedAt and createdAt - dataModel.SystemData.CreatedAt = dataModel.SystemData.LastModifiedAt - obj.ETag = "new-resource-etag" - obj.Data = dataModel - return nil - }) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewCreateOrUpdateRedisCache(opts) - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - require.NoError(t, err) - _ = resp.Apply(ctx, w, req) - require.Equal(t, testcase.expectedStatusCode, w.Result().StatusCode) - if !testcase.shouldFail { - actualOutput := &v20220315privatepreview.RedisCacheResource{} - _ = json.Unmarshal(w.Body.Bytes(), actualOutput) - require.Equal(t, expectedOutput, actualOutput) - - require.Equal(t, "new-resource-etag", w.Header().Get("ETag")) - } - }) - } - - updateExistingResourceTestCases := []struct { - desc string - headerKey string - headerValue string - inputFile string - resourceETag string - expectedStatusCode int - shouldFail bool - }{ - {"update-resource-no-if-match", "If-Match", "", "", "resource-etag", http.StatusOK, false}, - {"update-resource-with-diff-app", "If-Match", "", "20220315privatepreview_input_diff_app.json", "resource-etag", http.StatusBadRequest, true}, - {"update-resource-*-if-match", "If-Match", "*", "", "resource-etag", http.StatusOK, false}, - {"update-resource-matching-if-match", "If-Match", "matching-etag", "", "matching-etag", http.StatusOK, false}, - {"update-resource-not-matching-if-match", "If-Match", "not-matching-etag", "", "another-etag", http.StatusPreconditionFailed, true}, - {"update-resource-*-if-none-match", "If-None-Match", "*", "", "another-etag", http.StatusPreconditionFailed, true}, - } - - for _, testcase := range updateExistingResourceTestCases { - t.Run(testcase.desc, func(t *testing.T) { - input, dataModel, expectedOutput := getTestModelsForGetAndListApis20220315privatepreview() - if testcase.inputFile != "" { - input = &v20220315privatepreview.RedisCacheResource{} - _ = json.Unmarshal(testutil.ReadFixture(testcase.inputFile), input) - } - w := httptest.NewRecorder() - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodGet, testHeaderfile, input) - req.Header.Set(testcase.headerKey, testcase.headerValue) - ctx := testutil.ARMTestContextFromRequest(req) - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return &store.Object{ - Metadata: store.Metadata{ID: id, ETag: testcase.resourceETag}, - Data: dataModel, - }, nil - }) - - if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) - mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) - - mStorageClient. - EXPECT(). - Save(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, obj *store.Object, opts ...store.SaveOptions) error { - obj.ETag = "updated-resource-etag" - obj.Data = dataModel - return nil - }) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewCreateOrUpdateRedisCache(opts) - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - _ = resp.Apply(ctx, w, req) - require.NoError(t, err) - require.Equal(t, testcase.expectedStatusCode, w.Result().StatusCode) - - if !testcase.shouldFail { - actualOutput := &v20220315privatepreview.RedisCacheResource{} - _ = json.Unmarshal(w.Body.Bytes(), actualOutput) - require.Equal(t, expectedOutput, actualOutput) - - require.Equal(t, "updated-resource-etag", w.Header().Get("ETag")) - } - }) - } -} diff --git a/pkg/linkrp/frontend/controller/rediscaches/deleterediscache.go b/pkg/linkrp/frontend/controller/rediscaches/deleterediscache.go deleted file mode 100644 index 9c70a6d826..0000000000 --- a/pkg/linkrp/frontend/controller/rediscaches/deleterediscache.go +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package rediscaches - -import ( - "context" - "errors" - "net/http" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - "github.com/project-radius/radius/pkg/armrpc/rest" - "github.com/project-radius/radius/pkg/linkrp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/datamodel/converter" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/ucp/store" -) - -var _ ctrl.Controller = (*DeleteRedisCache)(nil) - -// DeleteRedisCache is the controller implementation to delete redisCache link resource. -type DeleteRedisCache struct { - ctrl.Operation[*datamodel.RedisCache, datamodel.RedisCache] - dp deployment.DeploymentProcessor -} - -// NewDeleteRedisCache creates a new instance DeleteRedisCache. -func NewDeleteRedisCache(opts frontend_ctrl.Options) (ctrl.Controller, error) { - return &DeleteRedisCache{ - Operation: ctrl.NewOperation(opts.Options, - ctrl.ResourceOptions[datamodel.RedisCache]{ - RequestConverter: converter.RedisCacheDataModelFromVersioned, - ResponseConverter: converter.RedisCacheDataModelToVersioned, - }), - dp: opts.DeployProcessor, - }, nil -} - -func (redisCache *DeleteRedisCache) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) { - serviceCtx := v1.ARMRequestContextFromContext(ctx) - - old, etag, err := redisCache.GetResource(ctx, serviceCtx.ResourceID) - if err != nil { - return nil, err - } - - if old == nil { - return rest.NewNoContentResponse(), nil - } - - if etag == "" { - return rest.NewNoContentResponse(), nil - } - - r, err := redisCache.PrepareResource(ctx, req, nil, old, etag) - if r != nil || err != nil { - return r, err - } - - err = redisCache.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) - if err != nil { - return nil, err - } - - err = redisCache.StorageClient().Delete(ctx, serviceCtx.ResourceID.String()) - if err != nil { - if errors.Is(&store.ErrNotFound{}, err) { - return rest.NewNoContentResponse(), nil - } - return nil, err - } - - return rest.NewOKResponse(nil), nil -} diff --git a/pkg/linkrp/frontend/controller/rediscaches/deleterediscache_test.go b/pkg/linkrp/frontend/controller/rediscaches/deleterediscache_test.go deleted file mode 100644 index 9e33667ff5..0000000000 --- a/pkg/linkrp/frontend/controller/rediscaches/deleterediscache_test.go +++ /dev/null @@ -1,184 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// ------------------------------------------------------------ - -package rediscaches - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "testing" - - v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" - ctrl "github.com/project-radius/radius/pkg/armrpc/frontend/controller" - frontend_ctrl "github.com/project-radius/radius/pkg/linkrp/frontend/controller" - "github.com/project-radius/radius/pkg/linkrp/frontend/deployment" - "github.com/project-radius/radius/pkg/ucp/store" - "github.com/project-radius/radius/test/testutil" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" -) - -func TestDeleteRedisCache_20220315PrivatePreview(t *testing.T) { - mctrl := gomock.NewController(t) - defer mctrl.Finish() - - mStorageClient := store.NewMockStorageClient(mctrl) - mDeploymentProcessor := deployment.NewMockDeploymentProcessor(mctrl) - ctx := context.Background() - - t.Parallel() - - t.Run("delete non-existing resource", func(t *testing.T) { - w := httptest.NewRecorder() - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodDelete, testHeaderfile, nil) - ctx := testutil.ARMTestContextFromRequest(req) - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return nil, &store.ErrNotFound{} - }) - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewDeleteRedisCache(opts) - - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - require.NoError(t, err) - err = resp.Apply(ctx, w, req) - require.NoError(t, err) - - result := w.Result() - require.Equal(t, http.StatusNoContent, result.StatusCode) - - body := result.Body - defer body.Close() - payload, err := io.ReadAll(body) - require.NoError(t, err) - require.Empty(t, payload, "response body should be empty") - }) - - existingResourceDeleteTestCases := []struct { - desc string - ifMatchETag string - resourceETag string - expectedStatusCode int - shouldFail bool - }{ - {"delete-existing-resource-no-if-match", "", "random-etag", http.StatusOK, false}, - {"delete-not-existing-resource-no-if-match", "", "", http.StatusNoContent, true}, - {"delete-existing-resource-matching-if-match", "matching-etag", "matching-etag", http.StatusOK, false}, - {"delete-existing-resource-not-matching-if-match", "not-matching-etag", "another-etag", http.StatusPreconditionFailed, true}, - {"delete-not-existing-resource-*-if-match", "*", "", http.StatusNoContent, true}, - {"delete-existing-resource-*-if-match", "*", "random-etag", http.StatusOK, false}, - } - - for _, testcase := range existingResourceDeleteTestCases { - t.Run(testcase.desc, func(t *testing.T) { - w := httptest.NewRecorder() - - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodDelete, testHeaderfile, nil) - req.Header.Set("If-Match", testcase.ifMatchETag) - - ctx := testutil.ARMTestContextFromRequest(req) - _, redisDataModel, _ := getTestModels20220315privatepreview() - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return &store.Object{ - Metadata: store.Metadata{ID: id, ETag: testcase.resourceETag}, - Data: redisDataModel, - }, nil - }) - - if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) - mStorageClient. - EXPECT(). - Delete(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.DeleteOptions) error { - return nil - }) - } - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewDeleteRedisCache(opts) - require.NoError(t, err) - resp, err := ctl.Run(ctx, w, req) - require.NoError(t, err) - err = resp.Apply(ctx, w, req) - require.NoError(t, err) - - result := w.Result() - require.Equal(t, testcase.expectedStatusCode, result.StatusCode) - - body := result.Body - defer body.Close() - payload, err := io.ReadAll(body) - require.NoError(t, err) - - if result.StatusCode == http.StatusOK || result.StatusCode == http.StatusNoContent { - // We return either 200 or 204 without a response body for success. - require.Empty(t, payload, "response body should be empty") - } else { - armerr := v1.ErrorResponse{} - err = json.Unmarshal(payload, &armerr) - require.NoError(t, err) - require.Equal(t, v1.CodePreconditionFailed, armerr.Error.Code) - require.NotEmpty(t, armerr.Error.Target) - } - }) - - t.Run("delete deploymentprocessor error", func(t *testing.T) { - req, _ := testutil.GetARMTestHTTPRequest(ctx, http.MethodDelete, testHeaderfile, nil) - ctx := testutil.ARMTestContextFromRequest(req) - _, redisDataModel, _ := getTestModels20220315privatepreview() - w := httptest.NewRecorder() - - mStorageClient. - EXPECT(). - Get(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) { - return &store.Object{ - Metadata: store.Metadata{ID: id, ETag: "test-etag"}, - Data: redisDataModel, - }, nil - }) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("deploymentprocessor: failed to delete the output resource")) - - opts := frontend_ctrl.Options{ - Options: ctrl.Options{ - StorageClient: mStorageClient, - }, - DeployProcessor: mDeploymentProcessor, - } - - ctl, err := NewDeleteRedisCache(opts) - require.NoError(t, err) - _, err = ctl.Run(ctx, w, req) - require.Error(t, err) - }) - } -} diff --git a/pkg/linkrp/frontend/controller/rediscaches/v20220315privatepreview_test.go b/pkg/linkrp/frontend/controller/rediscaches/v20220315privatepreview_test.go index 2ec1c75ec2..33a8e4d03f 100644 --- a/pkg/linkrp/frontend/controller/rediscaches/v20220315privatepreview_test.go +++ b/pkg/linkrp/frontend/controller/rediscaches/v20220315privatepreview_test.go @@ -30,19 +30,3 @@ func getTestModels20220315privatepreview() (input *v20220315privatepreview.Redis return input, dataModel, output } - -func getTestModelsForGetAndListApis20220315privatepreview() (input *v20220315privatepreview.RedisCacheResource, dataModel *datamodel.RedisCache, output *v20220315privatepreview.RedisCacheResource) { - rawInput := testutil.ReadFixture("20220315privatepreview_input.json") - input = &v20220315privatepreview.RedisCacheResource{} - _ = json.Unmarshal(rawInput, input) - - rawDataModel := testutil.ReadFixture("20220315privatepreview_datamodel.json") - dataModel = &datamodel.RedisCache{} - _ = json.Unmarshal(rawDataModel, dataModel) - - rawExpectedOutput := testutil.ReadFixture("20220315privatepreview_output.json") - output = &v20220315privatepreview.RedisCacheResource{} - _ = json.Unmarshal(rawExpectedOutput, output) - - return input, dataModel, output -} diff --git a/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase.go b/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase.go index 47357db32a..13584e9731 100644 --- a/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase.go +++ b/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase.go @@ -74,7 +74,7 @@ func (sqlDatabase *CreateOrUpdateSqlDatabase) Run(ctx context.Context, w http.Re return nil, err } - newResource.Properties.Status.OutputResources = deploymentOutput.Resources + newResource.Properties.Status.OutputResources = deploymentOutput.DeployedOutputResources newResource.ComputedValues = deploymentOutput.ComputedValues newResource.SecretValues = deploymentOutput.SecretValues if server, ok := deploymentOutput.ComputedValues["server"].(string); ok { @@ -86,7 +86,7 @@ func (sqlDatabase *CreateOrUpdateSqlDatabase) Run(ctx context.Context, w http.Re if old != nil { diff := rpv1.GetGCOutputResources(newResource.Properties.Status.OutputResources, old.Properties.Status.OutputResources) - err = sqlDatabase.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: newResource, OutputResources: diff, ComputedValues: newResource.ComputedValues, SecretValues: newResource.SecretValues, RecipeData: newResource.RecipeData}) + err = sqlDatabase.dp.Delete(ctx, serviceCtx.ResourceID, diff) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase_test.go b/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase_test.go index 64b9454d88..cb48864de2 100644 --- a/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase_test.go +++ b/pkg/linkrp/frontend/controller/sqldatabases/createorupdatesqldatabase_test.go @@ -27,7 +27,7 @@ import ( "github.com/stretchr/testify/require" ) -func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.DeploymentOutput) { +func getDeploymentProcessorOutputs() (renderers.RendererOutput, rpv1.DeploymentOutput) { rendererOutput := renderers.RendererOutput{ Resources: []rpv1.OutputResource{ { @@ -51,8 +51,8 @@ func getDeploymentProcessorOutputs() (renderers.RendererOutput, deployment.Deplo }, } - deploymentOutput := deployment.DeploymentOutput{ - Resources: []rpv1.OutputResource{ + deploymentOutput := rpv1.DeploymentOutput{ + DeployedOutputResources: []rpv1.OutputResource{ { LocalID: rpv1.LocalIDAzureSqlServer, ResourceType: resourcemodel.ResourceType{ @@ -188,7 +188,7 @@ func TestCreateOrUpdateSqlDatabase_20220315PrivatePreview(t *testing.T) { if !testcase.shouldFail { mDeploymentProcessor.EXPECT().Render(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(rendererOutput, nil) mDeploymentProcessor.EXPECT().Deploy(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(deploymentOutput, nil) - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). diff --git a/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase.go b/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase.go index 49f24ac302..6034fdda86 100644 --- a/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase.go +++ b/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase.go @@ -61,7 +61,7 @@ func (sqlDatabase *DeleteSqlDatabase) Run(ctx context.Context, w http.ResponseWr return r, err } - err = sqlDatabase.dp.Delete(ctx, deployment.ResourceData{ID: serviceCtx.ResourceID, Resource: old, OutputResources: old.Properties.Status.OutputResources, ComputedValues: old.ComputedValues, SecretValues: old.SecretValues, RecipeData: old.RecipeData}) + err = sqlDatabase.dp.Delete(ctx, serviceCtx.ResourceID, old.Properties.Status.OutputResources) if err != nil { return nil, err } diff --git a/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase_test.go b/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase_test.go index add55dc17c..2561b22023 100644 --- a/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase_test.go +++ b/pkg/linkrp/frontend/controller/sqldatabases/deletesqldatabase_test.go @@ -107,7 +107,7 @@ func TestDeleteSqlDatabase_20220315PrivatePreview(t *testing.T) { }) if !testcase.shouldFail { - mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) + mDeploymentProcessor.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) mStorageClient. EXPECT(). Delete(gomock.Any(), gomock.Any()). diff --git a/pkg/linkrp/frontend/deployment/deploymentprocessor.go b/pkg/linkrp/frontend/deployment/deploymentprocessor.go index 9c298dd593..f9584c877e 100644 --- a/pkg/linkrp/frontend/deployment/deploymentprocessor.go +++ b/pkg/linkrp/frontend/deployment/deploymentprocessor.go @@ -34,8 +34,8 @@ import ( type DeploymentProcessor interface { Render(ctx context.Context, id resources.ID, resource v1.ResourceDataModel) (renderers.RendererOutput, error) - Deploy(ctx context.Context, id resources.ID, rendererOutput renderers.RendererOutput) (DeploymentOutput, error) - Delete(ctx context.Context, resource ResourceData) error + Deploy(ctx context.Context, id resources.ID, rendererOutput renderers.RendererOutput) (rpv1.DeploymentOutput, error) + Delete(ctx context.Context, id resources.ID, outputResources []rpv1.OutputResource) error FetchSecrets(ctx context.Context, resource ResourceData) (map[string]any, error) } @@ -51,21 +51,13 @@ type deploymentProcessor struct { secretClient sv.SecretValueClient k8s client.Client } - -type DeploymentOutput struct { - Resources []rpv1.OutputResource - ComputedValues map[string]any - SecretValues map[string]rpv1.SecretValueReference - RecipeData datamodel.RecipeData -} - type ResourceData struct { ID resources.ID Resource v1.ResourceDataModel OutputResources []rpv1.OutputResource ComputedValues map[string]any SecretValues map[string]rpv1.SecretValueReference - RecipeData datamodel.RecipeData + RecipeData linkrp.RecipeData } type EnvironmentMetadata struct { @@ -118,7 +110,7 @@ func (dp *deploymentProcessor) Render(ctx context.Context, id resources.ID, reso rendererOutput, err := renderer.Render(ctx, resource, renderers.RenderOptions{ Namespace: kubeNamespace, - RecipeProperties: datamodel.RecipeProperties{ + RecipeProperties: linkrp.RecipeProperties{ LinkRecipe: recipe, LinkType: envMetadata.RecipeLinkType, TemplatePath: envMetadata.RecipeTemplatePath, @@ -157,7 +149,7 @@ func (dp *deploymentProcessor) getResourceRenderer(id resources.ID) (renderers.R // Deploys rendered output resources in order of dependencies // returns updated outputresource properties and computed values -func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources.ID, rendererOutput renderers.RendererOutput) (DeploymentOutput, error) { +func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources.ID, rendererOutput renderers.RendererOutput) (rpv1.DeploymentOutput, error) { logger := logr.FromContextOrDiscard(ctx).WithValues(logging.LogFieldResourceID, resourceID.String()) // Deploy logger.Info("Deploying radius resource") @@ -166,7 +158,7 @@ func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources. if rendererOutput.RecipeData.Name != "" { deployedRecipeResourceIDs, err := dp.appmodel.GetRecipeModel().RecipeHandler.DeployRecipe(ctx, rendererOutput.RecipeData.RecipeProperties, rendererOutput.EnvironmentProviders, rendererOutput.RecipeContext) if err != nil { - return DeploymentOutput{}, err + return rpv1.DeploymentOutput{}, err } rendererOutput.RecipeData.Resources = deployedRecipeResourceIDs } @@ -174,7 +166,7 @@ func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources. // Order output resources in deployment dependency order orderedOutputResources, err := rpv1.OrderOutputResources(rendererOutput.Resources) if err != nil { - return DeploymentOutput{}, err + return rpv1.DeploymentOutput{}, err } // Recipe based links - Add deployed recipe resource IDs to output resource; Validate that the resource exists by doing a GET on the resource; Populate expected computed values from response of the GET request. @@ -183,12 +175,15 @@ func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources. updatedOutputResources := []rpv1.OutputResource{} computedValues := make(map[string]any) for _, outputResource := range orderedOutputResources { + if outputResource.IsRadiusManaged() && rendererOutput.RecipeData.Name == "" { + return rpv1.DeploymentOutput{}, fmt.Errorf("resources deployed through recipe must be Radius managed") + } // Add resources deployed by recipe to output resource identity for _, id := range rendererOutput.RecipeData.Resources { if rendererOutput.RecipeData.Provider == resourcemodel.ProviderAzure { parsedID, err := resources.ParseResource(id) if err != nil { - return DeploymentOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("failed to parse id %q of the resource deployed by recipe %q for resource %q: %s", id, rendererOutput.RecipeData.Name, resourceID.String(), err.Error())) + return rpv1.DeploymentOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("failed to parse id %q of the resource deployed by recipe %q for resource %q: %s", id, rendererOutput.RecipeData.Name, resourceID.String(), err.Error())) } if outputResource.ProviderResourceType == parsedID.Type() { @@ -199,7 +194,7 @@ func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources. deployedComputedValues, err := dp.deployOutputResource(ctx, resourceID, &outputResource, rendererOutput) if err != nil { - return DeploymentOutput{}, err + return rpv1.DeploymentOutput{}, err } updatedOutputResources = append(updatedOutputResources, outputResource) @@ -218,11 +213,11 @@ func (dp *deploymentProcessor) Deploy(ctx context.Context, resourceID resources. } } - return DeploymentOutput{ - Resources: updatedOutputResources, - ComputedValues: computedValues, - SecretValues: rendererOutput.SecretValues, - RecipeData: rendererOutput.RecipeData, + return rpv1.DeploymentOutput{ + DeployedOutputResources: updatedOutputResources, + ComputedValues: computedValues, + SecretValues: rendererOutput.SecretValues, + RecipeData: rendererOutput.RecipeData, }, nil } @@ -278,10 +273,10 @@ func (dp *deploymentProcessor) deployOutputResource(ctx context.Context, id reso return computedValues, nil } -func (dp *deploymentProcessor) Delete(ctx context.Context, resourceData ResourceData) error { - logger := logr.FromContextOrDiscard(ctx).WithValues(logging.LogFieldResourceID, resourceData.ID) +func (dp *deploymentProcessor) Delete(ctx context.Context, id resources.ID, outputResources []rpv1.OutputResource) error { + logger := logr.FromContextOrDiscard(ctx).WithValues(logging.LogFieldResourceID, id) - orderedOutputResources, err := rpv1.OrderOutputResources(resourceData.OutputResources) + orderedOutputResources, err := rpv1.OrderOutputResources(outputResources) if err != nil { return err } @@ -290,10 +285,6 @@ func (dp *deploymentProcessor) Delete(ctx context.Context, resourceData Resource for i := len(orderedOutputResources) - 1; i >= 0; i-- { outputResource := orderedOutputResources[i] logger.Info(fmt.Sprintf("Deleting output resource: %v, LocalID: %s, resource type: %s\n", outputResource.Identity, outputResource.LocalID, outputResource.ResourceType.Type)) - if resourceData.RecipeData.Name != "" && !outputResource.IsRadiusManaged() { - // If the resource is not Radius managed for a link tied to a recipe, then this is a bug in the output resource initialization in renderer - return fmt.Errorf("resources deployed through recipe must be Radius managed") - } outputResourceModel, err := dp.appmodel.LookupOutputResourceModel(outputResource.ResourceType) if err != nil { return err @@ -337,7 +328,7 @@ func (dp *deploymentProcessor) FetchSecrets(ctx context.Context, resourceData Re return secretValues, nil } -func (dp *deploymentProcessor) fetchSecret(ctx context.Context, outputResources []rpv1.OutputResource, reference rpv1.SecretValueReference, recipeData datamodel.RecipeData) (any, error) { +func (dp *deploymentProcessor) fetchSecret(ctx context.Context, outputResources []rpv1.OutputResource, reference rpv1.SecretValueReference, recipeData linkrp.RecipeData) (any, error) { if reference.Value != "" { // The secret reference contains the value itself return reference.Value, nil @@ -359,7 +350,7 @@ func (dp *deploymentProcessor) fetchSecret(ctx context.Context, outputResources } // getMetadataFromResource returns the environment id and the recipe name to look up environment metadata -func (dp *deploymentProcessor) getMetadataFromResource(ctx context.Context, resourceID resources.ID, resource v1.DataModelInterface) (basicResource *rpv1.BasicResourceProperties, recipe datamodel.LinkRecipe, err error) { +func (dp *deploymentProcessor) getMetadataFromResource(ctx context.Context, resourceID resources.ID, resource v1.DataModelInterface) (basicResource *rpv1.BasicResourceProperties, recipe linkrp.LinkRecipe, err error) { resourceType := strings.ToLower(resourceID.Type()) switch resourceType { case strings.ToLower(linkrp.MongoDatabasesResourceType): diff --git a/pkg/linkrp/frontend/deployment/deploymentprocessor_test.go b/pkg/linkrp/frontend/deployment/deploymentprocessor_test.go index 7edac47819..0b87bd4fcb 100644 --- a/pkg/linkrp/frontend/deployment/deploymentprocessor_test.go +++ b/pkg/linkrp/frontend/deployment/deploymentprocessor_test.go @@ -87,7 +87,7 @@ func buildInputResourceMongo(mode string) (testResource datamodel.MongoDatabase) if mode == modeResource { testResource.Properties.Resource = cosmosMongoID } else if mode == modeRecipe { - testResource.Properties.Recipe = datamodel.LinkRecipe{ + testResource.Properties.Recipe = linkrp.LinkRecipe{ Name: recipeName, Parameters: recipeParams, } @@ -194,11 +194,11 @@ func buildRendererOutputMongo(mode string) (rendererOutput renderers.RendererOut } } - recipeData := datamodel.RecipeData{} + recipeData := linkrp.RecipeData{} if mode == modeRecipe { - recipeData = datamodel.RecipeData{ - RecipeProperties: datamodel.RecipeProperties{ - LinkRecipe: datamodel.LinkRecipe{ + recipeData = linkrp.RecipeData{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: recipeName, Parameters: recipeParams, }, @@ -413,24 +413,24 @@ func Test_Render(t *testing.T) { require.NoError(t, err) env, err := resources.ParseResource(testResource.Properties.Environment) require.NoError(t, err) - testRendererOutput.RecipeContext = datamodel.RecipeContext{ - Resource: datamodel.Resource{ - ResourceInfo: datamodel.ResourceInfo{ + testRendererOutput.RecipeContext = linkrp.RecipeContext{ + Resource: linkrp.Resource{ + ResourceInfo: linkrp.ResourceInfo{ ID: testResource.ID, Name: testResource.Name, }, Type: testResource.Type, }, - Application: datamodel.ResourceInfo{ + Application: linkrp.ResourceInfo{ ID: testResource.Properties.Application, Name: app.Name(), }, - Environment: datamodel.ResourceInfo{ + Environment: linkrp.ResourceInfo{ ID: testResource.Properties.Environment, Name: env.Name(), }, - Runtime: datamodel.Runtime{ - Kubernetes: datamodel.Kubernetes{ + Runtime: linkrp.Runtime{ + Kubernetes: linkrp.Kubernetes{ Namespace: "radius-test-app", EnvironmentNamespace: "radius-test-env", }, @@ -458,20 +458,20 @@ func Test_Render(t *testing.T) { testRendererOutput := buildRendererOutputMongo(modeResource) env, err := resources.ParseResource(testResource.Properties.Environment) require.NoError(t, err) - testRendererOutput.RecipeContext = datamodel.RecipeContext{ - Resource: datamodel.Resource{ - ResourceInfo: datamodel.ResourceInfo{ + testRendererOutput.RecipeContext = linkrp.RecipeContext{ + Resource: linkrp.Resource{ + ResourceInfo: linkrp.ResourceInfo{ ID: testResource.ID, Name: testResource.Name, }, Type: testResource.Type, }, - Environment: datamodel.ResourceInfo{ + Environment: linkrp.ResourceInfo{ ID: testResource.Properties.Environment, Name: env.Name(), }, - Runtime: datamodel.Runtime{ - Kubernetes: datamodel.Kubernetes{ + Runtime: linkrp.Runtime{ + Kubernetes: linkrp.Kubernetes{ Namespace: "radius-test-env", EnvironmentNamespace: "radius-test-env", }, @@ -713,9 +713,9 @@ func Test_Deploy(t *testing.T) { deploymentOutput, err := dp.Deploy(ctx, mongoLinkResourceID, testRendererOutput) require.NoError(t, err) - require.Equal(t, len(testRendererOutput.Resources), len(deploymentOutput.Resources)) - require.NotEqual(t, resourcemodel.ResourceIdentity{}, deploymentOutput.Resources[0].Identity) - require.NotEqual(t, resourcemodel.ResourceIdentity{}, deploymentOutput.Resources[1].Identity) + require.Equal(t, len(testRendererOutput.Resources), len(deploymentOutput.DeployedOutputResources)) + require.NotEqual(t, resourcemodel.ResourceIdentity{}, deploymentOutput.DeployedOutputResources[0].Identity) + require.NotEqual(t, resourcemodel.ResourceIdentity{}, deploymentOutput.DeployedOutputResources[1].Identity) require.Equal(t, testRendererOutput.SecretValues, deploymentOutput.SecretValues) require.Equal(t, map[string]any{renderers.DatabaseNameValue: "test-database", renderers.Host: testRendererOutput.ComputedValues[renderers.Host].Value}, deploymentOutput.ComputedValues) }) @@ -853,7 +853,7 @@ func Test_DeployRenderedResources_ComputedValues(t *testing.T) { "test-key3": "jsonpointer-value", } require.Equal(t, expected, deploymentOutput.ComputedValues) - require.Equal(t, expectedCosmosAccountIdentity, deploymentOutput.Resources[0].Identity) + require.Equal(t, expectedCosmosAccountIdentity, deploymentOutput.DeployedOutputResources[0].Identity) } func Test_Deploy_InvalidComputedValues(t *testing.T) { @@ -937,33 +937,25 @@ func Test_Delete(t *testing.T) { dp := deploymentProcessor{mocks.model, mocks.storageProvider, mocks.secretsValueClient, nil} testOutputResources := buildOutputResourcesMongo(modeRecipe) - testResourceData := ResourceData{ - ID: mongoLinkResourceID, - OutputResources: testOutputResources, - } t.Run("Verify deletion for mode resource", func(t *testing.T) { outputResources := buildOutputResourcesMongo(modeResource) - resourceData := ResourceData{ - ID: mongoLinkResourceID, - OutputResources: outputResources, - } mocks.resourceHandler.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(2).Return(nil) - err := dp.Delete(ctx, resourceData) + err := dp.Delete(ctx, mongoLinkResourceID, outputResources) require.NoError(t, err) }) t.Run("Verify delete success with recipe resources", func(t *testing.T) { mocks.resourceHandler.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(2).Return(nil) - err := dp.Delete(ctx, testResourceData) + err := dp.Delete(ctx, mongoLinkResourceID, testOutputResources) require.NoError(t, err) }) t.Run("Verify delete failure", func(t *testing.T) { mocks.resourceHandler.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("failed to delete the resource")) - err := dp.Delete(ctx, testResourceData) + err := dp.Delete(ctx, mongoLinkResourceID, testOutputResources) require.Error(t, err) }) @@ -989,12 +981,8 @@ func Test_Delete(t *testing.T) { }, }, } - resourceData := ResourceData{ - OutputResources: outputResources, - ID: mongoLinkResourceID, - } - err := dp.Delete(ctx, resourceData) + err := dp.Delete(ctx, mongoLinkResourceID, outputResources) require.Error(t, err) require.Equal(t, "missing localID for outputresource", err.Error()) }) @@ -1009,11 +997,7 @@ func Test_Delete(t *testing.T) { }, }, } - resourceData := ResourceData{ - OutputResources: outputResources, - ID: mongoLinkResourceID, - } - err := dp.Delete(ctx, resourceData) + err := dp.Delete(ctx, mongoLinkResourceID, outputResources) require.Error(t, err) require.Equal(t, "output resource kind 'Provider: azure, Type: foo' is unsupported", err.Error()) }) @@ -1026,22 +1010,18 @@ func Test_Delete_Dapr(t *testing.T) { daprLinkResourceID := getResourceID(daprLinkID) testOutputResources := buildOutputResourcesDapr(modeResource) - testResourceData := ResourceData{ - ID: daprLinkResourceID, - OutputResources: testOutputResources, - } t.Run("Verify handler delete is invoked", func(t *testing.T) { mocks.resourceHandler.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(nil) - err := dp.Delete(ctx, testResourceData) + err := dp.Delete(ctx, daprLinkResourceID, testOutputResources) require.NoError(t, err) }) t.Run("Verify delete failure", func(t *testing.T) { mocks.resourceHandler.EXPECT().Delete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("failed to delete the resource")) - err := dp.Delete(ctx, testResourceData) + err := dp.Delete(ctx, daprLinkResourceID, testOutputResources) require.Error(t, err) }) } diff --git a/pkg/linkrp/frontend/deployment/mock_deploymentprocessor.go b/pkg/linkrp/frontend/deployment/mock_deploymentprocessor.go index 9d0283bcda..63c354a232 100644 --- a/pkg/linkrp/frontend/deployment/mock_deploymentprocessor.go +++ b/pkg/linkrp/frontend/deployment/mock_deploymentprocessor.go @@ -11,6 +11,7 @@ import ( gomock "github.com/golang/mock/gomock" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" renderers "github.com/project-radius/radius/pkg/linkrp/renderers" + v10 "github.com/project-radius/radius/pkg/rp/v1" resources "github.com/project-radius/radius/pkg/ucp/resources" ) @@ -38,24 +39,24 @@ func (m *MockDeploymentProcessor) EXPECT() *MockDeploymentProcessorMockRecorder } // Delete mocks base method. -func (m *MockDeploymentProcessor) Delete(arg0 context.Context, arg1 ResourceData) error { +func (m *MockDeploymentProcessor) Delete(arg0 context.Context, arg1 resources.ID, arg2 []v10.OutputResource) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. -func (mr *MockDeploymentProcessorMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockDeploymentProcessorMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDeploymentProcessor)(nil).Delete), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDeploymentProcessor)(nil).Delete), arg0, arg1, arg2) } // Deploy mocks base method. -func (m *MockDeploymentProcessor) Deploy(arg0 context.Context, arg1 resources.ID, arg2 renderers.RendererOutput) (DeploymentOutput, error) { +func (m *MockDeploymentProcessor) Deploy(arg0 context.Context, arg1 resources.ID, arg2 renderers.RendererOutput) (v10.DeploymentOutput, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Deploy", arg0, arg1, arg2) - ret0, _ := ret[0].(DeploymentOutput) + ret0, _ := ret[0].(v10.DeploymentOutput) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/linkrp/frontend/handler/routes.go b/pkg/linkrp/frontend/handler/routes.go index 601f7dc34c..9a4ecf8b63 100644 --- a/pkg/linkrp/frontend/handler/routes.go +++ b/pkg/linkrp/frontend/handler/routes.go @@ -7,6 +7,7 @@ package handler import ( "context" + "time" "github.com/gorilla/mux" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" @@ -14,6 +15,7 @@ import ( "github.com/project-radius/radius/pkg/armrpc/frontend/defaultoperation" "github.com/project-radius/radius/pkg/armrpc/frontend/server" "github.com/project-radius/radius/pkg/linkrp" + rp_frontend "github.com/project-radius/radius/pkg/rp/frontend" "github.com/project-radius/radius/pkg/validator" "github.com/project-radius/radius/swagger" @@ -115,7 +117,16 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.MongoDatabasesResourceType, Method: v1.OperationPut, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return mongo_ctrl.NewCreateOrUpdateMongoDatabase(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncPut(opt, + frontend_ctrl.ResourceOptions[datamodel.MongoDatabase]{ + RequestConverter: converter.MongoDatabaseDataModelFromVersioned, + ResponseConverter: converter.MongoDatabaseDataModelToVersioned, + UpdateFilters: []frontend_ctrl.UpdateFilter[datamodel.MongoDatabase]{ + rp_frontend.PrepareRadiusResource[*datamodel.MongoDatabase], + }, + AsyncOperationTimeout: time.Duration(10) * time.Minute, + }, + ) }, }, { @@ -123,7 +134,16 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.MongoDatabasesResourceType, Method: v1.OperationPatch, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return mongo_ctrl.NewCreateOrUpdateMongoDatabase(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncPut(opt, + frontend_ctrl.ResourceOptions[datamodel.MongoDatabase]{ + RequestConverter: converter.MongoDatabaseDataModelFromVersioned, + ResponseConverter: converter.MongoDatabaseDataModelToVersioned, + UpdateFilters: []frontend_ctrl.UpdateFilter[datamodel.MongoDatabase]{ + rp_frontend.PrepareRadiusResource[*datamodel.MongoDatabase], + }, + AsyncOperationTimeout: time.Duration(8) * time.Minute, + }, + ) }, }, { @@ -131,7 +151,13 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.MongoDatabasesResourceType, Method: v1.OperationDelete, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return mongo_ctrl.NewDeleteMongoDatabase(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncDelete(opt, + frontend_ctrl.ResourceOptions[datamodel.MongoDatabase]{ + RequestConverter: converter.MongoDatabaseDataModelFromVersioned, + ResponseConverter: converter.MongoDatabaseDataModelToVersioned, + AsyncOperationTimeout: time.Duration(15) * time.Minute, + }, + ) }, }, { @@ -361,7 +387,16 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.RedisCachesResourceType, Method: v1.OperationPut, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return redis_ctrl.NewCreateOrUpdateRedisCache(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncPut(opt, + frontend_ctrl.ResourceOptions[datamodel.RedisCache]{ + RequestConverter: converter.RedisCacheDataModelFromVersioned, + ResponseConverter: converter.RedisCacheDataModelToVersioned, + UpdateFilters: []frontend_ctrl.UpdateFilter[datamodel.RedisCache]{ + rp_frontend.PrepareRadiusResource[*datamodel.RedisCache], + }, + AsyncOperationTimeout: time.Duration(10) * time.Minute, + }, + ) }, }, { @@ -369,7 +404,16 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.RedisCachesResourceType, Method: v1.OperationPatch, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return redis_ctrl.NewCreateOrUpdateRedisCache(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncPut(opt, + frontend_ctrl.ResourceOptions[datamodel.RedisCache]{ + RequestConverter: converter.RedisCacheDataModelFromVersioned, + ResponseConverter: converter.RedisCacheDataModelToVersioned, + UpdateFilters: []frontend_ctrl.UpdateFilter[datamodel.RedisCache]{ + rp_frontend.PrepareRadiusResource[*datamodel.RedisCache], + }, + AsyncOperationTimeout: time.Duration(10) * time.Minute, + }, + ) }, }, { @@ -377,7 +421,13 @@ func AddRoutes(ctx context.Context, router *mux.Router, pathBase string, isARM b ResourceType: linkrp.RedisCachesResourceType, Method: v1.OperationDelete, HandlerFactory: func(opt frontend_ctrl.Options) (frontend_ctrl.Controller, error) { - return redis_ctrl.NewDeleteRedisCache(link_frontend_ctrl.Options{Options: opt, DeployProcessor: dp}) + return defaultoperation.NewDefaultAsyncDelete(opt, + frontend_ctrl.ResourceOptions[datamodel.RedisCache]{ + RequestConverter: converter.RedisCacheDataModelFromVersioned, + ResponseConverter: converter.RedisCacheDataModelToVersioned, + AsyncOperationTimeout: time.Duration(10) * time.Minute, + }, + ) }, }, { diff --git a/pkg/linkrp/handlers/mock_recipe_handler.go b/pkg/linkrp/handlers/mock_recipe_handler.go index aebdc22cb8..172e8f084c 100644 --- a/pkg/linkrp/handlers/mock_recipe_handler.go +++ b/pkg/linkrp/handlers/mock_recipe_handler.go @@ -10,7 +10,7 @@ import ( gomock "github.com/golang/mock/gomock" datamodel "github.com/project-radius/radius/pkg/corerp/datamodel" - datamodel0 "github.com/project-radius/radius/pkg/linkrp/datamodel" + linkrp "github.com/project-radius/radius/pkg/linkrp" ) // MockRecipeHandler is a mock of RecipeHandler interface. @@ -37,7 +37,7 @@ func (m *MockRecipeHandler) EXPECT() *MockRecipeHandlerMockRecorder { } // DeployRecipe mocks base method. -func (m *MockRecipeHandler) DeployRecipe(arg0 context.Context, arg1 datamodel0.RecipeProperties, arg2 datamodel.Providers, arg3 datamodel0.RecipeContext) ([]string, error) { +func (m *MockRecipeHandler) DeployRecipe(arg0 context.Context, arg1 linkrp.RecipeProperties, arg2 datamodel.Providers, arg3 linkrp.RecipeContext) ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeployRecipe", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]string) diff --git a/pkg/linkrp/handlers/recipe_handler.go b/pkg/linkrp/handlers/recipe_handler.go index 90905c12e2..673546c7ef 100644 --- a/pkg/linkrp/handlers/recipe_handler.go +++ b/pkg/linkrp/handlers/recipe_handler.go @@ -20,6 +20,7 @@ import ( "github.com/project-radius/radius/pkg/azure/armauth" "github.com/project-radius/radius/pkg/azure/clientv2" coreDatamodel "github.com/project-radius/radius/pkg/corerp/datamodel" + "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/linkrp/datamodel" "github.com/project-radius/radius/pkg/logging" "oras.land/oras-go/v2/content" @@ -32,7 +33,7 @@ const deploymentPrefix = "recipe" // //go:generate mockgen -destination=./mock_recipe_handler.go -package=handlers -self_package github.com/project-radius/radius/pkg/linkrp/handlers github.com/project-radius/radius/pkg/linkrp/handlers RecipeHandler type RecipeHandler interface { - DeployRecipe(ctx context.Context, recipe datamodel.RecipeProperties, envProviders coreDatamodel.Providers, recipeContext datamodel.RecipeContext) ([]string, error) + DeployRecipe(ctx context.Context, recipe linkrp.RecipeProperties, envProviders coreDatamodel.Providers, recipeContext linkrp.RecipeContext) ([]string, error) } func NewRecipeHandler(arm *armauth.ArmConfig) RecipeHandler { @@ -48,7 +49,7 @@ type azureRecipeHandler struct { // DeployRecipe deploys the recipe template fetched from the provided recipe TemplatePath using the providers scope. // Currently the implementation assumes TemplatePath is location of an ARM JSON template in Azure Container Registry. // Returns resource IDs of the resources deployed by the template -func (handler *azureRecipeHandler) DeployRecipe(ctx context.Context, recipe datamodel.RecipeProperties, envProviders coreDatamodel.Providers, recipeContext datamodel.RecipeContext) (deployedResources []string, err error) { +func (handler *azureRecipeHandler) DeployRecipe(ctx context.Context, recipe linkrp.RecipeProperties, envProviders coreDatamodel.Providers, recipeContext linkrp.RecipeContext) (deployedResources []string, err error) { if recipe.TemplatePath == "" { return nil, fmt.Errorf("recipe template path cannot be empty") } diff --git a/pkg/linkrp/handlers/recipe_handler_test.go b/pkg/linkrp/handlers/recipe_handler_test.go index 75008de5af..b0d452769e 100644 --- a/pkg/linkrp/handlers/recipe_handler_test.go +++ b/pkg/linkrp/handlers/recipe_handler_test.go @@ -8,7 +8,7 @@ package handlers import ( "testing" - "github.com/project-radius/radius/pkg/linkrp/datamodel" + "github.com/project-radius/radius/pkg/linkrp" "github.com/stretchr/testify/require" ) @@ -44,24 +44,24 @@ func Test_ParameterConflict(t *testing.T) { func Test_ContextParameter(t *testing.T) { linkID := "/subscriptions/testSub/resourceGroups/testGroup/providers/applications.link/mongodatabases/mongo0" - expectedLinkContext := datamodel.RecipeContext{ - Resource: datamodel.Resource{ - ResourceInfo: datamodel.ResourceInfo{ + expectedLinkContext := linkrp.RecipeContext{ + Resource: linkrp.Resource{ + ResourceInfo: linkrp.ResourceInfo{ ID: "/subscriptions/testSub/resourceGroups/testGroup/providers/applications.link/mongodatabases/mongo0", Name: "mongo0", }, Type: "applications.link/mongodatabases", }, - Application: datamodel.ResourceInfo{ + Application: linkrp.ResourceInfo{ Name: "testApplication", ID: "/subscriptions/test-sub/resourceGroups/test-group/providers/Applications.Core/applications/testApplication", }, - Environment: datamodel.ResourceInfo{ + Environment: linkrp.ResourceInfo{ Name: "env0", ID: "/subscriptions/test-sub/resourceGroups/test-group/providers/Applications.Core/environments/env0", }, - Runtime: datamodel.Runtime{ - Kubernetes: datamodel.Kubernetes{ + Runtime: linkrp.Runtime{ + Kubernetes: linkrp.Kubernetes{ Namespace: "radius-test-app", EnvironmentNamespace: "radius-test-env", }, @@ -79,24 +79,24 @@ func Test_DevParameterWithContextParameter(t *testing.T) { "port": 2030, "name": "test-parameters", } - recipeContext := datamodel.RecipeContext{ - Resource: datamodel.Resource{ - ResourceInfo: datamodel.ResourceInfo{ + recipeContext := linkrp.RecipeContext{ + Resource: linkrp.Resource{ + ResourceInfo: linkrp.ResourceInfo{ ID: "/subscriptions/testSub/resourceGroups/testGroup/providers/applications.link/mongodatabases/mongo0", Name: "mongo0", }, Type: "Applications.Link/mongoDatabases", }, - Application: datamodel.ResourceInfo{ + Application: linkrp.ResourceInfo{ ID: "/subscriptions/test-sub/resourceGroups/test-group/providers/Applications.Core/applications/testApplication", Name: "testApplication", }, - Environment: datamodel.ResourceInfo{ + Environment: linkrp.ResourceInfo{ ID: "/subscriptions/test-sub/resourceGroups/test-group/providers/Applications.Core/environments/env0", Name: "env0", }, - Runtime: datamodel.Runtime{ - Kubernetes: datamodel.Kubernetes{ + Runtime: linkrp.Runtime{ + Kubernetes: linkrp.Kubernetes{ EnvironmentNamespace: "radius-test-env", Namespace: "radius-test-app", }, diff --git a/pkg/linkrp/handlers/recipe_helper.go b/pkg/linkrp/handlers/recipe_helper.go index 8c747037bf..b3a25ab394 100644 --- a/pkg/linkrp/handlers/recipe_helper.go +++ b/pkg/linkrp/handlers/recipe_helper.go @@ -9,13 +9,13 @@ import ( "fmt" dockerParser "github.com/novln/docker-parser" - "github.com/project-radius/radius/pkg/linkrp/datamodel" + "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/ucp/resources" ) // CreateRecipeContextParameter creates the context parameter for the recipe with the link, environment and application info -func CreateRecipeContextParameter(resourceID, environmentID, environmentNamespace, applicationID, applicationNamespace string) (*datamodel.RecipeContext, error) { - recipeContext := datamodel.RecipeContext{} +func CreateRecipeContextParameter(resourceID, environmentID, environmentNamespace, applicationID, applicationNamespace string) (*linkrp.RecipeContext, error) { + recipeContext := linkrp.RecipeContext{} parsedLink, err := resources.ParseResource(resourceID) if err != nil { @@ -48,7 +48,7 @@ func CreateRecipeContextParameter(resourceID, environmentID, environmentNamespac // createRecipeParameters creates the parameters to be passed for recipe deployment after handling conflicts in parameters set by operator and developer. // In case of conflict the developer parameter takes precedence. If recipe has context parameter defined adds the context information to the parameters list -func createRecipeParameters(devParams, operatorParams map[string]any, isCxtSet bool, recipeContext *datamodel.RecipeContext) map[string]any { +func createRecipeParameters(devParams, operatorParams map[string]any, isCxtSet bool, recipeContext *linkrp.RecipeContext) map[string]any { parameters := map[string]any{} for k, v := range operatorParams { if _, ok := devParams[k]; !ok { diff --git a/pkg/linkrp/renderers/daprstatestores/recipe.go b/pkg/linkrp/renderers/daprstatestores/recipe.go index c6bb5bb048..6bbd5be0ff 100644 --- a/pkg/linkrp/renderers/daprstatestores/recipe.go +++ b/pkg/linkrp/renderers/daprstatestores/recipe.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/go-autorest/autorest/to" "github.com/project-radius/radius/pkg/azure/azresources" "github.com/project-radius/radius/pkg/azure/clientv2" + "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/linkrp/datamodel" "github.com/project-radius/radius/pkg/linkrp/handlers" "github.com/project-radius/radius/pkg/linkrp/renderers" @@ -28,7 +29,7 @@ func GetDaprStateStoreRecipe(resource *datamodel.DaprStateStore, applicationName if err != nil { return renderers.RendererOutput{}, err } - recipeData := datamodel.RecipeData{ + recipeData := linkrp.RecipeData{ Provider: resourcemodel.ProviderAzure, RecipeProperties: options.RecipeProperties, APIVersion: clientv2.StateStoreClientAPIVersion, diff --git a/pkg/linkrp/renderers/daprstatestores/renderer_test.go b/pkg/linkrp/renderers/daprstatestores/renderer_test.go index de1a3ca9f1..6fb14c2e2c 100644 --- a/pkg/linkrp/renderers/daprstatestores/renderer_test.go +++ b/pkg/linkrp/renderers/daprstatestores/renderer_test.go @@ -362,7 +362,7 @@ func Test_Render_Recipe_Success(t *testing.T) { Environment: environmentID, }, Mode: datamodel.LinkModeRecipe, - Recipe: datamodel.LinkRecipe{ + Recipe: linkrp.LinkRecipe{ Name: "daprstatestores", }, }, @@ -370,8 +370,8 @@ func Test_Render_Recipe_Success(t *testing.T) { renderer.StateStores = SupportedStateStoreModes result, err := renderer.Render(context.Background(), &resource, renderers.RenderOptions{ Namespace: "radius-test", - RecipeProperties: datamodel.RecipeProperties{ - LinkRecipe: datamodel.LinkRecipe{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: "daprstatestores", }, TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/daprstatestores:v1", @@ -421,7 +421,7 @@ func Test_Render_Recipe_InvalidLinkType(t *testing.T) { Environment: environmentID, }, Mode: datamodel.LinkModeRecipe, - Recipe: datamodel.LinkRecipe{ + Recipe: linkrp.LinkRecipe{ Name: "daprstatestores", }, }, @@ -429,8 +429,8 @@ func Test_Render_Recipe_InvalidLinkType(t *testing.T) { renderer.StateStores = SupportedStateStoreModes _, err := renderer.Render(context.Background(), &resource, renderers.RenderOptions{ Namespace: "radius-test", - RecipeProperties: datamodel.RecipeProperties{ - LinkRecipe: datamodel.LinkRecipe{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: "daprstatestores", }, TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/daprstatestores:v1", diff --git a/pkg/linkrp/renderers/mongodatabases/renderer.go b/pkg/linkrp/renderers/mongodatabases/renderer.go index b798a8356a..8b3dc202e4 100644 --- a/pkg/linkrp/renderers/mongodatabases/renderer.go +++ b/pkg/linkrp/renderers/mongodatabases/renderer.go @@ -13,6 +13,7 @@ import ( v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" "github.com/project-radius/radius/pkg/azure/azresources" "github.com/project-radius/radius/pkg/azure/clientv2" + "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/linkrp/datamodel" "github.com/project-radius/radius/pkg/linkrp/renderers" "github.com/project-radius/radius/pkg/resourcekinds" @@ -73,7 +74,7 @@ func RenderAzureRecipe(resource *datamodel.MongoDatabase, options renderers.Rend return renderers.RendererOutput{}, err } - recipeData := datamodel.RecipeData{ + recipeData := linkrp.RecipeData{ Provider: resourcemodel.ProviderAzure, RecipeProperties: options.RecipeProperties, APIVersion: clientv2.DocumentDBManagementClientAPIVersion, diff --git a/pkg/linkrp/renderers/mongodatabases/renderer_test.go b/pkg/linkrp/renderers/mongodatabases/renderer_test.go index 63700411b9..f26960bfdd 100644 --- a/pkg/linkrp/renderers/mongodatabases/renderer_test.go +++ b/pkg/linkrp/renderers/mongodatabases/renderer_test.go @@ -349,7 +349,7 @@ func Test_Render_Recipe_Success(t *testing.T) { }, Mode: datamodel.LinkModeRecipe, MongoDatabaseRecipeProperties: datamodel.MongoDatabaseRecipeProperties{ - Recipe: datamodel.LinkRecipe{ + Recipe: linkrp.LinkRecipe{ Name: "mongodb", Parameters: map[string]any{ "throughput": 400, @@ -389,8 +389,8 @@ func Test_Render_Recipe_Success(t *testing.T) { } output, err := renderer.Render(ctx, &mongoDBResource, renderers.RenderOptions{ - RecipeProperties: datamodel.RecipeProperties{ - LinkRecipe: datamodel.LinkRecipe{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: "mongodb", Parameters: map[string]any{ "throughput": 400, @@ -442,7 +442,7 @@ func Test_Render_Recipe_InvalidLinkType(t *testing.T) { }, Mode: datamodel.LinkModeRecipe, MongoDatabaseRecipeProperties: datamodel.MongoDatabaseRecipeProperties{ - Recipe: datamodel.LinkRecipe{ + Recipe: linkrp.LinkRecipe{ Name: "mongodb", }, }, @@ -450,8 +450,8 @@ func Test_Render_Recipe_InvalidLinkType(t *testing.T) { } _, err := renderer.Render(ctx, &mongoDBResource, renderers.RenderOptions{ - RecipeProperties: datamodel.RecipeProperties{ - LinkRecipe: datamodel.LinkRecipe{ + RecipeProperties: linkrp.RecipeProperties{ + LinkRecipe: linkrp.LinkRecipe{ Name: "mongodb", }, TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1", diff --git a/pkg/linkrp/renderers/types.go b/pkg/linkrp/renderers/types.go index 3e1ec05483..7dc02377e2 100644 --- a/pkg/linkrp/renderers/types.go +++ b/pkg/linkrp/renderers/types.go @@ -11,7 +11,7 @@ import ( v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" coreDatamodel "github.com/project-radius/radius/pkg/corerp/datamodel" - "github.com/project-radius/radius/pkg/linkrp/datamodel" + "github.com/project-radius/radius/pkg/linkrp" rpv1 "github.com/project-radius/radius/pkg/rp/v1" ) @@ -36,7 +36,7 @@ type Renderer interface { } type RenderOptions struct { Namespace string - RecipeProperties datamodel.RecipeProperties + RecipeProperties linkrp.RecipeProperties EnvironmentProviders coreDatamodel.Providers } @@ -44,11 +44,11 @@ type RendererOutput struct { Resources []rpv1.OutputResource ComputedValues map[string]ComputedValueReference SecretValues map[string]rpv1.SecretValueReference - RecipeData datamodel.RecipeData + RecipeData linkrp.RecipeData // EnvironmentProviders specifies the providers mapped to the linked environment needed to deploy the recipe EnvironmentProviders coreDatamodel.Providers // RecipeContext specifies the context parameters for the recipe deployment - RecipeContext datamodel.RecipeContext + RecipeContext linkrp.RecipeContext } // ComputedValueReference represents a non-secret value that can accessed once the output resources diff --git a/pkg/linkrp/types.go b/pkg/linkrp/types.go index 81577c06f1..6cd9c397c2 100644 --- a/pkg/linkrp/types.go +++ b/pkg/linkrp/types.go @@ -2,8 +2,71 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // ------------------------------------------------------------ + package linkrp +type RecipeData struct { + RecipeProperties + + Provider string + + // API version to use to perform operations on resources supported by the link. + // For example for Azure resources, every service has different REST API version that must be specified in the request. + APIVersion string + + // Resource ids of the resources deployed by the recipe + Resources []string +} + +// RecipeProperties represents the information needed to deploy a recipe +type RecipeProperties struct { + LinkRecipe // LinkRecipe is the recipe of the resource to be deployed + LinkType string // LinkType represent the type of the link + TemplatePath string // TemplatePath represent the recipe location + EnvParameters map[string]any // EnvParameters represents the parameters set by the operator while linking the recipe to an environment +} + +// LinkRecipe is the recipe details used to automatically deploy underlying infrastructure for a link +type LinkRecipe struct { + // Name of the recipe within the environment to use + Name string `json:"name,omitempty"` + // Parameters are key/value parameters to pass into the recipe at deployment + Parameters map[string]any `json:"parameters,omitempty"` +} + +// RecipeContext Recipe template authors can leverage the RecipeContext parameter to access Link properties to +// generate name and properties that are unique for the Link calling the recipe. +type RecipeContext struct { + Resource Resource `json:"resource,omitempty"` + Application ResourceInfo `json:"application,omitempty"` + Environment ResourceInfo `json:"environment,omitempty"` + Runtime Runtime `json:"runtime,omitempty"` +} + +// Resource contains the information needed to deploy a recipe. +// In the case the resource is a Link, it represents the Link's id, name and type. +type Resource struct { + ResourceInfo + Type string `json:"type"` +} + +// ResourceInfo name and id of the resource +type ResourceInfo struct { + Name string `json:"name"` + ID string `json:"id"` +} + +type Runtime struct { + Kubernetes Kubernetes `json:"kubernetes,omitempty"` +} + +type Kubernetes struct { + // Namespace is set to the applicationNamespace when the Link is application-scoped, and set to the environmentNamespace when the Link is environment scoped + Namespace string `json:"namespace"` + // EnvironmentNamespace is set to environment namespace. + EnvironmentNamespace string `json:"environmentNamespace"` +} + const ( DaprInvokeHttpRoutesResourceType = "Applications.Link/daprInvokeHttpRoutes" DaprPubSubBrokersResourceType = "Applications.Link/daprPubSubBrokers" diff --git a/pkg/rp/frontend/resource_test.go b/pkg/rp/frontend/resource_test.go index 38de711061..7f99f7b1d5 100644 --- a/pkg/rp/frontend/resource_test.go +++ b/pkg/rp/frontend/resource_test.go @@ -30,8 +30,9 @@ func (r *TestResourceDataModel) ResourceTypeName() string { } // ApplyDeploymentOutput applies the properties changes based on the deployment output. -func (c *TestResourceDataModel) ApplyDeploymentOutput(do rpv1.DeploymentOutput) { +func (c *TestResourceDataModel) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error { c.Properties.Status.OutputResources = do.DeployedOutputResources + return nil } // OutputResources returns the output resources array. diff --git a/pkg/rp/v1/types.go b/pkg/rp/v1/types.go index f8a6b5f00d..1558b6380a 100644 --- a/pkg/rp/v1/types.go +++ b/pkg/rp/v1/types.go @@ -9,6 +9,7 @@ import ( "strings" v1 "github.com/project-radius/radius/pkg/armrpc/api/v1" + "github.com/project-radius/radius/pkg/linkrp" "github.com/project-radius/radius/pkg/resourcemodel" ) @@ -78,7 +79,7 @@ type KubernetesComputeProperties struct { type RadiusResourceModel interface { v1.ResourceDataModel - ApplyDeploymentOutput(deploymentOutput DeploymentOutput) + ApplyDeploymentOutput(deploymentOutput DeploymentOutput) error OutputResources() []OutputResource ResourceMetadata() *BasicResourceProperties @@ -89,14 +90,15 @@ type DeploymentOutput struct { DeployedOutputResources []OutputResource ComputedValues map[string]any SecretValues map[string]SecretValueReference + RecipeData linkrp.RecipeData } // DeploymentDataModel is the interface that wraps existing data models // and enables us to use in generic deployment backend controllers. type DeploymentDataModel interface { - v1.DataModelInterface + v1.ResourceDataModel - ApplyDeploymentOutput(deploymentOutput DeploymentOutput) + ApplyDeploymentOutput(deploymentOutput DeploymentOutput) error OutputResources() []OutputResource }