Skip to content

Commit

Permalink
Adding commands for user-defined-types (radius-project#7699)
Browse files Browse the repository at this point in the history
This commit adds new commands and capabilities for working with
user-defined-types in the CLI.

Some new commands:

- `rad resourceprovider *`: CRUDL lifecycle management for a resource
provider.
- `rad resourceprovider new`: Scaffolding a template for a resource
provider.
- `rad resourcetype [show|list]`: RL lifecycle management for resource
types (read-only).
- `rad resource create`: CU lifecycle management for any resource type.

Also updated `rad resource delete` and similar commands to work with
user-defined-types. Many commands validate a fixed list of resource
types, this update allows commands to work with an arbitrary resource
type.

- This pull request adds or changes features of Radius and has an
approved issue (issue link required).

Part of: radius-project#6688

Signed-off-by: Ryan Nowak <[email protected]>
  • Loading branch information
rynowak committed Nov 11, 2024
1 parent 8433c6a commit 1c78091
Show file tree
Hide file tree
Showing 14 changed files with 968 additions and 4 deletions.
4 changes: 4 additions & 0 deletions cmd/rad/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
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_show "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/show"
Expand Down Expand Up @@ -238,6 +239,9 @@ func initSubCommands() {
resourceProviderListCmd, _ := resourceprovider_list.NewCommand(framework)
resourceProviderCmd.AddCommand(resourceProviderListCmd)

resourceProviderCreateCmd, _ := resourceprovider_create.NewCommand(framework)
resourceProviderCmd.AddCommand(resourceProviderCreateCmd)

resourceProviderDeleteCmd, _ := resourceprovider_delete.NewCommand(framework)
resourceProviderCmd.AddCommand(resourceProviderDeleteCmd)

Expand Down
155 changes: 155 additions & 0 deletions pkg/cli/cmd/resourceprovider/create/create.go
Original file line number Diff line number Diff line change
@@ -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
}
128 changes: 128 additions & 0 deletions pkg/cli/cmd/resourceprovider/create/create_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
35 changes: 35 additions & 0 deletions pkg/cli/cmd/resourceprovider/create/testdata/resourceprovider.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
Loading

0 comments on commit 1c78091

Please sign in to comment.