Skip to content

Commit

Permalink
Add rad resource-provider create
Browse files Browse the repository at this point in the history
This change adds a new command that can register a resource provider, types, and api versions.

Users author a YAML file (manifest), and the `rad` CLI will turn that into a series of API calls.

See: radius-project/design-notes#74 for the somewhat in-progress design of the manifest. It's likely that the manifest will continue to evolve, and we'll update the code in main to match.

This PR contains:

- The new command `rad resource-provider create`
- The parsing and validation logic for the manifest
- Plumbing for calling the resource type APIs from the CLI

Signed-off-by: Ryan Nowak <nowakra@gmail.com>
  • Loading branch information
rynowak committed Nov 13, 2024
1 parent dcd669a commit 6a05640
Show file tree
Hide file tree
Showing 22 changed files with 1,270 additions and 5 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/goccy/go-yaml v1.13.7 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/gnostic-models v0.6.8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.13.7 h1:5k2i973KptPV1mur30XMXwGepDmskip4gA2zHWzWmOY=
github.com/goccy/go-yaml v1.13.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
Expand Down
17 changes: 16 additions & 1 deletion pkg/cli/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ type LogStream struct {

//go:generate mockgen -typed -destination=./mock_applicationsclient.go -package=clients -self_package github.com/radius-project/radius/pkg/cli/clients github.com/radius-project/radius/pkg/cli/clients ApplicationsManagementClient

// ApplicationsManagementClient is used to interface with management features like listing resources by app, show details of a resource.
// ApplicationsManagementClient is the client abstraction used with the CLI to interact wih the Radius API.
//
// Some conventions about the parameter names and values.
//
// - The client is constructed with a default scope. e.g: /planes/radius/local/resourceGroups/myGroup.
// - Parameters named like applicationNameOrID are used to identify an application by its name + default scope, or it's resource id.
// - The planeName parameter is used to specify the plane name. This is usually "local".
type ApplicationsManagementClient interface {
// ListResourcesOfType lists all resources of a given type in the configured scope.
ListResourcesOfType(ctx context.Context, resourceType string) ([]generated.GenericResource, error)
Expand Down Expand Up @@ -227,8 +233,17 @@ type ApplicationsManagementClient interface {
// 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)

// CreateOrUpdateResourceType creates or updates a resource type in the configured scope.
CreateOrUpdateResourceType(ctx context.Context, planeName string, providerNamespace string, resourceTypeName string, resource *ucp_v20231001preview.ResourceTypeResource) (ucp_v20231001preview.ResourceTypeResource, error)

// DeleteResourceType deletes a resource type in the configured scope.
DeleteResourceType(ctx context.Context, planeName string, providerNamespace string, resourceTypeName string) (bool, error)

// CreateOrUpdateAPIVersion creates or updates an API version in the configured scope.
CreateOrUpdateAPIVersion(ctx context.Context, planeName string, providerNamespace string, resourceTypeName string, apiVersionName string, resource *ucp_v20231001preview.APIVersionResource) (ucp_v20231001preview.APIVersionResource, error)

// CreateOrUpdateLocation creates or updates a resource provider location in the configured scope.
CreateOrUpdateLocation(ctx context.Context, planeName string, providerNamespace string, locationName string, resource *ucp_v20231001preview.LocationResource) (ucp_v20231001preview.LocationResource, error)
}

// ShallowCopy creates a shallow copy of the DeploymentParameters object by iterating through the original object and
Expand Down
78 changes: 78 additions & 0 deletions pkg/cli/clients/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type UCPApplicationsManagementClient struct {
resourceGroupClientFactory func() (resourceGroupClient, error)
resourceProviderClientFactory func() (resourceProviderClient, error)
resourceTypeClientFactory func() (resourceTypeClient, error)
apiVersionClientFactory func() (apiVersionClient, error)
locationClientFactory func() (locationClient, error)
capture func(ctx context.Context, capture **http.Response) context.Context
}

Expand Down Expand Up @@ -802,6 +804,26 @@ func (amc *UCPApplicationsManagementClient) GetResourceProviderSummary(ctx conte
return response.ResourceProviderSummary, nil
}

// CreateOrUpdateResourceType creates or updates a resource type in the configured scope.
func (amc *UCPApplicationsManagementClient) CreateOrUpdateResourceType(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string, resource *ucpv20231001.ResourceTypeResource) (ucpv20231001.ResourceTypeResource, error) {
client, err := amc.createResourceTypeClient()
if err != nil {
return ucpv20231001.ResourceTypeResource{}, err
}

poller, err := client.BeginCreateOrUpdate(ctx, planeName, resourceProviderName, resourceTypeName, *resource, &ucpv20231001.ResourceTypesClientBeginCreateOrUpdateOptions{})
if err != nil {
return ucpv20231001.ResourceTypeResource{}, err
}

response, err := poller.PollUntilDone(ctx, nil)
if err != nil {
return ucpv20231001.ResourceTypeResource{}, err
}

return response.ResourceTypeResource, nil
}

// DeleteResourceType deletes a resource type in the configured scope.
func (amc *UCPApplicationsManagementClient) DeleteResourceType(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string) (bool, error) {
client, err := amc.createResourceTypeClient()
Expand All @@ -825,6 +847,46 @@ func (amc *UCPApplicationsManagementClient) DeleteResourceType(ctx context.Conte
return response.StatusCode != 204, nil
}

// CreateOrUpdateAPIVersion creates or updates an API version in the configured scope.
func (amc *UCPApplicationsManagementClient) CreateOrUpdateAPIVersion(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string, apiVersionName string, resource *ucpv20231001.APIVersionResource) (ucpv20231001.APIVersionResource, error) {
client, err := amc.createAPIVersionClient()
if err != nil {
return ucpv20231001.APIVersionResource{}, err
}

poller, err := client.BeginCreateOrUpdate(ctx, planeName, resourceProviderName, resourceTypeName, apiVersionName, *resource, &ucpv20231001.APIVersionsClientBeginCreateOrUpdateOptions{})
if err != nil {
return ucpv20231001.APIVersionResource{}, err
}

response, err := poller.PollUntilDone(ctx, nil)
if err != nil {
return ucpv20231001.APIVersionResource{}, err
}

return response.APIVersionResource, nil
}

// CreateOrUpdateLocation creates or updates a resource provider location in the configured scope.
func (amc *UCPApplicationsManagementClient) CreateOrUpdateLocation(ctx context.Context, planeName string, resourceProviderName string, locationName string, resource *ucpv20231001.LocationResource) (ucpv20231001.LocationResource, error) {
client, err := amc.createLocationClient()
if err != nil {
return ucpv20231001.LocationResource{}, err
}

poller, err := client.BeginCreateOrUpdate(ctx, planeName, resourceProviderName, locationName, *resource, &ucpv20231001.LocationsClientBeginCreateOrUpdateOptions{})
if err != nil {
return ucpv20231001.LocationResource{}, err
}

response, err := poller.PollUntilDone(ctx, nil)
if err != nil {
return ucpv20231001.LocationResource{}, err
}

return response.LocationResource, nil
}

func (amc *UCPApplicationsManagementClient) createApplicationClient(scope string) (applicationResourceClient, error) {
if amc.applicationResourceClientFactory == nil {
// Generated client doesn't like the leading '/' in the scope.
Expand Down Expand Up @@ -876,6 +938,22 @@ func (amc *UCPApplicationsManagementClient) createResourceTypeClient() (resource
return amc.resourceTypeClientFactory()
}

func (amc *UCPApplicationsManagementClient) createAPIVersionClient() (apiVersionClient, error) {
if amc.apiVersionClientFactory == nil {
return ucpv20231001.NewAPIVersionsClient(&aztoken.AnonymousCredential{}, amc.ClientOptions)
}

return amc.apiVersionClientFactory()
}

func (amc *UCPApplicationsManagementClient) createLocationClient() (locationClient, error) {
if amc.locationClientFactory == nil {
return ucpv20231001.NewLocationsClient(&aztoken.AnonymousCredential{}, amc.ClientOptions)
}

return amc.locationClientFactory()
}

func (amc *UCPApplicationsManagementClient) extractScopeAndName(nameOrID string) (string, string, error) {
if strings.HasPrefix(nameOrID, resources.SegmentSeparator) {
// Treat this as a resource id.
Expand Down
15 changes: 13 additions & 2 deletions pkg/cli/clients/management_mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,resourceProviderClient,resourceTypeClient
//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,resourceTypeClient,apiVersonClient,locationClient

// genericResourceClient is an interface for mocking the generated SDK client for any resource.
type genericResourceClient interface {
Expand Down Expand Up @@ -82,7 +82,18 @@ type resourceProviderClient interface {
NewListProviderSummariesPager(planeName string, options *ucpv20231001.ResourceProvidersClientListProviderSummariesOptions) *runtime.Pager[ucpv20231001.ResourceProvidersClientListProviderSummariesResponse]
}

// resourceProviderClient is an interface for mocking the generated SDK client for resource types.
// resourceTypeClient is an interface for mocking the generated SDK client for resource types.
type resourceTypeClient interface {
BeginCreateOrUpdate(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string, resource ucpv20231001.ResourceTypeResource, options *ucpv20231001.ResourceTypesClientBeginCreateOrUpdateOptions) (*runtime.Poller[ucpv20231001.ResourceTypesClientCreateOrUpdateResponse], error)
BeginDelete(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string, options *ucpv20231001.ResourceTypesClientBeginDeleteOptions) (*runtime.Poller[ucpv20231001.ResourceTypesClientDeleteResponse], error)
}

// apiVersionClient is an interface for mocking the generated SDK client for API versions.
type apiVersionClient interface {
BeginCreateOrUpdate(ctx context.Context, planeName string, resourceProviderName string, resourceTypeName string, apiVersionName string, resource ucpv20231001.APIVersionResource, options *ucpv20231001.APIVersionsClientBeginCreateOrUpdateOptions) (*runtime.Poller[ucpv20231001.APIVersionsClientCreateOrUpdateResponse], error)
}

// locationClient is an interface for mocking the generated SDK client for locations.
type locationClient interface {
BeginCreateOrUpdate(ctx context.Context, planeName string, resourceProviderName string, locationName string, resource ucpv20231001.LocationResource, options *ucpv20231001.LocationsClientBeginCreateOrUpdateOptions) (*runtime.Poller[ucpv20231001.LocationsClientCreateOrUpdateResponse], error)
}
90 changes: 89 additions & 1 deletion pkg/cli/clients/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ func Test_ResourceProvider(t *testing.T) {
testResourceProviderName := "Applications.Test"

expectedResource := ucp.ResourceProviderResource{
ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders" + testResourceProviderName),
ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/" + testResourceProviderName),
Name: &testResourceProviderName,
Type: to.Ptr("System.Resources/resourceProviders"),
Location: to.Ptr(v1.LocationGlobal),
Expand Down Expand Up @@ -1076,6 +1076,25 @@ func Test_ResourceType(t *testing.T) {
testResourceProviderName := "Applications.Test"
testResourceTypeName := "testResources"

expectedResource := ucp.ResourceTypeResource{
ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/" + testResourceProviderName + "/resourceTypes/" + testResourceTypeName),
Name: &testResourceTypeName,
Type: to.Ptr("System.Resources/resourceProviders/resourceTypes"),
}

t.Run("CreateOrUpdateResourceType", func(t *testing.T) {
mock := NewMockresourceTypeClient(gomock.NewController(t))
client := createClient(mock)

mock.EXPECT().
BeginCreateOrUpdate(gomock.Any(), "local", testResourceProviderName, testResourceTypeName, expectedResource, gomock.Any()).
Return(poller(&ucp.ResourceTypesClientCreateOrUpdateResponse{ResourceTypeResource: expectedResource}), nil)

result, err := client.CreateOrUpdateResourceType(context.Background(), "local", testResourceProviderName, testResourceTypeName, &expectedResource)
require.NoError(t, err)
require.Equal(t, expectedResource, result)
})

t.Run("DeleteResourceType", func(t *testing.T) {
mock := NewMockresourceTypeClient(gomock.NewController(t))
client := createClient(mock)
Expand All @@ -1093,6 +1112,75 @@ func Test_ResourceType(t *testing.T) {
})
}

func Test_APIVersion(t *testing.T) {
createClient := func(wrapped apiVersionClient) *UCPApplicationsManagementClient {
return &UCPApplicationsManagementClient{
RootScope: testScope,
apiVersionClientFactory: func() (apiVersionClient, error) {
return wrapped, nil
},
capture: testCapture,
}
}

testResourceProviderName := "Applications.Test"
testResourceTypeName := "testResources"
testAPIVersionResourceName := "2025-01-01"

expectedResource := ucp.APIVersionResource{
ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/" + testResourceProviderName + "/resourceTypes/" + testResourceTypeName + "/apiVersions/" + testAPIVersionResourceName),
Name: &testAPIVersionResourceName,
Type: to.Ptr("System.Resources/resourceProviders/resourceTypes/apiVersions"),
}

t.Run("CreateOrUpdateAPIVersion", func(t *testing.T) {
mock := NewMockapiVersionClient(gomock.NewController(t))
client := createClient(mock)

mock.EXPECT().
BeginCreateOrUpdate(gomock.Any(), "local", testResourceProviderName, testResourceTypeName, testAPIVersionResourceName, expectedResource, gomock.Any()).
Return(poller(&ucp.APIVersionsClientCreateOrUpdateResponse{APIVersionResource: expectedResource}), nil)

result, err := client.CreateOrUpdateAPIVersion(context.Background(), "local", testResourceProviderName, testResourceTypeName, testAPIVersionResourceName, &expectedResource)
require.NoError(t, err)
require.Equal(t, expectedResource, result)
})
}

func Test_Location(t *testing.T) {
createClient := func(wrapped locationClient) *UCPApplicationsManagementClient {
return &UCPApplicationsManagementClient{
RootScope: testScope,
locationClientFactory: func() (locationClient, error) {
return wrapped, nil
},
capture: testCapture,
}
}

testResourceProviderName := "Applications.Test"
testLocationName := "east"

expectedResource := ucp.LocationResource{
ID: to.Ptr("/planes/radius/local/providers/System.Resources/resourceProviders/" + testResourceProviderName + "/locations/" + testLocationName),
Name: &testLocationName,
Type: to.Ptr("System.Resources/resourceProviders/locations"),
}

t.Run("CreateOrUpdateLocation", func(t *testing.T) {
mock := NewMocklocationClient(gomock.NewController(t))
client := createClient(mock)

mock.EXPECT().
BeginCreateOrUpdate(gomock.Any(), "local", testResourceProviderName, testLocationName, expectedResource, gomock.Any()).
Return(poller(&ucp.LocationsClientCreateOrUpdateResponse{LocationResource: expectedResource}), nil)

result, err := client.CreateOrUpdateLocation(context.Background(), "local", testResourceProviderName, testLocationName, &expectedResource)
require.NoError(t, err)
require.Equal(t, expectedResource, result)
})
}

func Test_extractScopeAndName(t *testing.T) {
client := UCPApplicationsManagementClient{
RootScope: testScope,
Expand Down
Loading

0 comments on commit 6a05640

Please sign in to comment.