diff --git a/cmd/rad/cmd/resourceprovider.go b/cmd/rad/cmd/resourceprovider.go new file mode 100644 index 00000000000..44dc98a4c75 --- /dev/null +++ b/cmd/rad/cmd/resourceprovider.go @@ -0,0 +1,34 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + RootCmd.AddCommand(resourceProviderCmd) + resourceProviderCmd.PersistentFlags().StringP("workspace", "w", "", "The workspace name") +} + +func NewResourceProviderCommand() *cobra.Command { + return &cobra.Command{ + Use: "resourceprovider", + Short: "Manage resource providers", + Long: `Manage resource providers`, + } +} diff --git a/cmd/rad/cmd/resourcetype.go b/cmd/rad/cmd/resourcetype.go new file mode 100644 index 00000000000..41ffeef8820 --- /dev/null +++ b/cmd/rad/cmd/resourcetype.go @@ -0,0 +1,34 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + RootCmd.AddCommand(resourceTypeCmd) + resourceTypeCmd.PersistentFlags().StringP("workspace", "w", "", "The workspace name") +} + +func NewResourceTypeCommand() *cobra.Command { + return &cobra.Command{ + Use: "resourcetype", + Short: "Manage resource types", + Long: `Manage resource types`, + } +} diff --git a/cmd/rad/cmd/root.go b/cmd/rad/cmd/root.go index 1320cb17a59..a52b4dcd8f3 100644 --- a/cmd/rad/cmd/root.go +++ b/cmd/rad/cmd/root.go @@ -53,9 +53,17 @@ import ( recipe_register "github.com/radius-project/radius/pkg/cli/cmd/recipe/register" recipe_show "github.com/radius-project/radius/pkg/cli/cmd/recipe/show" recipe_unregister "github.com/radius-project/radius/pkg/cli/cmd/recipe/unregister" + resource_create "github.com/radius-project/radius/pkg/cli/cmd/resource/create" resource_delete "github.com/radius-project/radius/pkg/cli/cmd/resource/delete" resource_list "github.com/radius-project/radius/pkg/cli/cmd/resource/list" resource_show "github.com/radius-project/radius/pkg/cli/cmd/resource/show" + resourceprovider_create "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/create" + resourceprovider_delete "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/delete" + resourceprovider_list "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/list" + resourceprovider_new "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/new" + resourceprovider_show "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/show" + resourcetype_list "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/list" + resourcetype_show "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/show" "github.com/radius-project/radius/pkg/cli/cmd/run" "github.com/radius-project/radius/pkg/cli/cmd/uninstall" uninstall_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/uninstall/kubernetes" @@ -100,6 +108,8 @@ const ( var applicationCmd = NewAppCommand() var resourceCmd = NewResourceCommand() +var resourceProviderCmd = NewResourceProviderCommand() +var resourceTypeCmd = NewResourceTypeCommand() var recipeCmd = NewRecipeCommand() var envCmd = NewEnvironmentCommand() var workspaceCmd = NewWorkspaceCommand() @@ -211,14 +221,38 @@ func initSubCommands() { runCmd, _ := run.NewCommand(framework) RootCmd.AddCommand(runCmd) - showCmd, _ := resource_show.NewCommand(framework) - resourceCmd.AddCommand(showCmd) + resourceShowCmd, _ := resource_show.NewCommand(framework) + resourceCmd.AddCommand(resourceShowCmd) - listCmd, _ := resource_list.NewCommand(framework) - resourceCmd.AddCommand(listCmd) + resourceListCommand, _ := resource_list.NewCommand(framework) + resourceCmd.AddCommand(resourceListCommand) - deleteCmd, _ := resource_delete.NewCommand(framework) - resourceCmd.AddCommand(deleteCmd) + resourceCreateCmd, _ := resource_create.NewCommand(framework) + resourceCmd.AddCommand(resourceCreateCmd) + + resourceDeleteCmd, _ := resource_delete.NewCommand(framework) + resourceCmd.AddCommand(resourceDeleteCmd) + + resourceProviderNewCmd, _ := resourceprovider_new.NewCommand(framework) + resourceProviderCmd.AddCommand(resourceProviderNewCmd) + + resourceProviderShowCmd, _ := resourceprovider_show.NewCommand(framework) + resourceProviderCmd.AddCommand(resourceProviderShowCmd) + + resourceProviderListCmd, _ := resourceprovider_list.NewCommand(framework) + resourceProviderCmd.AddCommand(resourceProviderListCmd) + + resourceProviderCreateCmd, _ := resourceprovider_create.NewCommand(framework) + resourceProviderCmd.AddCommand(resourceProviderCreateCmd) + + resourceProviderDeleteCmd, _ := resourceprovider_delete.NewCommand(framework) + resourceProviderCmd.AddCommand(resourceProviderDeleteCmd) + + resourceTypeShowCmd, _ := resourcetype_show.NewCommand(framework) + resourceTypeCmd.AddCommand(resourceTypeShowCmd) + + resourceTypeListCmd, _ := resourcetype_list.NewCommand(framework) + resourceTypeCmd.AddCommand(resourceTypeListCmd) listRecipeCmd, _ := recipe_list.NewCommand(framework) recipeCmd.AddCommand(listRecipeCmd) diff --git a/pkg/cli/clients/clients.go b/pkg/cli/clients/clients.go index 281f286c821..1946baf8758 100644 --- a/pkg/cli/clients/clients.go +++ b/pkg/cli/clients/clients.go @@ -155,6 +155,9 @@ type ApplicationsManagementClient interface { // GetResource retrieves a resource by its type and name (or id). GetResource(ctx context.Context, resourceType string, resourceNameOrID string) (generated.GenericResource, error) + // CreateOrUpdateResource creates or updates a resource using its type name (or id). + CreateOrUpdateResource(ctx context.Context, resourceType string, resourceNameOrID string, resource *generated.GenericResource) (generated.GenericResource, error) + // DeleteResource deletes a resource by its type and name (or id). DeleteResource(ctx context.Context, resourceType string, resourceNameOrID string) (bool, error) @@ -205,6 +208,24 @@ type ApplicationsManagementClient interface { // DeleteResourceGroup deletes a resource group by its name. DeleteResourceGroup(ctx context.Context, planeName string, resourceGroupName string) (bool, error) + + // ListResourceProviders lists all resource providers in the configured scope. + ListResourceProviders(ctx context.Context, planeName string) ([]ucp_v20231001preview.ResourceProviderResource, error) + + // GetResourceProvider gets the resource provider with the specified name in the configured scope. + GetResourceProvider(ctx context.Context, planeName string, providerNamespace string) (ucp_v20231001preview.ResourceProviderResource, error) + + // CreateOrUpdateResourceProvider creates or updates a resource provider in the configured scope. + CreateOrUpdateResourceProvider(ctx context.Context, planeName string, providerNamespace string, resource *ucp_v20231001preview.ResourceProviderResource) (ucp_v20231001preview.ResourceProviderResource, error) + + // DeleteResourceProvider deletes a resource provider in the configured scope. + DeleteResourceProvider(ctx context.Context, planeName string, providerNamespace string) (bool, error) + + // ListResourceProviderSummaries list the summary data of all resource providers in the configured scope. + ListResourceProviderSummaries(ctx context.Context, planeName string) ([]ucp_v20231001preview.ResourceProviderSummary, error) + + // GetResourceProviderSummary gets the resource provider summary with the specified name in the configured scope. + GetResourceProviderSummary(ctx context.Context, planeName string, providerNamespace string) (ucp_v20231001preview.ResourceProviderSummary, error) } // ShallowCopy creates a shallow copy of the DeploymentParameters object by iterating through the original object and diff --git a/pkg/cli/clients/management.go b/pkg/cli/clients/management.go index eb689133773..e5ab3394650 100644 --- a/pkg/cli/clients/management.go +++ b/pkg/cli/clients/management.go @@ -48,6 +48,7 @@ type UCPApplicationsManagementClient struct { applicationResourceClientFactory func(scope string) (applicationResourceClient, error) environmentResourceClientFactory func(scope string) (environmentResourceClient, error) resourceGroupClientFactory func() (resourceGroupClient, error) + resourceProviderClientFactory func() (resourceProviderClient, error) capture func(ctx context.Context, capture **http.Response) context.Context } @@ -196,6 +197,31 @@ func (amc *UCPApplicationsManagementClient) GetResource(ctx context.Context, res return getResponse.GenericResource, nil } +// CreateOrUpdateResource creates or updates a resource using its type name (or id). +func (amc *UCPApplicationsManagementClient) CreateOrUpdateResource(ctx context.Context, resourceType string, resourceNameOrID string, resource *generated.GenericResource) (generated.GenericResource, error) { + scope, name, err := amc.extractScopeAndName(resourceNameOrID) + if err != nil { + return generated.GenericResource{}, err + } + + client, err := amc.createGenericClient(scope, resourceType) + if err != nil { + return generated.GenericResource{}, err + } + + poller, err := client.BeginCreateOrUpdate(ctx, name, *resource, &generated.GenericResourcesClientBeginCreateOrUpdateOptions{}) + if err != nil { + return generated.GenericResource{}, err + } + + response, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return generated.GenericResource{}, err + } + + return response.GenericResource, nil +} + // DeleteResource deletes a resource by its type and name (or id). func (amc *UCPApplicationsManagementClient) DeleteResource(ctx context.Context, resourceType string, resourceNameOrID string) (bool, error) { scope, name, err := amc.extractScopeAndName(resourceNameOrID) @@ -656,6 +682,125 @@ func (amc *UCPApplicationsManagementClient) DeleteResourceGroup(ctx context.Cont return response.StatusCode != 204, nil } +// ListResourceProviders lists all resource providers in the configured scope. +func (amc *UCPApplicationsManagementClient) ListResourceProviders(ctx context.Context, planeName string) ([]ucpv20231001.ResourceProviderResource, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return nil, err + } + + results := []ucpv20231001.ResourceProviderResource{} + pager := client.NewListPager(planeName, &ucpv20231001.ResourceProvidersClientListOptions{}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, resourceGroup := range page.Value { + results = append(results, *resourceGroup) + } + } + + return results, nil +} + +// GetResourceProvider gets the resource provider with the specified name in the configured scope. +func (amc *UCPApplicationsManagementClient) GetResourceProvider(ctx context.Context, planeName string, providerNamespace string) (ucpv20231001.ResourceProviderResource, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return ucpv20231001.ResourceProviderResource{}, err + } + + response, err := client.Get(ctx, planeName, providerNamespace, &ucpv20231001.ResourceProvidersClientGetOptions{}) + if err != nil { + return ucpv20231001.ResourceProviderResource{}, err + } + + return response.ResourceProviderResource, nil +} + +// CreateOrUpdateResourceProvider creates or updates a resource provider in the configured scope. +func (amc *UCPApplicationsManagementClient) CreateOrUpdateResourceProvider(ctx context.Context, planeName string, providerNamespace string, resource *ucpv20231001.ResourceProviderResource) (ucpv20231001.ResourceProviderResource, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return ucpv20231001.ResourceProviderResource{}, err + } + + poller, err := client.BeginCreateOrUpdate(ctx, planeName, providerNamespace, *resource, &ucpv20231001.ResourceProvidersClientBeginCreateOrUpdateOptions{}) + if err != nil { + return ucpv20231001.ResourceProviderResource{}, err + } + + response, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return ucpv20231001.ResourceProviderResource{}, err + } + + return response.ResourceProviderResource, nil +} + +// DeleteResourceProvider deletes a resource provider in the configured scope. +func (amc *UCPApplicationsManagementClient) DeleteResourceProvider(ctx context.Context, planeName string, providerNamespace string) (bool, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return false, err + } + + var response *http.Response + ctx = amc.captureResponse(ctx, &response) + + poller, err := client.BeginDelete(ctx, planeName, providerNamespace, &ucpv20231001.ResourceProvidersClientBeginDeleteOptions{}) + if err != nil { + return false, err + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return false, err + } + + return response.StatusCode != 204, nil +} + +// ListResourceProviderSummaries lists all resource provider summaries in the configured scope. +func (amc *UCPApplicationsManagementClient) ListResourceProviderSummaries(ctx context.Context, planeName string) ([]ucpv20231001.ResourceProviderSummary, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return nil, err + } + + results := []ucpv20231001.ResourceProviderSummary{} + pager := client.NewListProviderSummariesPager(planeName, &ucpv20231001.ResourceProvidersClientListProviderSummariesOptions{}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, summary := range page.Value { + results = append(results, *summary) + } + } + + return results, nil +} + +// GetResourceProvider gets the resource provider summary with the specified name in the configured scope. +func (amc *UCPApplicationsManagementClient) GetResourceProviderSummary(ctx context.Context, planeName string, providerNamespace string) (ucpv20231001.ResourceProviderSummary, error) { + client, err := amc.createResourceProviderClient() + if err != nil { + return ucpv20231001.ResourceProviderSummary{}, err + } + + response, err := client.GetProviderSummary(ctx, planeName, providerNamespace, &ucpv20231001.ResourceProvidersClientGetProviderSummaryOptions{}) + if err != nil { + return ucpv20231001.ResourceProviderSummary{}, err + } + + return response.ResourceProviderSummary, nil +} + func (amc *UCPApplicationsManagementClient) createApplicationClient(scope string) (applicationResourceClient, error) { if amc.applicationResourceClientFactory == nil { // Generated client doesn't like the leading '/' in the scope. @@ -691,6 +836,14 @@ func (amc *UCPApplicationsManagementClient) createResourceGroupClient() (resourc return amc.resourceGroupClientFactory() } +func (amc *UCPApplicationsManagementClient) createResourceProviderClient() (resourceProviderClient, error) { + if amc.resourceProviderClientFactory == nil { + return ucpv20231001.NewResourceProvidersClient(&aztoken.AnonymousCredential{}, amc.ClientOptions) + } + + return amc.resourceProviderClientFactory() +} + func (amc *UCPApplicationsManagementClient) extractScopeAndName(nameOrID string) (string, string, error) { if strings.HasPrefix(nameOrID, resources.SegmentSeparator) { // Treat this as a resource id. diff --git a/pkg/cli/clients/management_mocks.go b/pkg/cli/clients/management_mocks.go index 93d6e42305f..23a62d17749 100644 --- a/pkg/cli/clients/management_mocks.go +++ b/pkg/cli/clients/management_mocks.go @@ -34,7 +34,7 @@ import ( // Because these interfaces are non-exported, they MUST be defined in their own file // and we MUST use -source on mockgen to generate mocks for them. -//go:generate mockgen -typed -source=./management_mocks.go -destination=./mock_management_wrapped_clients.go -package=clients -self_package github.com/radius-project/radius/pkg/cli/clients github.com/radius-project/radius/pkg/cli/clients genericResourceClient,applicationResourceClient,environmentResourceClient,resourceGroupClient +//go:generate mockgen -typed -source=./management_mocks.go -destination=./mock_management_wrapped_clients.go -package=clients -self_package github.com/radius-project/radius/pkg/cli/clients github.com/radius-project/radius/pkg/cli/clients genericResourceClient,applicationResourceClient,environmentResourceClient,resourceGroupClient,resourceProviderClient // genericResourceClient is an interface for mocking the generated SDK client for any resource. type genericResourceClient interface { @@ -71,3 +71,13 @@ type resourceGroupClient interface { Get(ctx context.Context, planeName string, resourceGroupName string, options *ucpv20231001.ResourceGroupsClientGetOptions) (ucpv20231001.ResourceGroupsClientGetResponse, error) NewListPager(planeName string, options *ucpv20231001.ResourceGroupsClientListOptions) *runtime.Pager[ucpv20231001.ResourceGroupsClientListResponse] } + +// resourceProviderClient is an interface for mocking the generated SDK client for resource providers. +type resourceProviderClient interface { + BeginCreateOrUpdate(ctx context.Context, planeName string, resourceProviderName string, resource ucpv20231001.ResourceProviderResource, options *ucpv20231001.ResourceProvidersClientBeginCreateOrUpdateOptions) (*runtime.Poller[ucpv20231001.ResourceProvidersClientCreateOrUpdateResponse], error) + BeginDelete(ctx context.Context, planeName string, resourceProviderName string, options *ucpv20231001.ResourceProvidersClientBeginDeleteOptions) (*runtime.Poller[ucpv20231001.ResourceProvidersClientDeleteResponse], error) + Get(ctx context.Context, planeName string, resourceProviderName string, options *ucpv20231001.ResourceProvidersClientGetOptions) (ucpv20231001.ResourceProvidersClientGetResponse, error) + NewListPager(planeName string, options *ucpv20231001.ResourceProvidersClientListOptions) *runtime.Pager[ucpv20231001.ResourceProvidersClientListResponse] + GetProviderSummary(ctx context.Context, planeName string, resourceProviderName string, options *ucpv20231001.ResourceProvidersClientGetProviderSummaryOptions) (ucpv20231001.ResourceProvidersClientGetProviderSummaryResponse, error) + NewListProviderSummariesPager(planeName string, options *ucpv20231001.ResourceProvidersClientListProviderSummariesOptions) *runtime.Pager[ucpv20231001.ResourceProvidersClientListProviderSummariesResponse] +} diff --git a/pkg/cli/clients/management_test.go b/pkg/cli/clients/management_test.go index e093d003cb1..5f775a92985 100644 --- a/pkg/cli/clients/management_test.go +++ b/pkg/cli/clients/management_test.go @@ -220,6 +220,19 @@ func Test_Resource(t *testing.T) { require.Equal(t, expectedResource, resource) }) + t.Run("CreateOrUpdateResource", func(t *testing.T) { + mock := NewMockgenericResourceClient(gomock.NewController(t)) + client := createClient(mock) + + mock.EXPECT(). + BeginCreateOrUpdate(gomock.Any(), testResourceName, expectedResource, gomock.Any()). + Return(poller(&generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: expectedResource}), nil) + + response, err := client.CreateOrUpdateResource(context.Background(), testResourceType, testResourceID, &expectedResource) + require.NoError(t, err) + require.Equal(t, expectedResource, response) + }) + t.Run("DeleteResource", func(t *testing.T) { mock := NewMockgenericResourceClient(gomock.NewController(t)) client := createClient(mock) @@ -828,6 +841,125 @@ func Test_ResourceGroup(t *testing.T) { }) } +func Test_ResourceProvider(t *testing.T) { + createClient := func(wrapped resourceProviderClient) *UCPApplicationsManagementClient { + return &UCPApplicationsManagementClient{ + RootScope: testScope, + resourceProviderClientFactory: func() (resourceProviderClient, error) { + return wrapped, nil + }, + capture: testCapture, + } + } + + testResourceProviderName := "Applications.Test" + + expectedResource := ucp.ResourceProviderResource{ + ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders" + testResourceProviderName), + Name: &testResourceProviderName, + Type: to.Ptr("System.Resources/resourceProviders"), + Location: to.Ptr(v1.LocationGlobal), + } + + t.Run("ListResourceProviders", func(t *testing.T) { + mock := NewMockresourceProviderClient(gomock.NewController(t)) + client := createClient(mock) + + resourceProviderPages := []ucp.ResourceProvidersClientListResponse{ + { + ResourceProviderResourceListResult: ucp.ResourceProviderResourceListResult{ + Value: []*ucp.ResourceProviderResource{ + { + ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/Applications.Test1"), + Name: to.Ptr("Applications.Test1"), + Type: to.Ptr("System.Resources/resourceProviders"), + Location: to.Ptr(v1.LocationGlobal), + }, + { + ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/Applications.Test2"), + Name: to.Ptr("Applications.Test2"), + Type: to.Ptr("System.Resources/resourceProviders"), + Location: to.Ptr(v1.LocationGlobal), + }, + }, + NextLink: to.Ptr("0"), + }, + }, + { + ResourceProviderResourceListResult: ucp.ResourceProviderResourceListResult{ + Value: []*ucp.ResourceProviderResource{ + { + ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/Applications.Test3"), + Name: to.Ptr("Applications.Test3"), + Type: to.Ptr("System.Resources/resourceProviders"), + Location: to.Ptr(v1.LocationGlobal), + }, + { + ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/Applications.Test4"), + Name: to.Ptr("Applications.Test4"), + Type: to.Ptr("System.Resources/resourceProviders"), + Location: to.Ptr(v1.LocationGlobal), + }, + }, + NextLink: to.Ptr("1"), + }, + }, + } + + mock.EXPECT(). + NewListPager(gomock.Any(), gomock.Any()). + Return(pager(resourceProviderPages)) + + expected := []ucp.ResourceProviderResource{*resourceProviderPages[0].Value[0], *resourceProviderPages[0].Value[1], *resourceProviderPages[1].Value[0], *resourceProviderPages[1].Value[1]} + + groups, err := client.ListResourceProviders(context.Background(), "local") + require.NoError(t, err) + require.Equal(t, expected, groups) + }) + + t.Run("GetResourceProvider", func(t *testing.T) { + mock := NewMockresourceProviderClient(gomock.NewController(t)) + client := createClient(mock) + + mock.EXPECT(). + Get(gomock.Any(), "local", testResourceProviderName, gomock.Any()). + Return(ucp.ResourceProvidersClientGetResponse{ResourceProviderResource: expectedResource}, nil) + + group, err := client.GetResourceProvider(context.Background(), "local", testResourceProviderName) + require.NoError(t, err) + require.Equal(t, expectedResource, group) + }) + + t.Run("CreateOrUpdateResourceProvider", func(t *testing.T) { + mock := NewMockresourceProviderClient(gomock.NewController(t)) + client := createClient(mock) + + mock.EXPECT(). + BeginCreateOrUpdate(gomock.Any(), "local", testResourceProviderName, expectedResource, gomock.Any()). + Return(poller(&ucp.ResourceProvidersClientCreateOrUpdateResponse{ResourceProviderResource: expectedResource}), nil) + + result, err := client.CreateOrUpdateResourceProvider(context.Background(), "local", testResourceProviderName, &expectedResource) + require.NoError(t, err) + require.Equal(t, result, expectedResource) + }) + + t.Run("DeleteResourceProvider", func(t *testing.T) { + mock := NewMockresourceProviderClient(gomock.NewController(t)) + client := createClient(mock) + + mock.EXPECT(). + BeginDelete(gomock.Any(), "local", testResourceProviderName, gomock.Any()). + DoAndReturn(func(ctx context.Context, s1, s2 string, rgcdo *ucp.ResourceProvidersClientBeginDeleteOptions) (*runtime.Poller[ucp.ResourceProvidersClientDeleteResponse], error) { + setCapture(ctx, &http.Response{StatusCode: 200}) + return poller(&ucp.ResourceProvidersClientDeleteResponse{}), nil + }) + + deleted, err := client.DeleteResourceProvider(context.Background(), "local", testResourceProviderName) + require.NoError(t, err) + require.True(t, deleted) + }) +} + func Test_extractScopeAndName(t *testing.T) { client := UCPApplicationsManagementClient{ RootScope: testScope, @@ -959,8 +1091,9 @@ type holder struct { } func setCapture(ctx context.Context, response *http.Response) { - holder := ctx.Value(holder{}).(*holder) - if holder != nil { + obj := ctx.Value(holder{}) + if obj != nil { + holder := obj.(*holder) *holder.capture = response } } diff --git a/pkg/cli/clients/mock_applicationsclient.go b/pkg/cli/clients/mock_applicationsclient.go index 26832d9a6a9..e9489db8d9a 100644 --- a/pkg/cli/clients/mock_applicationsclient.go +++ b/pkg/cli/clients/mock_applicationsclient.go @@ -156,6 +156,45 @@ func (c *MockApplicationsManagementClientCreateOrUpdateEnvironmentCall) DoAndRet return c } +// CreateOrUpdateResource mocks base method. +func (m *MockApplicationsManagementClient) CreateOrUpdateResource(arg0 context.Context, arg1, arg2 string, arg3 *generated.GenericResource) (generated.GenericResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateResource", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(generated.GenericResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateResource indicates an expected call of CreateOrUpdateResource. +func (mr *MockApplicationsManagementClientMockRecorder) CreateOrUpdateResource(arg0, arg1, arg2, arg3 any) *MockApplicationsManagementClientCreateOrUpdateResourceCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateResource", reflect.TypeOf((*MockApplicationsManagementClient)(nil).CreateOrUpdateResource), arg0, arg1, arg2, arg3) + return &MockApplicationsManagementClientCreateOrUpdateResourceCall{Call: call} +} + +// MockApplicationsManagementClientCreateOrUpdateResourceCall wrap *gomock.Call +type MockApplicationsManagementClientCreateOrUpdateResourceCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientCreateOrUpdateResourceCall) Return(arg0 generated.GenericResource, arg1 error) *MockApplicationsManagementClientCreateOrUpdateResourceCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientCreateOrUpdateResourceCall) Do(f func(context.Context, string, string, *generated.GenericResource) (generated.GenericResource, error)) *MockApplicationsManagementClientCreateOrUpdateResourceCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientCreateOrUpdateResourceCall) DoAndReturn(f func(context.Context, string, string, *generated.GenericResource) (generated.GenericResource, error)) *MockApplicationsManagementClientCreateOrUpdateResourceCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // CreateOrUpdateResourceGroup mocks base method. func (m *MockApplicationsManagementClient) CreateOrUpdateResourceGroup(arg0 context.Context, arg1, arg2 string, arg3 *v20231001preview0.ResourceGroupResource) error { m.ctrl.T.Helper() @@ -194,6 +233,45 @@ func (c *MockApplicationsManagementClientCreateOrUpdateResourceGroupCall) DoAndR return c } +// CreateOrUpdateResourceProvider mocks base method. +func (m *MockApplicationsManagementClient) CreateOrUpdateResourceProvider(arg0 context.Context, arg1, arg2 string, arg3 *v20231001preview0.ResourceProviderResource) (v20231001preview0.ResourceProviderResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateResourceProvider", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(v20231001preview0.ResourceProviderResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateResourceProvider indicates an expected call of CreateOrUpdateResourceProvider. +func (mr *MockApplicationsManagementClientMockRecorder) CreateOrUpdateResourceProvider(arg0, arg1, arg2, arg3 any) *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateResourceProvider", reflect.TypeOf((*MockApplicationsManagementClient)(nil).CreateOrUpdateResourceProvider), arg0, arg1, arg2, arg3) + return &MockApplicationsManagementClientCreateOrUpdateResourceProviderCall{Call: call} +} + +// MockApplicationsManagementClientCreateOrUpdateResourceProviderCall wrap *gomock.Call +type MockApplicationsManagementClientCreateOrUpdateResourceProviderCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall) Return(arg0 v20231001preview0.ResourceProviderResource, arg1 error) *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall) Do(f func(context.Context, string, string, *v20231001preview0.ResourceProviderResource) (v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall) DoAndReturn(f func(context.Context, string, string, *v20231001preview0.ResourceProviderResource) (v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientCreateOrUpdateResourceProviderCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // DeleteApplication mocks base method. func (m *MockApplicationsManagementClient) DeleteApplication(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() @@ -350,6 +428,45 @@ func (c *MockApplicationsManagementClientDeleteResourceGroupCall) DoAndReturn(f return c } +// DeleteResourceProvider mocks base method. +func (m *MockApplicationsManagementClient) DeleteResourceProvider(arg0 context.Context, arg1, arg2 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteResourceProvider", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteResourceProvider indicates an expected call of DeleteResourceProvider. +func (mr *MockApplicationsManagementClientMockRecorder) DeleteResourceProvider(arg0, arg1, arg2 any) *MockApplicationsManagementClientDeleteResourceProviderCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResourceProvider", reflect.TypeOf((*MockApplicationsManagementClient)(nil).DeleteResourceProvider), arg0, arg1, arg2) + return &MockApplicationsManagementClientDeleteResourceProviderCall{Call: call} +} + +// MockApplicationsManagementClientDeleteResourceProviderCall wrap *gomock.Call +type MockApplicationsManagementClientDeleteResourceProviderCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientDeleteResourceProviderCall) Return(arg0 bool, arg1 error) *MockApplicationsManagementClientDeleteResourceProviderCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientDeleteResourceProviderCall) Do(f func(context.Context, string, string) (bool, error)) *MockApplicationsManagementClientDeleteResourceProviderCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientDeleteResourceProviderCall) DoAndReturn(f func(context.Context, string, string) (bool, error)) *MockApplicationsManagementClientDeleteResourceProviderCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // GetApplication mocks base method. func (m *MockApplicationsManagementClient) GetApplication(arg0 context.Context, arg1 string) (v20231001preview.ApplicationResource, error) { m.ctrl.T.Helper() @@ -584,6 +701,84 @@ func (c *MockApplicationsManagementClientGetResourceGroupCall) DoAndReturn(f fun return c } +// GetResourceProvider mocks base method. +func (m *MockApplicationsManagementClient) GetResourceProvider(arg0 context.Context, arg1, arg2 string) (v20231001preview0.ResourceProviderResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetResourceProvider", arg0, arg1, arg2) + ret0, _ := ret[0].(v20231001preview0.ResourceProviderResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourceProvider indicates an expected call of GetResourceProvider. +func (mr *MockApplicationsManagementClientMockRecorder) GetResourceProvider(arg0, arg1, arg2 any) *MockApplicationsManagementClientGetResourceProviderCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceProvider", reflect.TypeOf((*MockApplicationsManagementClient)(nil).GetResourceProvider), arg0, arg1, arg2) + return &MockApplicationsManagementClientGetResourceProviderCall{Call: call} +} + +// MockApplicationsManagementClientGetResourceProviderCall wrap *gomock.Call +type MockApplicationsManagementClientGetResourceProviderCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientGetResourceProviderCall) Return(arg0 v20231001preview0.ResourceProviderResource, arg1 error) *MockApplicationsManagementClientGetResourceProviderCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientGetResourceProviderCall) Do(f func(context.Context, string, string) (v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientGetResourceProviderCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientGetResourceProviderCall) DoAndReturn(f func(context.Context, string, string) (v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientGetResourceProviderCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetResourceProviderSummary mocks base method. +func (m *MockApplicationsManagementClient) GetResourceProviderSummary(arg0 context.Context, arg1, arg2 string) (v20231001preview0.ResourceProviderSummary, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetResourceProviderSummary", arg0, arg1, arg2) + ret0, _ := ret[0].(v20231001preview0.ResourceProviderSummary) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourceProviderSummary indicates an expected call of GetResourceProviderSummary. +func (mr *MockApplicationsManagementClientMockRecorder) GetResourceProviderSummary(arg0, arg1, arg2 any) *MockApplicationsManagementClientGetResourceProviderSummaryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceProviderSummary", reflect.TypeOf((*MockApplicationsManagementClient)(nil).GetResourceProviderSummary), arg0, arg1, arg2) + return &MockApplicationsManagementClientGetResourceProviderSummaryCall{Call: call} +} + +// MockApplicationsManagementClientGetResourceProviderSummaryCall wrap *gomock.Call +type MockApplicationsManagementClientGetResourceProviderSummaryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientGetResourceProviderSummaryCall) Return(arg0 v20231001preview0.ResourceProviderSummary, arg1 error) *MockApplicationsManagementClientGetResourceProviderSummaryCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientGetResourceProviderSummaryCall) Do(f func(context.Context, string, string) (v20231001preview0.ResourceProviderSummary, error)) *MockApplicationsManagementClientGetResourceProviderSummaryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientGetResourceProviderSummaryCall) DoAndReturn(f func(context.Context, string, string) (v20231001preview0.ResourceProviderSummary, error)) *MockApplicationsManagementClientGetResourceProviderSummaryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // ListApplications mocks base method. func (m *MockApplicationsManagementClient) ListApplications(arg0 context.Context) ([]v20231001preview.ApplicationResource, error) { m.ctrl.T.Helper() @@ -740,6 +935,84 @@ func (c *MockApplicationsManagementClientListResourceGroupsCall) DoAndReturn(f f return c } +// ListResourceProviderSummaries mocks base method. +func (m *MockApplicationsManagementClient) ListResourceProviderSummaries(arg0 context.Context, arg1 string) ([]v20231001preview0.ResourceProviderSummary, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListResourceProviderSummaries", arg0, arg1) + ret0, _ := ret[0].([]v20231001preview0.ResourceProviderSummary) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListResourceProviderSummaries indicates an expected call of ListResourceProviderSummaries. +func (mr *MockApplicationsManagementClientMockRecorder) ListResourceProviderSummaries(arg0, arg1 any) *MockApplicationsManagementClientListResourceProviderSummariesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourceProviderSummaries", reflect.TypeOf((*MockApplicationsManagementClient)(nil).ListResourceProviderSummaries), arg0, arg1) + return &MockApplicationsManagementClientListResourceProviderSummariesCall{Call: call} +} + +// MockApplicationsManagementClientListResourceProviderSummariesCall wrap *gomock.Call +type MockApplicationsManagementClientListResourceProviderSummariesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientListResourceProviderSummariesCall) Return(arg0 []v20231001preview0.ResourceProviderSummary, arg1 error) *MockApplicationsManagementClientListResourceProviderSummariesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientListResourceProviderSummariesCall) Do(f func(context.Context, string) ([]v20231001preview0.ResourceProviderSummary, error)) *MockApplicationsManagementClientListResourceProviderSummariesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientListResourceProviderSummariesCall) DoAndReturn(f func(context.Context, string) ([]v20231001preview0.ResourceProviderSummary, error)) *MockApplicationsManagementClientListResourceProviderSummariesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListResourceProviders mocks base method. +func (m *MockApplicationsManagementClient) ListResourceProviders(arg0 context.Context, arg1 string) ([]v20231001preview0.ResourceProviderResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListResourceProviders", arg0, arg1) + ret0, _ := ret[0].([]v20231001preview0.ResourceProviderResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListResourceProviders indicates an expected call of ListResourceProviders. +func (mr *MockApplicationsManagementClientMockRecorder) ListResourceProviders(arg0, arg1 any) *MockApplicationsManagementClientListResourceProvidersCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourceProviders", reflect.TypeOf((*MockApplicationsManagementClient)(nil).ListResourceProviders), arg0, arg1) + return &MockApplicationsManagementClientListResourceProvidersCall{Call: call} +} + +// MockApplicationsManagementClientListResourceProvidersCall wrap *gomock.Call +type MockApplicationsManagementClientListResourceProvidersCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockApplicationsManagementClientListResourceProvidersCall) Return(arg0 []v20231001preview0.ResourceProviderResource, arg1 error) *MockApplicationsManagementClientListResourceProvidersCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockApplicationsManagementClientListResourceProvidersCall) Do(f func(context.Context, string) ([]v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientListResourceProvidersCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockApplicationsManagementClientListResourceProvidersCall) DoAndReturn(f func(context.Context, string) ([]v20231001preview0.ResourceProviderResource, error)) *MockApplicationsManagementClientListResourceProvidersCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // ListResourcesInApplication mocks base method. func (m *MockApplicationsManagementClient) ListResourcesInApplication(arg0 context.Context, arg1 string) ([]generated.GenericResource, error) { m.ctrl.T.Helper() diff --git a/pkg/cli/clients/mock_management_wrapped_clients.go b/pkg/cli/clients/mock_management_wrapped_clients.go index 971f5adeddf..2e63a67ef8f 100644 --- a/pkg/cli/clients/mock_management_wrapped_clients.go +++ b/pkg/cli/clients/mock_management_wrapped_clients.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -typed -source=./management_mocks.go -destination=./mock_management_wrapped_clients.go -package=clients -self_package github.com/radius-project/radius/pkg/cli/clients github.com/radius-project/radius/pkg/cli/clients genericResourceClient,applicationResourceClient,environmentResourceClient,resourceGroupClient +// mockgen -typed -source=./management_mocks.go -destination=./mock_management_wrapped_clients.go -package=clients -self_package github.com/radius-project/radius/pkg/cli/clients github.com/radius-project/radius/pkg/cli/clients genericResourceClient,applicationResourceClient,environmentResourceClient,resourceGroupClient,resourceProviderClient // // Package clients is a generated GoMock package. @@ -809,3 +809,258 @@ func (c *MockresourceGroupClientNewListPagerCall) DoAndReturn(f func(string, *v2 c.Call = c.Call.DoAndReturn(f) return c } + +// MockresourceProviderClient is a mock of resourceProviderClient interface. +type MockresourceProviderClient struct { + ctrl *gomock.Controller + recorder *MockresourceProviderClientMockRecorder +} + +// MockresourceProviderClientMockRecorder is the mock recorder for MockresourceProviderClient. +type MockresourceProviderClientMockRecorder struct { + mock *MockresourceProviderClient +} + +// NewMockresourceProviderClient creates a new mock instance. +func NewMockresourceProviderClient(ctrl *gomock.Controller) *MockresourceProviderClient { + mock := &MockresourceProviderClient{ctrl: ctrl} + mock.recorder = &MockresourceProviderClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockresourceProviderClient) EXPECT() *MockresourceProviderClientMockRecorder { + return m.recorder +} + +// BeginCreateOrUpdate mocks base method. +func (m *MockresourceProviderClient) BeginCreateOrUpdate(ctx context.Context, planeName, resourceProviderName string, resource v20231001preview0.ResourceProviderResource, options *v20231001preview0.ResourceProvidersClientBeginCreateOrUpdateOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientCreateOrUpdateResponse], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginCreateOrUpdate", ctx, planeName, resourceProviderName, resource, options) + ret0, _ := ret[0].(*runtime.Poller[v20231001preview0.ResourceProvidersClientCreateOrUpdateResponse]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginCreateOrUpdate indicates an expected call of BeginCreateOrUpdate. +func (mr *MockresourceProviderClientMockRecorder) BeginCreateOrUpdate(ctx, planeName, resourceProviderName, resource, options any) *MockresourceProviderClientBeginCreateOrUpdateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginCreateOrUpdate", reflect.TypeOf((*MockresourceProviderClient)(nil).BeginCreateOrUpdate), ctx, planeName, resourceProviderName, resource, options) + return &MockresourceProviderClientBeginCreateOrUpdateCall{Call: call} +} + +// MockresourceProviderClientBeginCreateOrUpdateCall wrap *gomock.Call +type MockresourceProviderClientBeginCreateOrUpdateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientBeginCreateOrUpdateCall) Return(arg0 *runtime.Poller[v20231001preview0.ResourceProvidersClientCreateOrUpdateResponse], arg1 error) *MockresourceProviderClientBeginCreateOrUpdateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientBeginCreateOrUpdateCall) Do(f func(context.Context, string, string, v20231001preview0.ResourceProviderResource, *v20231001preview0.ResourceProvidersClientBeginCreateOrUpdateOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientCreateOrUpdateResponse], error)) *MockresourceProviderClientBeginCreateOrUpdateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientBeginCreateOrUpdateCall) DoAndReturn(f func(context.Context, string, string, v20231001preview0.ResourceProviderResource, *v20231001preview0.ResourceProvidersClientBeginCreateOrUpdateOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientCreateOrUpdateResponse], error)) *MockresourceProviderClientBeginCreateOrUpdateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// BeginDelete mocks base method. +func (m *MockresourceProviderClient) BeginDelete(ctx context.Context, planeName, resourceProviderName string, options *v20231001preview0.ResourceProvidersClientBeginDeleteOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientDeleteResponse], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginDelete", ctx, planeName, resourceProviderName, options) + ret0, _ := ret[0].(*runtime.Poller[v20231001preview0.ResourceProvidersClientDeleteResponse]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginDelete indicates an expected call of BeginDelete. +func (mr *MockresourceProviderClientMockRecorder) BeginDelete(ctx, planeName, resourceProviderName, options any) *MockresourceProviderClientBeginDeleteCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginDelete", reflect.TypeOf((*MockresourceProviderClient)(nil).BeginDelete), ctx, planeName, resourceProviderName, options) + return &MockresourceProviderClientBeginDeleteCall{Call: call} +} + +// MockresourceProviderClientBeginDeleteCall wrap *gomock.Call +type MockresourceProviderClientBeginDeleteCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientBeginDeleteCall) Return(arg0 *runtime.Poller[v20231001preview0.ResourceProvidersClientDeleteResponse], arg1 error) *MockresourceProviderClientBeginDeleteCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientBeginDeleteCall) Do(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientBeginDeleteOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientDeleteResponse], error)) *MockresourceProviderClientBeginDeleteCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientBeginDeleteCall) DoAndReturn(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientBeginDeleteOptions) (*runtime.Poller[v20231001preview0.ResourceProvidersClientDeleteResponse], error)) *MockresourceProviderClientBeginDeleteCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Get mocks base method. +func (m *MockresourceProviderClient) Get(ctx context.Context, planeName, resourceProviderName string, options *v20231001preview0.ResourceProvidersClientGetOptions) (v20231001preview0.ResourceProvidersClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, planeName, resourceProviderName, options) + ret0, _ := ret[0].(v20231001preview0.ResourceProvidersClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockresourceProviderClientMockRecorder) Get(ctx, planeName, resourceProviderName, options any) *MockresourceProviderClientGetCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockresourceProviderClient)(nil).Get), ctx, planeName, resourceProviderName, options) + return &MockresourceProviderClientGetCall{Call: call} +} + +// MockresourceProviderClientGetCall wrap *gomock.Call +type MockresourceProviderClientGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientGetCall) Return(arg0 v20231001preview0.ResourceProvidersClientGetResponse, arg1 error) *MockresourceProviderClientGetCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientGetCall) Do(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientGetOptions) (v20231001preview0.ResourceProvidersClientGetResponse, error)) *MockresourceProviderClientGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientGetCall) DoAndReturn(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientGetOptions) (v20231001preview0.ResourceProvidersClientGetResponse, error)) *MockresourceProviderClientGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetProviderSummary mocks base method. +func (m *MockresourceProviderClient) GetProviderSummary(ctx context.Context, planeName, resourceProviderName string, options *v20231001preview0.ResourceProvidersClientGetProviderSummaryOptions) (v20231001preview0.ResourceProvidersClientGetProviderSummaryResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProviderSummary", ctx, planeName, resourceProviderName, options) + ret0, _ := ret[0].(v20231001preview0.ResourceProvidersClientGetProviderSummaryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProviderSummary indicates an expected call of GetProviderSummary. +func (mr *MockresourceProviderClientMockRecorder) GetProviderSummary(ctx, planeName, resourceProviderName, options any) *MockresourceProviderClientGetProviderSummaryCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderSummary", reflect.TypeOf((*MockresourceProviderClient)(nil).GetProviderSummary), ctx, planeName, resourceProviderName, options) + return &MockresourceProviderClientGetProviderSummaryCall{Call: call} +} + +// MockresourceProviderClientGetProviderSummaryCall wrap *gomock.Call +type MockresourceProviderClientGetProviderSummaryCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientGetProviderSummaryCall) Return(arg0 v20231001preview0.ResourceProvidersClientGetProviderSummaryResponse, arg1 error) *MockresourceProviderClientGetProviderSummaryCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientGetProviderSummaryCall) Do(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientGetProviderSummaryOptions) (v20231001preview0.ResourceProvidersClientGetProviderSummaryResponse, error)) *MockresourceProviderClientGetProviderSummaryCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientGetProviderSummaryCall) DoAndReturn(f func(context.Context, string, string, *v20231001preview0.ResourceProvidersClientGetProviderSummaryOptions) (v20231001preview0.ResourceProvidersClientGetProviderSummaryResponse, error)) *MockresourceProviderClientGetProviderSummaryCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// NewListPager mocks base method. +func (m *MockresourceProviderClient) NewListPager(planeName string, options *v20231001preview0.ResourceProvidersClientListOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListResponse] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", planeName, options) + ret0, _ := ret[0].(*runtime.Pager[v20231001preview0.ResourceProvidersClientListResponse]) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockresourceProviderClientMockRecorder) NewListPager(planeName, options any) *MockresourceProviderClientNewListPagerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockresourceProviderClient)(nil).NewListPager), planeName, options) + return &MockresourceProviderClientNewListPagerCall{Call: call} +} + +// MockresourceProviderClientNewListPagerCall wrap *gomock.Call +type MockresourceProviderClientNewListPagerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientNewListPagerCall) Return(arg0 *runtime.Pager[v20231001preview0.ResourceProvidersClientListResponse]) *MockresourceProviderClientNewListPagerCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientNewListPagerCall) Do(f func(string, *v20231001preview0.ResourceProvidersClientListOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListResponse]) *MockresourceProviderClientNewListPagerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientNewListPagerCall) DoAndReturn(f func(string, *v20231001preview0.ResourceProvidersClientListOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListResponse]) *MockresourceProviderClientNewListPagerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// NewListProviderSummariesPager mocks base method. +func (m *MockresourceProviderClient) NewListProviderSummariesPager(planeName string, options *v20231001preview0.ResourceProvidersClientListProviderSummariesOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListProviderSummariesResponse] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListProviderSummariesPager", planeName, options) + ret0, _ := ret[0].(*runtime.Pager[v20231001preview0.ResourceProvidersClientListProviderSummariesResponse]) + return ret0 +} + +// NewListProviderSummariesPager indicates an expected call of NewListProviderSummariesPager. +func (mr *MockresourceProviderClientMockRecorder) NewListProviderSummariesPager(planeName, options any) *MockresourceProviderClientNewListProviderSummariesPagerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListProviderSummariesPager", reflect.TypeOf((*MockresourceProviderClient)(nil).NewListProviderSummariesPager), planeName, options) + return &MockresourceProviderClientNewListProviderSummariesPagerCall{Call: call} +} + +// MockresourceProviderClientNewListProviderSummariesPagerCall wrap *gomock.Call +type MockresourceProviderClientNewListProviderSummariesPagerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockresourceProviderClientNewListProviderSummariesPagerCall) Return(arg0 *runtime.Pager[v20231001preview0.ResourceProvidersClientListProviderSummariesResponse]) *MockresourceProviderClientNewListProviderSummariesPagerCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockresourceProviderClientNewListProviderSummariesPagerCall) Do(f func(string, *v20231001preview0.ResourceProvidersClientListProviderSummariesOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListProviderSummariesResponse]) *MockresourceProviderClientNewListProviderSummariesPagerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockresourceProviderClientNewListProviderSummariesPagerCall) DoAndReturn(f func(string, *v20231001preview0.ResourceProvidersClientListProviderSummariesOptions) *runtime.Pager[v20231001preview0.ResourceProvidersClientListProviderSummariesResponse]) *MockresourceProviderClientNewListProviderSummariesPagerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/pkg/cli/clivalidation.go b/pkg/cli/clivalidation.go index cecd2bdb42d..f2ba43d7ccd 100644 --- a/pkg/cli/clivalidation.go +++ b/pkg/cli/clivalidation.go @@ -284,16 +284,22 @@ func RequireResourceTypeAndName(args []string) (string, string, error) { return resourceType, resourceName, nil } -// example of resource Type: Applications.Datastores/redisCaches -// - // RequireResourceType checks if the first argument provided is a valid resource type and returns it if it is. If the // argument is not valid, an error is returned with a list of valid resource types. +// +// example of resource Type: Applications.Datastores/redisCaches func RequireResourceType(args []string) (string, error) { if len(args) < 1 { return "", errors.New("no resource type provided") } + resourceTypeName := args[0] + + // Allow any fully-qualified resource type. + if strings.Contains(resourceTypeName, "/") { + return resourceTypeName, nil + } + supportedTypes := []string{} foundTypes := []string{} for _, resourceType := range clients.ResourceTypesList { diff --git a/pkg/cli/clivalidation_test.go b/pkg/cli/clivalidation_test.go index d9d7317ed69..71fe283ed3c 100644 --- a/pkg/cli/clivalidation_test.go +++ b/pkg/cli/clivalidation_test.go @@ -55,6 +55,12 @@ func Test_RequireResourceType(t *testing.T) { want: "Applications.Datastores/mongoDatabases", wantErr: nil, }, + { + name: "Fully-qualified resource type", + args: []string{"Applications.Test/exampleResources"}, + want: "Applications.Test/exampleResources", + wantErr: nil, + }, { name: "Multiple resource types", args: []string{"secretStores"}, diff --git a/pkg/cli/cmd/resource/create/create.go b/pkg/cli/cmd/resource/create/create.go new file mode 100644 index 00000000000..e495f66b1af --- /dev/null +++ b/pkg/cli/cmd/resource/create/create.go @@ -0,0 +1,158 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package create + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resource create` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "create [resource type] [name] [input]", + Short: "Create or update a resource", + Long: `Create or update a resource + +Resources are the entities are the primary entities that make up applactions. + +Input can be passed in using a file or inline JSON as the second argument. Prefix the input with '@' to indicate a file path. +`, + Example: ` +# Create a resource (from file) +rad resource create 'Applications.Core/containers' mycontainer @/path/to/input.json + +# Create a resource (inline) +rad resource create 'Applications.Core/containers' mycontainer '{ ... }'`, + Args: cobra.ExactArgs(3), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resource create` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace + + ResourceType string + ResourceName string + Resource *generated.GenericResource +} + +// NewRunner creates an instance of the runner for the `rad resource create` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resource create` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + // Validate command line args and + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.ResourceType = args[0] + r.ResourceName = args[1] + r.Resource, err = readInput(args[2]) + if err != nil { + return err + } + + return nil +} + +func readInput(arg string) (*generated.GenericResource, error) { + // Input could either be a file or inline JSON. The @ prefix will help us determine which one it is. + var bs []byte + if strings.HasPrefix(arg, "@") { + inputFile := strings.TrimPrefix(arg, "@") + + var err error + bs, err = os.ReadFile(inputFile) + if err != nil { + return nil, clierrors.Message("Failed to read input file: %v", err) + } + } else { + bs = []byte(arg) + } + + decoder := json.NewDecoder(strings.NewReader(string(bs))) + decoder.DisallowUnknownFields() + + resource := generated.GenericResource{} + err := decoder.Decode(&resource) + if err != nil { + return nil, clierrors.Message("Invalid input, could not be converted to a resource: %v", err) + } + + return &resource, nil +} + +// Run runs the `rad resource create` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + response, err := client.CreateOrUpdateResource(ctx, r.ResourceType, r.ResourceName, r.Resource) + if err != nil { + return err + } + + err = r.Output.WriteFormatted(r.Format, response, common.GetResourceProviderTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resource/create/create_test.go b/pkg/cli/cmd/resource/create/create_test.go new file mode 100644 index 00000000000..e731f305caa --- /dev/null +++ b/pkg/cli/cmd/resource/create/create_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package create + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + + resource := map[string]any{ + "properties": map[string]any{ + "message": "Hello, world!", + }, + } + b, err := json.Marshal(resource) + require.NoError(t, err) + + directory := t.TempDir() + err = os.WriteFile(filepath.Join(directory, "valid-resource.json"), b, 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(directory, "invalid-resource.json"), []byte("{askdfe}"), 0644) + require.NoError(t, err) + + testcases := []radcli.ValidateInput{ + { + Name: "Valid: inline JSON", + Input: []string{"Applications.Test/exampleResources", "my-example", string(b)}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Valid: JSON file", + Input: []string{"Applications.Test/exampleResources", "my-example", "@valid-resource.json"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + CreateTempDirectory: directory, + }, + { + Name: "Valid: inline non-JSON", + Input: []string{"Applications.Test/exampleResources", "my-example", "{askdfe}"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: non-JSON file", + Input: []string{"Applications.Test/exampleResources", "my-example", "@invalid-resource.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + CreateTempDirectory: directory, + }, + { + Name: "Invalid: missing arguments", + Input: []string{"Applications.Test/exampleResources", "my-example", "@valid-resource.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"Applications.Test/exampleResources", "my-example", "@valid-resource.json", "dddddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: resource provider created", func(t *testing.T) { + ctrl := gomock.NewController(t) + + expectedResource := &generated.GenericResource{ + Properties: map[string]any{ + "message": "Hello, world!", + }, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + CreateOrUpdateResource(gomock.Any(), "Applications.Test/exampleResources", "my-example", expectedResource). + Return(*expectedResource, nil). + Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + ResourceType: "Applications.Test/exampleResources", + ResourceName: "my-example", + Resource: expectedResource, + Format: "table", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + }) +} diff --git a/pkg/cli/cmd/resource/delete/delete.go b/pkg/cli/cmd/resource/delete/delete.go index df366fe23d2..d442210b966 100644 --- a/pkg/cli/cmd/resource/delete/delete.go +++ b/pkg/cli/cmd/resource/delete/delete.go @@ -33,8 +33,9 @@ import ( ) const ( - deleteConfirmationWithoutApplication = "Are you sure you want to delete resource '%v' of type %v from environment '%v'?" - deleteConfirmationWithApplication = "Are you sure you want to delete resource '%v' of type %v in application '%v' from environment '%v'?" + deleteConfirmationWithoutApplicationOrEnvironment = "Are you sure you want to delete resource '%v' of type %v?" + deleteConfirmationWithoutApplication = "Are you sure you want to delete resource '%v' of type %v from environment '%v'?" + deleteConfirmationWithApplication = "Are you sure you want to delete resource '%v' of type %v in application '%v' from environment '%v'?" ) // NewCommand creates an instance of the command and runner for the `rad resource delete` command. @@ -153,7 +154,9 @@ func (r *Runner) Run(ctx context.Context) error { // Prompt user to confirm deletion if !r.Confirm { var promptMessage string - if applicationID.IsEmpty() { + if applicationID.IsEmpty() && environmentID.IsEmpty() { + promptMessage = fmt.Sprintf(deleteConfirmationWithoutApplicationOrEnvironment, r.ResourceName, r.ResourceType) + } else if applicationID.IsEmpty() { promptMessage = fmt.Sprintf(deleteConfirmationWithoutApplication, r.ResourceName, r.ResourceType, environmentID.Name()) } else { promptMessage = fmt.Sprintf(deleteConfirmationWithApplication, r.ResourceName, r.ResourceType, applicationID.Name(), environmentID.Name()) @@ -196,6 +199,7 @@ func (r *Runner) extractEnvironmentAndApplicationIDs(ctx context.Context, client // 3. The resource has an application but no environment. (common case for a *core* resource like a container) // - In this case, the environment can be looked up through the application // - See: https://github.com/radius-project/radius/issues/2928 + // 4. The resource has no environment or application. (eg: a Bicep deployment) if resource.Properties["environment"] != nil { environmentID, err = convertToResourceID(resource.Properties["environment"]) if err != nil { @@ -210,6 +214,11 @@ func (r *Runner) extractEnvironmentAndApplicationIDs(ctx context.Context, client } } + // Detect case 4: (no environment or application) + if environmentID.IsEmpty() && applicationID.IsEmpty() { + return resources.ID{}, resources.ID{}, nil + } + // At this point we have the environment and application IDs **if** they were returned by // the API. That covers case 1 & 2. Now we need to handle case 3, by doing an additional // lookup. @@ -217,6 +226,10 @@ func (r *Runner) extractEnvironmentAndApplicationIDs(ctx context.Context, client return environmentID, applicationID, nil // Case 1 or Case 2 } + if applicationID.IsEmpty() { + return resources.ID{}, resources.ID{}, nil + } + application, err := client.GetApplication(ctx, applicationID.String()) if clients.Is404Error(err) { // Ignore 404s for this case, and just assume there is no application. The user is diff --git a/pkg/cli/cmd/resource/delete/delete_test.go b/pkg/cli/cmd/resource/delete/delete_test.go index 4e908df0804..0dc8e07cf50 100644 --- a/pkg/cli/cmd/resource/delete/delete_test.go +++ b/pkg/cli/cmd/resource/delete/delete_test.go @@ -274,6 +274,7 @@ func Test_Run(t *testing.T) { Name: "kind-kind", Scope: "/planes/radius/local/resourceGroups/test-group", } + outputSink := &output.MockOutput{} runner := &Runner{ ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, @@ -331,6 +332,61 @@ func Test_Run(t *testing.T) { Return(true, nil). Times(1) + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: workspace, + ResourceType: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + InputPrompter: promptMock, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Resource deleted", + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Confirmed (case 4: no application or environment)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithoutApplicationOrEnvironment, "test-container", "Applications.Core/containers")). + Return(prompt.ConfirmYes, nil). + Times(1) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{}, + }, nil). + Times(1) + + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(true, nil). + Times(1) + workspace := &workspaces.Workspace{ Connection: map[string]any{ "kind": "kubernetes", @@ -391,6 +447,7 @@ func Test_Run(t *testing.T) { Name: "kind-kind", Scope: "/planes/radius/local/resourceGroups/test-group", } + outputSink := &output.MockOutput{} runner := &Runner{ ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, diff --git a/pkg/cli/cmd/resourceprovider/common/objectformat.go b/pkg/cli/cmd/resourceprovider/common/objectformat.go new file mode 100644 index 00000000000..ef4ce18f636 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/common/objectformat.go @@ -0,0 +1,39 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import "github.com/radius-project/radius/pkg/cli/output" + +// GetResourceProviderTableFormat returns the fields to output from a resource provider object. +func GetResourceProviderTableFormat() output.FormatterOptions { + return output.FormatterOptions{ + Columns: []output.Column{ + { + Heading: "RESOURCE", + JSONPath: "{ .Name }", + }, + { + Heading: "TYPE", + JSONPath: "{ .Type }", + }, + { + Heading: "STATE", + JSONPath: "{ .Properties.ProvisioningState }", + }, + }, + } +} diff --git a/pkg/cli/cmd/resourceprovider/create/create.go b/pkg/cli/cmd/resourceprovider/create/create.go new file mode 100644 index 00000000000..618af892576 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/create/create.go @@ -0,0 +1,155 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package create + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourceprovider create` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "create [resource provider namespace] [input]", + Short: "Create or update a resource provider", + Long: `Create or update a resource provider + +Resource providers are the entities that implement resource types such as 'Applications.Core/containers'. Resource providers can be defined, registered, and unregistered by users. + +Creating a resource provider defines new resource types that can be used in applications. + +Input can be passed in using a file or inline JSON as the second argument. Prefix the input with '@' to indicate a file path. +`, + Example: ` +# Create a resource provider (from file) +rad resourceprovider create Applications.Example @/path/to/input.json + +# Create a resource provider (inline) +rad resourceprovider create Applications.Example '{ ... }'`, + Args: cobra.ExactArgs(2), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourceprovider create` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace + + ResourceProviderNamespace string + ResourceProvider *v20231001preview.ResourceProviderResource +} + +// NewRunner creates an instance of the runner for the `rad resourceprovider create` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourceprovider create` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + // Validate command line args and + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.ResourceProviderNamespace = args[0] + r.ResourceProvider, err = readInput(args[1]) + if err != nil { + return err + } + + return nil +} + +func readInput(arg string) (*v20231001preview.ResourceProviderResource, error) { + var bs []byte + if strings.HasPrefix(arg, "@") { + inputFile := strings.TrimPrefix(arg, "@") + + var err error + bs, err = os.ReadFile(inputFile) + if err != nil { + return nil, clierrors.Message("Failed to read input file: %v", err) + } + } else { + bs = []byte(arg) + + } + + resource := v20231001preview.ResourceProviderResource{} + err := json.NewDecoder(strings.NewReader(string(bs))).Decode(&resource) + if err != nil { + return nil, clierrors.Message("Invalid input, could not be converted to a resource provider: %v", err) + } + + return &resource, nil +} + +// Run runs the `rad resourceprovider create` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + response, err := client.CreateOrUpdateResourceProvider(ctx, "local", r.ResourceProviderNamespace, r.ResourceProvider) + if err != nil { + return err + } + + err = r.Output.WriteFormatted(r.Format, response, common.GetResourceProviderTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resourceprovider/create/create_test.go b/pkg/cli/cmd/resourceprovider/create/create_test.go new file mode 100644 index 00000000000..7740c76ba72 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/create/create_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package create + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + + resourceProviderData, err := os.ReadFile("testdata/resourceprovider.json") + require.NoError(t, err) + + directory := t.TempDir() + err = os.WriteFile(filepath.Join(directory, "valid-resourceprovider.json"), resourceProviderData, 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(directory, "invalid-resourceprovider.json"), []byte("{askdfe}"), 0644) + require.NoError(t, err) + + testcases := []radcli.ValidateInput{ + { + Name: "Valid: inline JSON", + Input: []string{"Applications.Test", string(resourceProviderData)}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Valid: JSON file", + Input: []string{"Applications.Test", "@valid-resourceprovider.json"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + CreateTempDirectory: directory, + }, + { + Name: "Valid: inline non-JSON", + Input: []string{"Applications.Test", "{askdfe}"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: non-JSON file", + Input: []string{"Applications.Test", "@invalid-resourceprovider.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + CreateTempDirectory: directory, + }, + { + Name: "Invalid: missing arguments", + Input: []string{"Applications.Test", "@valid-resourceprovider.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"Applications.Test", "@valid-resourceprovider.json", "dddddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: resource created", func(t *testing.T) { + ctrl := gomock.NewController(t) + + resourceProviderData, err := os.ReadFile("testdata/resourceprovider.json") + require.NoError(t, err) + + expectedResourceProvider := &v20231001preview.ResourceProviderResource{} + err = json.Unmarshal(resourceProviderData, expectedResourceProvider) + require.NoError(t, err) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + CreateOrUpdateResourceProvider(gomock.Any(), "local", "Applications.Test", expectedResourceProvider). + Return(*expectedResourceProvider, nil). + Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + ResourceProviderNamespace: "Applications.Test", + ResourceProvider: expectedResourceProvider, + Format: "table", + } + + err = runner.Run(context.Background()) + require.NoError(t, err) + }) +} diff --git a/pkg/cli/cmd/resourceprovider/create/testdata/resourceprovider.json b/pkg/cli/cmd/resourceprovider/create/testdata/resourceprovider.json new file mode 100644 index 00000000000..574c2b8de45 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/create/testdata/resourceprovider.json @@ -0,0 +1,35 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "internal" + } + }, + "resourceTypes": [ + { + "resourceType": "exampleResources", + "routingType": "Internal", + "locations": [ + "global" + ], + "apiVersions": { + "2024-10-01-preview": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + }, + "capabilities": [ + "Recipe" + ], + "defaultApiVersion": "2024-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/pkg/cli/cmd/resourceprovider/delete/delete.go b/pkg/cli/cmd/resourceprovider/delete/delete.go new file mode 100644 index 00000000000..5b34a1de65b --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/delete/delete.go @@ -0,0 +1,146 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package delete + +import ( + "context" + "fmt" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/prompt" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/spf13/cobra" +) + +const ( + deleteConfirmation = "Are you sure you want to delete resource provider %q?" +) + +// NewCommand creates an instance of the `rad resourceprovider delete` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "delete [resource provider namespace]", + Short: "Delete resource provider", + Long: `Delete resource provider + +Resource providers are the entities that implement resource types such as 'Applications.Core/containers'. Resource providers can be defined, registered, and unregistered by users. + +Built-in resource providers like 'Applications.Core' can be deleted. + +Deleting a resource provider will delete all resources of the resource provider.`, + Example: ` +# Delete a resource provider +rad resourceprovider delete Applications.Core`, + Args: cobra.ExactArgs(1), + RunE: framework.RunCommand(runner), + } + + commonflags.AddConfirmationFlag(cmd) + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourceprovider delete` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + InputPrompter prompt.Interface + Output output.Interface + Format string + Workspace *workspaces.Workspace + + Confirm bool + ResourceProviderNamespace string +} + +// NewRunner creates an instance of the runner for the `rad resourceprovider delete` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + InputPrompter: factory.GetPrompter(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourceprovider delete` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.Confirm, err = cmd.Flags().GetBool("yes") + if err != nil { + return err + } + + r.ResourceProviderNamespace = args[0] + + return nil +} + +// Run runs the `rad resourceprovider delete` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + // Prompt user to confirm deletion + if !r.Confirm { + confirmed, err := prompt.YesOrNoPrompt(fmt.Sprintf(deleteConfirmation, r.ResourceProviderNamespace), prompt.ConfirmNo, r.InputPrompter) + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + deleted, err := client.DeleteResourceProvider(ctx, "local", r.ResourceProviderNamespace) + if clients.Is404Error(err) { + return clierrors.Message("The resource provider %q was not found or has been deleted.", r.ResourceProviderNamespace) + } else if err != nil { + return err + } + + if deleted { + r.Output.LogInfo("Resource provider %q deleted.", r.ResourceProviderNamespace) + } else { + r.Output.LogInfo("Resource provider %q does not exist or has already been deleted.", r.ResourceProviderNamespace) + } + + return nil +} diff --git a/pkg/cli/cmd/resourceprovider/delete/delete_test.go b/pkg/cli/cmd/resourceprovider/delete/delete_test.go new file mode 100644 index 00000000000..d427343db98 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/delete/delete_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package delete + +import ( + "context" + "fmt" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/prompt" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{"Applications.Test"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"Applications.Test", "dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: not enough many arguments", + Input: []string{}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: Resource Provider Deleted", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + DeleteResourceProvider(gomock.Any(), "local", "Applications.Test"). + Return(true, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceProviderNamespace: "Applications.Test", + Confirm: true, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Resource provider %q deleted.", + Params: []any{"Applications.Test"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Resource Provider Not Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + DeleteResourceProvider(gomock.Any(), "local", "Applications.Test"). + Return(false, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceProviderNamespace: "Applications.Test", + Confirm: true, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Resource provider %q does not exist or has already been deleted.", + Params: []any{"Applications.Test"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Confirmed", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + DeleteResourceProvider(gomock.Any(), "local", "Applications.Test"). + Return(true, nil). + Times(1) + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmation, "Applications.Test")). + Return(prompt.ConfirmYes, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + InputPrompter: promptMock, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceProviderNamespace: "Applications.Test", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Resource provider %q deleted.", + Params: []any{"Applications.Test"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) +} diff --git a/pkg/cli/cmd/resourceprovider/list/list.go b/pkg/cli/cmd/resourceprovider/list/list.go new file mode 100644 index 00000000000..44c2d69d715 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/list/list.go @@ -0,0 +1,115 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "context" + "slices" + "strings" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourceprovider list` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "list", + Short: "List resource providers", + Long: `List resource providers + +Resource providers are the entities that implement resource types such as 'Applications.Core/containers'. Resource providers can be defined, registered, and unregistered by users.`, + Example: ` +# List all resource providers +rad resourceprovider list`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourceprovider list` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace +} + +// NewRunner creates an instance of the runner for the `rad resourceprovider list` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourceprovider list` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + return nil +} + +// Run runs the `rad resourceprovider list` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + resourceProviders, err := client.ListResourceProviderSummaries(ctx, "local") + if err != nil { + return err + } + + slices.SortFunc(resourceProviders, func(a v20231001preview.ResourceProviderSummary, b v20231001preview.ResourceProviderSummary) int { + return strings.Compare(*a.Name, *b.Name) + }) + + err = r.Output.WriteFormatted(r.Format, resourceProviders, common.GetResourceProviderTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resourceprovider/list/list_test.go b/pkg/cli/cmd/resourceprovider/list/list_test.go new file mode 100644 index 00000000000..3862148a79c --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/list/list_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "context" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceProviders := []v20231001preview.ResourceProviderSummary{ + { + Name: to.Ptr("Applications.Test1"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources1": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + }, + { + Name: to.Ptr("Applications.Test2"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources2": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + "exampleResources3": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + }, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + ListResourceProviderSummaries(gomock.Any(), "local"). + Return(resourceProviders, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: resourceProviders, + Options: common.GetResourceProviderTableFormat(), + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) +} diff --git a/pkg/cli/cmd/resourceprovider/new/new.go b/pkg/cli/cmd/resourceprovider/new/new.go new file mode 100644 index 00000000000..50a0fcf1468 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/new/new.go @@ -0,0 +1,161 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package new + +import ( + "context" + "errors" + "os" + "os/exec" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourceprovider new` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "new", + Short: "Scaffold new resource provider", + Long: `Scaffold new resource provider + +This command outputs a JSON template that can be used to create a new resource provider. + +Resource providers are the entities that implement resource types such as 'Applications.Core/containers'. Resource providers can be defined, registered, and unregistered by users.`, + Example: ` +# Scaffold a new resource provider called 'Applications.Example' +rad resourceprovider new Applications.Example`, + Args: cobra.ExactArgs(1), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + cmd.Flags().BoolVar(&runner.Edit, "edit", false, "Open the new resource provider in the editor. Requires $EDITOR to be set.") + cmd.Flags().BoolVarP(&runner.Force, "force", "f", false, "Overwrite existing file if present") + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourceprovider new` command. +type Runner struct { + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + + ResourceProviderNamespace string + Edit bool + Force bool +} + +// NewRunner creates an instance of the runner for the `rad resourceprovider new` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourceprovider new` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.ResourceProviderNamespace = args[0] + + return nil +} + +// Run runs the `rad resourceprovider new` command. +func (r *Runner) Run(ctx context.Context) error { + fileName := r.ResourceProviderNamespace + ".json" + + _, err := os.Stat(fileName) + if errors.Is(err, os.ErrNotExist) { + // Nothing to do, file doesn't exist. + } else if err != nil { + return err + } else if !r.Force { + return clierrors.Message("File %q already exists, use --force to overwrite.", fileName) + } + + template := `{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "internal" + } + }, + "resourceTypes": [ + { + "resourceType": "example", + "routingType": "Internal", + "locations": [ + "global" + ], + "apiVersions": { + "2024-10-01-preview": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + }, + "capabilities": [ + "Recipe" + ], + "defaultApiVersion": "2024-10-01-preview" + } + ] + } +}` + + err = os.WriteFile(fileName, []byte(template), 0644) + if err != nil { + return err + } + + r.Output.LogInfo("Wrote template to: %s", fileName) + + if r.Edit { + editor := os.Getenv("EDITOR") + if editor == "" { + r.Output.LogInfo("warning: EDITOR environment variable is not set") + return nil + } + + cmd := exec.CommandContext(ctx, editor, fileName) + _ = cmd.Run() // Don't wait for command to finish + } + + return nil +} diff --git a/pkg/cli/cmd/resourceprovider/new/new_test.go b/pkg/cli/cmd/resourceprovider/new/new_test.go new file mode 100644 index 00000000000..aef81126d3a --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/new/new_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package new + +import ( + "context" + "os" + "testing" + + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{"Applications.Test"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: missing args", + Input: []string{}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many args", + Input: []string{"Applications.Test", "dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + expected, err := os.ReadFile("testdata/expected-output.json") + require.NoError(t, err) + + t.Run("Success: Scaffold resource provider", func(t *testing.T) { + original, err := os.Getwd() + require.NoError(t, err) + + directory := t.TempDir() + require.NoError(t, os.Chdir(directory)) + defer func() { + require.NoError(t, os.Chdir(original)) + }() + + mockOutput := &output.MockOutput{} + + runner := &Runner{ + Output: mockOutput, + ResourceProviderNamespace: "Applications.Test", + } + + err = runner.Run(context.Background()) + require.NoError(t, err) + + actual, err := os.ReadFile("Applications.Test.json") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + + expectedOutput := []any{ + output.LogOutput{ + Format: "Wrote template to: %s", + Params: []any{"Applications.Test.json"}, + }, + } + require.Equal(t, expectedOutput, mockOutput.Writes) + }) + + t.Run("Success: File exists -> force", func(t *testing.T) { + original, err := os.Getwd() + require.NoError(t, err) + + directory := t.TempDir() + require.NoError(t, os.Chdir(directory)) + defer func() { + require.NoError(t, os.Chdir(original)) + }() + + mockOutput := &output.MockOutput{} + + runner := &Runner{ + Output: mockOutput, + ResourceProviderNamespace: "Applications.Test", + Force: true, + } + + err = os.WriteFile("Applications.Test.json", []byte("{}"), 0644) + require.NoError(t, err) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + actual, err := os.ReadFile("Applications.Test.json") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + + expectedOutput := []any{ + output.LogOutput{ + Format: "Wrote template to: %s", + Params: []any{"Applications.Test.json"}, + }, + } + require.Equal(t, expectedOutput, mockOutput.Writes) + }) + + t.Run("Error: File exists -> canceled", func(t *testing.T) { + original, err := os.Getwd() + require.NoError(t, err) + + directory := t.TempDir() + require.NoError(t, os.Chdir(directory)) + defer func() { + require.NoError(t, os.Chdir(original)) + }() + + mockOutput := &output.MockOutput{} + + runner := &Runner{ + Output: mockOutput, + ResourceProviderNamespace: "Applications.Test", + } + + err = os.WriteFile("Applications.Test.json", []byte("{}"), 0644) + require.NoError(t, err) + + err = runner.Run(context.Background()) + require.Equal(t, err, clierrors.Message("File \"Applications.Test.json\" already exists, use --force to overwrite.")) + }) +} diff --git a/pkg/cli/cmd/resourceprovider/new/testdata/expected-output.json b/pkg/cli/cmd/resourceprovider/new/testdata/expected-output.json new file mode 100644 index 00000000000..ff90c03b65c --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/new/testdata/expected-output.json @@ -0,0 +1,35 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "internal" + } + }, + "resourceTypes": [ + { + "resourceType": "example", + "routingType": "Internal", + "locations": [ + "global" + ], + "apiVersions": { + "2024-10-01-preview": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + }, + "capabilities": [ + "Recipe" + ], + "defaultApiVersion": "2024-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/pkg/cli/cmd/resourceprovider/show/show.go b/pkg/cli/cmd/resourceprovider/show/show.go new file mode 100644 index 00000000000..b755343b229 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/show/show.go @@ -0,0 +1,115 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package show + +import ( + "context" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourceprovider show` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "show [resource provider namespace]", + Short: "Show resource provider", + Long: `Show resource provider + +Resource providers are the entities that implement resource types such as 'Applications.Core/containers'. Resource providers can be defined, registered, and unregistered by users.`, + Example: ` +# Show a resource provider +rad resourceprovider show Applications.Core`, + Args: cobra.ExactArgs(1), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourceprovider show` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace + ResourceProviderNamespace string +} + +// NewRunner creates an instance of the runner for the `rad resourceprovider show` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourceprovider show` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.ResourceProviderNamespace = args[0] + + return nil +} + +// Run runs the `rad resourceprovider show` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + resourceProviders, err := client.GetResourceProviderSummary(ctx, "local", r.ResourceProviderNamespace) + if clients.Is404Error(err) { + return clierrors.Message("The resource provider %q was not found or has been deleted.", r.ResourceProviderNamespace) + } else if err != nil { + return err + } + + err = r.Output.WriteFormatted(r.Format, resourceProviders, common.GetResourceProviderTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resourceprovider/show/show_test.go b/pkg/cli/cmd/resourceprovider/show/show_test.go new file mode 100644 index 00000000000..7adf0b0bbe8 --- /dev/null +++ b/pkg/cli/cmd/resourceprovider/show/show_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package show + +import ( + "context" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{"Applications.Test"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"Applications.Test", "dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: not enough many arguments", + Input: []string{}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + resourceProvider := v20231001preview.ResourceProviderSummary{ + Name: to.Ptr("Applications.Test"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + } + + t.Run("Success: Resource Provider Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResourceProviderSummary(gomock.Any(), "local", "Applications.Test"). + Return(resourceProvider, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceProviderNamespace: "Applications.Test", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: resourceProvider, + Options: common.GetResourceProviderTableFormat(), + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Error: Resource Provider Not Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResourceProviderSummary(gomock.Any(), "local", "Applications.AnotherTest"). + Return(v20231001preview.ResourceProviderSummary{}, radcli.Create404Error()). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceProviderNamespace: "Applications.AnotherTest", + } + + err := runner.Run(context.Background()) + require.Error(t, err) + require.Equal(t, clierrors.Message("The resource provider \"Applications.AnotherTest\" was not found or has been deleted."), err) + + require.Empty(t, outputSink.Writes) + }) +} diff --git a/pkg/cli/cmd/resourcetype/common/resourcetype.go b/pkg/cli/cmd/resourcetype/common/resourcetype.go new file mode 100644 index 00000000000..cf806978ea5 --- /dev/null +++ b/pkg/cli/cmd/resourcetype/common/resourcetype.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" +) + +// ResourceType is used by the CLI for display of resource types. +type ResourceType struct { + // Name is the fully-qualified name of the resource type. + Name string + // ResourceProviderNamespace is the namespace of the resource provider. + ResourceProviderNamespace string + // APIVersions is the list of API versions supported by the resource type. + APIVersions []string +} + +// ResourceTypesForProvider returns a list of resource types for a given provider. +func ResourceTypesForProvider(provider *v20231001preview.ResourceProviderSummary) []ResourceType { + resourceTypes := []ResourceType{} + for name, resourceType := range provider.ResourceTypes { + rt := ResourceType{ + Name: *provider.Name + "/" + name, + ResourceProviderNamespace: *provider.Name, + } + + for version := range resourceType.APIVersions { + rt.APIVersions = append(rt.APIVersions, version) + } + + resourceTypes = append(resourceTypes, rt) + } + return resourceTypes +} + +// GetResourceTypeTableFormat returns the fields to output from a resource type object. +func GetResourceTypeTableFormat() output.FormatterOptions { + return output.FormatterOptions{ + Columns: []output.Column{ + { + Heading: "TYPE", + JSONPath: "{ .Name }", + }, + { + Heading: "NAMESPACE", + JSONPath: "{ .ResourceProviderNamespace }", + }, + { + Heading: "APIVERSION", + JSONPath: "{ .APIVersions }", + }, + }, + } +} diff --git a/pkg/cli/cmd/resourcetype/list/list.go b/pkg/cli/cmd/resourcetype/list/list.go new file mode 100644 index 00000000000..07ebe84249a --- /dev/null +++ b/pkg/cli/cmd/resourcetype/list/list.go @@ -0,0 +1,119 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "context" + "slices" + "strings" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourcetype list` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "list", + Short: "List resource resource types", + Long: `List resource resource types + +Resource types are the entities that can be created and managed by Radius such as 'Applications.Core/containers'. Resource types can be configured using resource providers.`, + Example: ` +# List all resource types +rad resource type list`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourcetype list` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace +} + +// NewRunner creates an instance of the runner for the `rad resourcetype list` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourcetype list` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + return nil +} + +// Run runs the `rad resourceprovider list` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + resourceProviders, err := client.ListResourceProviderSummaries(ctx, "local") + if err != nil { + return err + } + + resourceTypes := []common.ResourceType{} + for _, resourceProvider := range resourceProviders { + resourceTypes = append(resourceTypes, common.ResourceTypesForProvider(&resourceProvider)...) + } + + slices.SortFunc(resourceTypes, func(a common.ResourceType, b common.ResourceType) int { + return strings.Compare(a.Name, b.Name) + }) + + err = r.Output.WriteFormatted(r.Format, resourceTypes, common.GetResourceTypeTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resourcetype/list/list_test.go b/pkg/cli/cmd/resourcetype/list/list_test.go new file mode 100644 index 00000000000..7fc7adc5012 --- /dev/null +++ b/pkg/cli/cmd/resourcetype/list/list_test.go @@ -0,0 +1,145 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "context" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceProviders := []v20231001preview.ResourceProviderSummary{ + { + Name: to.Ptr("Applications.Test1"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources1": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + }, + { + Name: to.Ptr("Applications.Test2"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources2": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + "exampleResources3": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + }, + } + + resourceTypes := []common.ResourceType{ + { + Name: "Applications.Test1/exampleResources1", + ResourceProviderNamespace: "Applications.Test1", + APIVersions: []string{"2023-10-01-preview"}, + }, + { + Name: "Applications.Test2/exampleResources2", + ResourceProviderNamespace: "Applications.Test2", + APIVersions: []string{"2023-10-01-preview"}, + }, + { + Name: "Applications.Test2/exampleResources3", + ResourceProviderNamespace: "Applications.Test2", + APIVersions: []string{"2023-10-01-preview"}, + }, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + ListResourceProviderSummaries(gomock.Any(), "local"). + Return(resourceProviders, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: resourceTypes, + Options: common.GetResourceTypeTableFormat(), + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) +} diff --git a/pkg/cli/cmd/resourcetype/show/show.go b/pkg/cli/cmd/resourcetype/show/show.go new file mode 100644 index 00000000000..da098efb4fc --- /dev/null +++ b/pkg/cli/cmd/resourcetype/show/show.go @@ -0,0 +1,136 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package show + +import ( + "context" + "slices" + "strings" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad resourcetype show` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "show", + Short: "Show resource resource type", + Long: `Show resource resource type + +Resource types are the entities that can be created and managed by Radius such as 'Applications.Core/containers'. Resource types can be configured using resource providers.`, + Example: ` +# Show a resource types +rad resourcetype show 'Applications.Core/containers'`, + Args: cobra.ExactArgs(1), + RunE: framework.RunCommand(runner), + } + + commonflags.AddOutputFlag(cmd) + commonflags.AddWorkspaceFlag(cmd) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad resourcetype show` command. +type Runner struct { + ConnectionFactory connections.Factory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace + + ResourceTypeName string + ResourceProviderNamespace string + ResourceTypeSuffix string +} + +// NewRunner creates an instance of the runner for the `rad resourcetype show` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad resourcetype show` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + r.Workspace = workspace + + format, err := cli.RequireOutput(cmd) + if err != nil { + return err + } + r.Format = format + + r.ResourceTypeName = args[0] + parts := strings.Split(r.ResourceTypeName, "/") + if len(parts) != 2 { + return clierrors.Message("Invalid resource type %q. Expected format: '/'", r.ResourceTypeName) + } + + r.ResourceProviderNamespace = parts[0] + r.ResourceTypeSuffix = parts[1] + + return nil +} + +// Run runs the `rad resourceprovider show` command. +func (r *Runner) Run(ctx context.Context) error { + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) + if err != nil { + return err + } + + resourceProvider, err := client.GetResourceProviderSummary(ctx, "local", r.ResourceProviderNamespace) + if clients.Is404Error(err) { + return clierrors.Message("The resource provider %q was not found or has been deleted.", r.ResourceProviderNamespace) + } else if err != nil { + return err + } + + resourceTypes := common.ResourceTypesForProvider(&resourceProvider) + idx := slices.IndexFunc(resourceTypes, func(rt common.ResourceType) bool { + return rt.Name == r.ResourceTypeName + }) + + if idx < 0 { + return clierrors.Message("Resource type %q not found in resource provider %q.", r.ResourceTypeSuffix, r.ResourceProviderNamespace) + } + + err = r.Output.WriteFormatted(r.Format, resourceTypes[idx], common.GetResourceTypeTableFormat()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/resourcetype/show/show_test.go b/pkg/cli/cmd/resourcetype/show/show_test.go new file mode 100644 index 00000000000..1d7553f2944 --- /dev/null +++ b/pkg/cli/cmd/resourcetype/show/show_test.go @@ -0,0 +1,204 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package show + +import ( + "context" + "testing" + + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/common" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/to" + "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" + "github.com/radius-project/radius/test/radcli" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + config := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + { + Name: "Valid", + Input: []string{"Applications.Test/exampleResources"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: not a resource type", + Input: []string{"Applications.Test"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: too many arguments", + Input: []string{"Applications.Test/exampleResources", "dddd"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: not enough many arguments", + Input: []string{}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + resourceProvider := v20231001preview.ResourceProviderSummary{ + Name: to.Ptr("Applications.Test"), + ResourceTypes: map[string]*v20231001preview.ResourceProviderSummaryResourceType{ + "exampleResources": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + } + + t.Run("Success: Resource Type Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + resourceType := common.ResourceType{ + Name: "Applications.Test/exampleResources", + ResourceProviderNamespace: "Applications.Test", + APIVersions: []string{"2023-10-01-preview"}, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResourceProviderSummary(gomock.Any(), "local", "Applications.Test"). + Return(resourceProvider, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceTypeName: "Applications.Test/exampleResources", + ResourceProviderNamespace: "Applications.Test", + ResourceTypeSuffix: "exampleResources", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: resourceType, + Options: common.GetResourceTypeTableFormat(), + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Error: Resource Provider Not Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResourceProviderSummary(gomock.Any(), "local", "Applications.AnotherTest"). + Return(v20231001preview.ResourceProviderSummary{}, radcli.Create404Error()). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceTypeName: "Applications.AnotherTest/exampleResources", + ResourceProviderNamespace: "Applications.AnotherTest", + ResourceTypeSuffix: "exampleResources", + } + + err := runner.Run(context.Background()) + require.Error(t, err) + require.Equal(t, clierrors.Message("The resource provider \"Applications.AnotherTest\" was not found or has been deleted."), err) + + require.Empty(t, outputSink.Writes) + }) + + t.Run("Error: Resource Type Not Found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResourceProviderSummary(gomock.Any(), "local", "Applications.Test"). + Return(resourceProvider, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Name: "kind-kind", + Scope: "/planes/radius/local/resourceGroups/test-group", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Format: "table", + Output: outputSink, + ResourceTypeName: "Applications.Test/anotherResources", + ResourceProviderNamespace: "Applications.Test", + ResourceTypeSuffix: "anotherResources", + } + + err := runner.Run(context.Background()) + require.Error(t, err) + require.Equal(t, clierrors.Message("Resource type \"anotherResources\" not found in resource provider \"Applications.Test\"."), err) + + require.Empty(t, outputSink.Writes) + }) +} diff --git a/test/radcli/shared.go b/test/radcli/shared.go index 49f58b44a86..fb891f72aef 100644 --- a/test/radcli/shared.go +++ b/test/radcli/shared.go @@ -123,10 +123,15 @@ func SharedValidateValidation(t *testing.T, factory func(framework framework.Fac } if testcase.CreateTempDirectory != "" { + // Allow the test to specify a relative or absolute path. + directoryPath := testcase.CreateTempDirectory + if !filepath.IsAbs(testcase.CreateTempDirectory) { + tempRoot := t.TempDir() + directoryPath = filepath.Join(tempRoot, testcase.CreateTempDirectory) + } + // Will be automatically deleted after the test - tempRoot := t.TempDir() - combined := filepath.Join(tempRoot, testcase.CreateTempDirectory) - err := os.MkdirAll(combined, 0775) + err := os.MkdirAll(directoryPath, 0775) require.NoError(t, err) wd, err := os.Getwd() @@ -136,7 +141,7 @@ func SharedValidateValidation(t *testing.T, factory func(framework framework.Fac }() // Change to the new directory before running the test code. - err = os.Chdir(combined) + err = os.Chdir(directoryPath) require.NoError(t, err) } diff --git a/udt-examples/Applications.Core.json b/udt-examples/Applications.Core.json new file mode 100644 index 00000000000..8e47d517cd2 --- /dev/null +++ b/udt-examples/Applications.Core.json @@ -0,0 +1,110 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "http://localhost:8080" + } + }, + "resourceTypes": [ + { + "resourceType": "applications", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "containers", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "environments", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "extenders", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "gateways", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "secretStores", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "volumes", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/udt-examples/Applications.Dapr.json b/udt-examples/Applications.Dapr.json new file mode 100644 index 00000000000..826672a22f0 --- /dev/null +++ b/udt-examples/Applications.Dapr.json @@ -0,0 +1,54 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "http://localhost:8080" + } + }, + "resourceTypes": [ + { + "resourceType": "mongoDatabases", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "redisCaches", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "sqlDatabases", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/udt-examples/Applications.Datastores.json b/udt-examples/Applications.Datastores.json new file mode 100644 index 00000000000..c1f0318616a --- /dev/null +++ b/udt-examples/Applications.Datastores.json @@ -0,0 +1,54 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "http://localhost:8080" + } + }, + "resourceTypes": [ + { + "resourceType": "pubSubBrokers", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "secretStores", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + }, + { + "resourceType": "stateStores", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/udt-examples/Applications.Messaging.json b/udt-examples/Applications.Messaging.json new file mode 100644 index 00000000000..dc219eb5ce9 --- /dev/null +++ b/udt-examples/Applications.Messaging.json @@ -0,0 +1,26 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "http://localhost:8080" + } + }, + "resourceTypes": [ + { + "resourceType": "rabbitMQQueues", + "routingType": "Provider", + "locations": [ + "global" + ], + "apiVersions": { + "2023-10-01-preview": { + "schema": {} + } + }, + "capabilities": [], + "defaultApiVersion": "2023-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/udt-examples/Contoso.Example.json b/udt-examples/Contoso.Example.json new file mode 100644 index 00000000000..574c2b8de45 --- /dev/null +++ b/udt-examples/Contoso.Example.json @@ -0,0 +1,35 @@ +{ + "location": "global", + "properties": { + "locations": { + "global": { + "address": "internal" + } + }, + "resourceTypes": [ + { + "resourceType": "exampleResources", + "routingType": "Internal", + "locations": [ + "global" + ], + "apiVersions": { + "2024-10-01-preview": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + }, + "capabilities": [ + "Recipe" + ], + "defaultApiVersion": "2024-10-01-preview" + } + ] + } +} \ No newline at end of file diff --git a/udt-examples/exampleResources.json b/udt-examples/exampleResources.json new file mode 100644 index 00000000000..83bb3641726 --- /dev/null +++ b/udt-examples/exampleResources.json @@ -0,0 +1,5 @@ +{ + "properties": { + "message": "Hello, world!" + } +} \ No newline at end of file