diff --git a/cmd/rad/cmd/root.go b/cmd/rad/cmd/root.go index 87c9f9b563..e360d5ba00 100644 --- a/cmd/rad/cmd/root.go +++ b/cmd/rad/cmd/root.go @@ -62,6 +62,7 @@ import ( 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_show "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/show" + resourcetype_create "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/create" resourcetype_delete "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/delete" resourcetype_list "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/list" resourcetype_show "github.com/radius-project/radius/pkg/cli/cmd/resourcetype/show" @@ -273,6 +274,9 @@ func initSubCommands() { resourceTypeDeleteCmd, _ := resourcetype_delete.NewCommand(framework) resourceTypeCmd.AddCommand(resourceTypeDeleteCmd) + resourceTypeCreateCmd, _ := resourcetype_create.NewCommand(framework) + resourceTypeCmd.AddCommand(resourceTypeCreateCmd) + listRecipeCmd, _ := recipe_list.NewCommand(framework) recipeCmd.AddCommand(listRecipeCmd) diff --git a/pkg/cli/cmd/resourcetype/create/create.go b/pkg/cli/cmd/resourcetype/create/create.go index 40ec34eed9..792a96f4e6 100644 --- a/pkg/cli/cmd/resourcetype/create/create.go +++ b/pkg/cli/cmd/resourcetype/create/create.go @@ -19,16 +19,17 @@ package create import ( "context" - v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "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/cmd/resourcetype/common" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/manifest" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/pkg/sdk" "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" "github.com/spf13/cobra" ) @@ -55,38 +56,39 @@ rad resource-type create myType --from-file /path/to/input.yaml # Create a resource type from JSON file rad resource-type create myType --from-file /path/to/input.json `, - Args: cobra.ExactArgs(0), + Args: cobra.ExactArgs(1), RunE: framework.RunCommand(runner), } commonflags.AddOutputFlag(cmd) commonflags.AddWorkspaceFlag(cmd) commonflags.AddFromFileFlagVar(cmd, &runner.ResourceProviderManifestFilePath) - _ = cmd.MarkFlagRequired("from-file") - _ = cmd.MarkFlagFilename("from-file", "yaml", "json") 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 + UCPClientFactory *v20231001preview.ClientFactory + ConfigHolder *framework.ConfigHolder + Output output.Interface + Format string + Workspace *workspaces.Workspace ResourceProviderManifestFilePath string ResourceProvider *manifest.ResourceProvider ResourceTypeName string + Logger func(format string, args ...any) } // 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(), + ConfigHolder: factory.GetConfigHolder(), + Output: factory.GetOutput(), + Logger: func(format string, args ...any) { + output.LogInfo(format, args...) + }, } } @@ -126,64 +128,53 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { // Run runs the `rad resourcetype create` command. func (r *Runner) Run(ctx context.Context) error { - client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) - if err != nil { - return err + // Initialize the client factory if it hasn't been set externally. + // This allows for flexibility where a test UCPClientFactory can be set externally during testing. + if r.UCPClientFactory == nil { + err := r.initializeClientFactory(ctx, r.Workspace) + if err != nil { + return err + } } - r.Output.LogInfo("Checking resource provider %q exists", r.ResourceProvider.Name) - _, err = client.GetResourceProvider(ctx, "local", r.ResourceProvider.Name) - - if clients.Is404Error(err) { - return clierrors.Message("Resource Provider %q not found. Please create using `rad resorce-provider create` before creating a resource-type", r.ResourceProvider.Name) - } else if err != nil { - return err + //response, err := r.UCPClientFactory.NewResourceProvidersClient().Get(ctx, "local", r.ResourceProvider.Name, nil) + response, err := r.UCPClientFactory.NewResourceTypesClient().Get(ctx, "local", r.ResourceProvider.Name, r.ResourceTypeName, nil) + if err != nil { + r.Output.LogInfo("Resource provider %q not found.", r.ResourceProvider.Name) + if err := manifest.RegisterFile(ctx, r.UCPClientFactory, "local", r.ResourceProviderManifestFilePath, r.Logger); err != nil { + return err + } + } else { + r.Output.LogInfo("Resource provider %q found. Registering resource type %q.", r.ResourceProvider.Name, r.ResourceTypeName) + if err := manifest.RegisterType(ctx, r.UCPClientFactory, "local", r.ResourceProviderManifestFilePath, r.ResourceTypeName, r.Logger); err != nil { + return err + } } - // The location resource contains references to all of the resource types and API versions that the resource provider supports. - // We're instantiating the struct here so we can update it as we loop. - locationResource := v20231001preview.LocationResource{ - Properties: &v20231001preview.LocationProperties{ - ResourceTypes: map[string]*v20231001preview.LocationResourceType{}, - }, - } + // Add a blank line before printing the result. + r.Output.LogInfo("") - resourceType := r.ResourceProvider.Types[r.ResourceTypeName] - r.Output.LogInfo("Creating resource type %s/%s", r.ResourceProvider.Name, r.ResourceTypeName) - _, err = client.CreateOrUpdateResourceType(ctx, "local", r.ResourceProvider.Name, r.ResourceTypeName, &v20231001preview.ResourceTypeResource{ - Properties: &v20231001preview.ResourceTypeProperties{ - DefaultAPIVersion: resourceType.DefaultAPIVersion, - }, - }) + err = r.Output.WriteFormatted(r.Format, response, common.GetResourceTypeTableFormat()) if err != nil { return err } - locationResourceType := &v20231001preview.LocationResourceType{ - APIVersions: map[string]map[string]any{}, - } - - for apiVersionName := range resourceType.APIVersions { - r.Output.LogInfo("Creating API Version %s/%s@%s", r.ResourceProvider.Name, r.ResourceTypeName, apiVersionName) - _, err := client.CreateOrUpdateAPIVersion(ctx, "local", r.ResourceProvider.Name, r.ResourceTypeName, apiVersionName, &v20231001preview.APIVersionResource{ - Properties: &v20231001preview.APIVersionProperties{}, - }) - if err != nil { - return err - } + return nil +} - locationResourceType.APIVersions[apiVersionName] = map[string]any{} +func (r *Runner) initializeClientFactory(ctx context.Context, workspace *workspaces.Workspace) error { + connection, err := workspace.Connect(ctx) + if err != nil { + return err } - locationResource.Properties.ResourceTypes[r.ResourceTypeName] = locationResourceType + clientOptions := sdk.NewClientOptions(connection) - r.Output.LogInfo("Creating location %s/%s", r.ResourceProvider.Name, v1.LocationGlobal) - _, err = client.CreateOrUpdateLocation(ctx, "local", r.ResourceProvider.Name, v1.LocationGlobal, &locationResource) + clientFactory, err := v20231001preview.NewClientFactory(&aztoken.AnonymousCredential{}, clientOptions) if err != nil { return err } - r.Output.LogInfo("Resource type %s/%s created successfully", r.ResourceProvider.Name, r.ResourceTypeName) - + r.UCPClientFactory = clientFactory return nil } diff --git a/pkg/cli/cmd/resourcetype/create/create_test.go b/pkg/cli/cmd/resourcetype/create/create_test.go new file mode 100644 index 0000000000..79450e0c52 --- /dev/null +++ b/pkg/cli/cmd/resourcetype/create/create_test.go @@ -0,0 +1,97 @@ +/* +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 ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/manifest" + "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" +) + +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{"coolResources", "--from-file", "testdata/valid.yaml"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: Error in manifest", + Input: []string{"testResources", "--from-file", "testdata/missing-required-field.yaml"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + { + Name: "Invalid: missing arguments", + Input: []string{"--from-file", "testdata/valid.yaml"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{Config: config}, + }, + } + + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: resource type created", func(t *testing.T) { + + resourceProviderData, err := manifest.ReadFile("testdata/valid.yaml") + require.NoError(t, err) + + expectedResourceType := "testResources" + + clientFactory, err := manifest.NewTestClientFactory() + require.NoError(t, err) + + var logBuffer bytes.Buffer + logger := func(format string, args ...any) { + fmt.Fprintf(&logBuffer, format+"\n", args...) + } + + runner := &Runner{ + UCPClientFactory: clientFactory, + Output: &output.MockOutput{}, + Workspace: &workspaces.Workspace{}, + ResourceProvider: resourceProviderData, + Format: "table", + Logger: logger, + ResourceProviderManifestFilePath: "testdata/valid.yaml", + ResourceTypeName: expectedResourceType, + } + + err = runner.Run(context.Background()) + require.NoError(t, err) + + logOutput := logBuffer.String() + require.Contains(t, logOutput, fmt.Sprintf("Resource type %s/%s created successfully", resourceProviderData.Name, expectedResourceType)) + }) +} diff --git a/pkg/cli/cmd/resourcetype/create/testdata/missing-required-field.yaml b/pkg/cli/cmd/resourcetype/create/testdata/missing-required-field.yaml new file mode 100644 index 0000000000..15ab61b11d --- /dev/null +++ b/pkg/cli/cmd/resourcetype/create/testdata/missing-required-field.yaml @@ -0,0 +1,6 @@ +types: + testResources: + apiVersions: + '2025-01-01-preview': + schema: {} + capabilities: ["Recipes"] \ No newline at end of file diff --git a/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml b/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml new file mode 100644 index 0000000000..49bc2b9116 --- /dev/null +++ b/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml @@ -0,0 +1,12 @@ +name: CoolCompany.Resources +types: + testResources: + apiVersions: + '2025-01-01-preview': + schema: {} + capabilities: ["Recipes"] + coolResources: + apiVersions: + '2025-01-01-preview': + schema: {} + capabilities: ["Recipes"] \ No newline at end of file diff --git a/pkg/cli/manifest/registermanifest.go b/pkg/cli/manifest/registermanifest.go index c605cfdc38..df6ab830f5 100644 --- a/pkg/cli/manifest/registermanifest.go +++ b/pkg/cli/manifest/registermanifest.go @@ -201,15 +201,21 @@ func RegisterType(ctx context.Context, clientFactory *v20231001preview.ClientFac return err } + var defaultAPIVersion string + if resourceType.DefaultAPIVersion == nil { + defaultAPIVersion = "2023-10-01-preview" + } else { + defaultAPIVersion = *resourceType.DefaultAPIVersion + } locationResource := locationResourceGetResponse.LocationResource locationResource.Properties.ResourceTypes[typeName] = &v20231001preview.LocationResourceType{ APIVersions: map[string]map[string]any{ - *resourceType.DefaultAPIVersion: {}, + defaultAPIVersion: {}, }, } //set it back to resource provider - logIfEnabled(logger, "Updating location %s/%s", resourceProvider.Name, v1.LocationGlobal) + logIfEnabled(logger, "Updating location %s/%s with new resource type", resourceProvider.Name, v1.LocationGlobal) locationPoller, err := clientFactory.NewLocationsClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, v1.LocationGlobal, locationResource, nil) if err != nil { return err @@ -220,6 +226,7 @@ func RegisterType(ctx context.Context, clientFactory *v20231001preview.ClientFac return err } + logIfEnabled(logger, "Resource type %s/%s created successfully", resourceProvider.Name, typeName) return nil } diff --git a/pkg/cli/manifest/testclientfactory.go b/pkg/cli/manifest/testclientfactory.go index 85e9117d1f..80c61ed721 100644 --- a/pkg/cli/manifest/testclientfactory.go +++ b/pkg/cli/manifest/testclientfactory.go @@ -138,8 +138,29 @@ func NewTestClientFactory() (*v20231001preview.ClientFactory, error) { return }, + Get: func( + ctx context.Context, + planeName string, + resourceProviderName string, + locationName string, + options *v20231001preview.LocationsClientGetOptions, + ) (resp azfake.Responder[v20231001preview.LocationsClientGetResponse], errResp azfake.ErrorResponder) { + response := v20231001preview.LocationsClientGetResponse{ + LocationResource: v20231001preview.LocationResource{ + Name: to.Ptr(locationName), + ID: to.Ptr("id"), + Properties: &v20231001preview.LocationProperties{ + ResourceTypes: map[string]*v20231001preview.LocationResourceType{}, + }, + }, + } + resp.SetResponse(http.StatusOK, response, nil) + return + }, } - + //func(ctx context.Context, planeName string, resourceProviderName string, locationName string, options *v20231001preview.LocationsClientGetOptions) + // (resp "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake".Responder[v20231001preview.LocationsClientGetResponse], errResp "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake".ErrorResponder) + //(ctx, planeName, resourceProvider.Name, v1.LocationGlobal, nil) serverFactory := ucpfake.ServerFactory{ ResourceProvidersServer: resourceProvidersServer, ResourceTypesServer: resourceTypesServer,