Skip to content

Commit

Permalink
Implement resource provider API for dynamic rp (#8177)
Browse files Browse the repository at this point in the history
# Description

This change implements the standard resource provider API for dynamic
resources. This enables the dynamic-rp to serve the standard CRUDL API
functionality for user-defined types.

This change does NOT add recipe support, that will come in the next
change. The pattern that's supported by this change is an "inert"
resource which has no backend functionality. Recipe support will be a
superset of this functionality.


## Type of change


Part of: #6688 

## Contributor checklist
Please verify that the PR meets the following requirements, where
applicable:

- [ ] An overview of proposed schema changes is included in a linked
GitHub issue.
- [ ] A design document PR is created in the [design-notes
repository](https://github.com/radius-project/design-notes/), if new
APIs are being introduced.
- [ ] If applicable, design document has been reviewed and approved by
Radius maintainers/approvers.
- [ ] A PR for the [samples
repository](https://github.com/radius-project/samples) is created, if
existing samples are affected by the changes in this PR.
- [ ] A PR for the [documentation
repository](https://github.com/radius-project/docs) is created, if the
changes in this PR affect the documentation or any user facing updates
are made.
- [ ] A PR for the [recipes
repository](https://github.com/radius-project/recipes) is created, if
existing recipes are affected by the changes in this PR.

Signed-off-by: Ryan Nowak <[email protected]>
  • Loading branch information
rynowak authored Dec 31, 2024
1 parent 4952890 commit 710509a
Show file tree
Hide file tree
Showing 18 changed files with 864 additions and 19 deletions.
1 change: 1 addition & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jobs:
node-version: "22"
- name: Run `make format-check`
id: format-check
continue-on-error: true
run: |
make format-check
- name: Check for formatting failures
Expand Down
12 changes: 7 additions & 5 deletions build/prettier.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
# limitations under the License.
# ------------------------------------------------------------

.PHONY: prettier-check prettier-format me prettier
##@ Formatting (of JSON files)

PRETTIER_VERSION := 3.3.3

format-check:
.PHONY: format-check
format-check: ## Checks the formatting of JSON files.
@echo "$(ARROW) Checking for formatting issues using prettier..."
@echo ""
@npx prettier@$(PRETTIER_VERSION) --check "*/**/*.{ts,js,mjs,json}"
@npx --yes prettier@$(PRETTIER_VERSION) --check "*/**/*.{ts,js,mjs,json}"

format-write:
.PHONY: format-write
format-write: ## Updates the formatting of JSON files.
@echo "$(ARROW) Reformatting files using prettier..."
@echo ""
@npx prettier@$(PRETTIER_VERSION) --write "*/**/*.{ts,js,mjs,json}"
@npx --yes prettier@$(PRETTIER_VERSION) --write "*/**/*.{ts,js,mjs,json}"
38 changes: 38 additions & 0 deletions pkg/dynamicrp/api/dynamicresource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
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 api

// DynamicResource is used as the versioned resource model for dynamic resources.
//
// A dynamic resource uses a user-provided OpenAPI specification to define the resource schema. Therefore,
// the properties of the resource are not known at compile time.
type DynamicResource struct {
// ID is the resource ID.
ID *string `json:"id"`
// Name is the resource name.
Name *string `json:"name"`
// Type is the resource type.
Type *string `json:"type"`
// Location is the resource location.
Location *string `json:"location"`
// Tags are the resource tags.
Tags map[string]*string `json:"tags,omitempty"`
// Properties stores the properties of the resource.
Properties map[string]any `json:"properties,omitempty"`
// SystemData stores the system data of the resource.
SystemData map[string]any `json:"systemData,omitempty"`
}
122 changes: 122 additions & 0 deletions pkg/dynamicrp/api/dynamicresource_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
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 api

import (
"encoding/json"
"fmt"

v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/dynamicrp/datamodel"
"github.com/radius-project/radius/pkg/to"
)

const (
// TODO
Version = "2023-01-01"
)

// ConvertTo converts the versioned model to the datamodel.
func (d *DynamicResource) ConvertTo() (v1.DataModelInterface, error) {
// Note: we always round-trip the properties through JSON to ensure that the conversion is possible, and
// to make a defensive copy of the data.
bs, err := json.Marshal(d.Properties)
if err != nil {
return nil, fmt.Errorf("failed to marshal properties: %w", err)
}

properties := map[string]any{}
err = json.Unmarshal(bs, &properties)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal properties: %w", err)
}

dm := &datamodel.DynamicResource{
BaseResource: v1.BaseResource{
TrackedResource: v1.TrackedResource{
ID: to.String(d.ID),
Name: to.String(d.Name),
Type: to.String(d.Type),
Location: to.String(d.Location),
Tags: to.StringMap(d.Tags),
},
InternalMetadata: v1.InternalMetadata{
UpdatedAPIVersion: Version,
},
},
Properties: properties,
}

return dm, nil
}

// ConvertFrom converts the datamodel to the versioned model.
func (d *DynamicResource) ConvertFrom(src v1.DataModelInterface) error {
dm, ok := src.(*datamodel.DynamicResource)
if !ok {
return v1.ErrInvalidModelConversion
}

// Note: we always round-trip the properties through JSON to ensure that the conversion is possible, and
// to make a defensive copy of the data.
bs, err := json.Marshal(dm.Properties)
if err != nil {
return fmt.Errorf("failed to marshal properties: %w", err)
}

properties := map[string]any{}
err = json.Unmarshal(bs, &properties)
if err != nil {
return fmt.Errorf("failed to unmarshal properties: %w", err)
}

d.ID = &dm.ID
d.Name = &dm.Name
d.Type = &dm.Type
d.Location = &dm.Location
d.Tags = *to.StringMapPtr(dm.Tags)
d.SystemData = fromSystemDataDataModel(dm.SystemData)
d.Properties = properties
d.Properties["provisioningState"] = fromProvisioningStateDataModel(dm.AsyncProvisioningState)

return nil
}

func fromSystemDataDataModel(input v1.SystemData) map[string]any {
bs, err := json.Marshal(input)
if err != nil {
// This should never fail. We've designed the SystemData type to be serializable.
panic("marshalling system data failed: " + err.Error())
}

result := map[string]any{}
err = json.Unmarshal(bs, &result)
if err != nil {
// This should never fail. We've designed the SystemData type to be serializable.
panic("unmarshalling system data failed: " + err.Error())
}

return result
}

func fromProvisioningStateDataModel(input v1.ProvisioningState) string {
if input == "" {
return string(v1.ProvisioningStateSucceeded)
}

return string(input)
}
126 changes: 126 additions & 0 deletions pkg/dynamicrp/api/dynamicresource_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
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 api

import (
"encoding/json"
"testing"

v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/dynamicrp/datamodel"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/test/testutil"

"github.com/stretchr/testify/require"
)

func Test_DynamicResource_ConvertVersionedToDataModel(t *testing.T) {
conversionTests := []struct {
filename string
expected *datamodel.DynamicResource
err error
}{
{
filename: "dynamicresource-resource.json",
expected: &datamodel.DynamicResource{
BaseResource: v1.BaseResource{
TrackedResource: v1.TrackedResource{
ID: "/planes/radius/local/resourceGroups/test/providers/Applications.Test/testResources/testResource",
Name: "testResource",
Type: "Applications.Test/testResources",
Location: "global",
Tags: map[string]string{
"env": "dev",
},
},
InternalMetadata: v1.InternalMetadata{
UpdatedAPIVersion: Version,
},
},
Properties: map[string]any{
"message": "Hello, world!",
},
},
},
}

for _, tt := range conversionTests {
t.Run(tt.filename, func(t *testing.T) {
rawPayload := testutil.ReadFixture(tt.filename)
r := &DynamicResource{}
err := json.Unmarshal(rawPayload, r)
require.NoError(t, err)

dm, err := r.ConvertTo()

if tt.err != nil {
require.ErrorIs(t, err, tt.err)
} else {
require.NoError(t, err)
ct := dm.(*datamodel.DynamicResource)
require.Equal(t, tt.expected, ct)
}
})
}
}

func Test_DynamicResource_ConvertDataModelToVersioned(t *testing.T) {
conversionTests := []struct {
filename string
expected *DynamicResource
err error
}{
{
filename: "dynamicresource-datamodel.json",
expected: &DynamicResource{
ID: to.Ptr("/planes/radius/local/resourceGroups/test/providers/Applications.Test/testResources/testResource"),
Name: to.Ptr("testResource"),
Type: to.Ptr("Applications.Test/testResources"),
Location: to.Ptr("global"),
Tags: map[string]*string{
"env": to.Ptr("dev"),
},
Properties: map[string]any{
"provisioningState": fromProvisioningStateDataModel(v1.ProvisioningStateSucceeded),
"message": "Hello, world!",
},
},
},
}

for _, tt := range conversionTests {
t.Run(tt.filename, func(t *testing.T) {
rawPayload := testutil.ReadFixture(tt.filename)
dm := &datamodel.DynamicResource{}
err := json.Unmarshal(rawPayload, dm)
require.NoError(t, err)

resource := &DynamicResource{}
err = resource.ConvertFrom(dm)

// Avoid hardcoding the SystemData field in tests.
tt.expected.SystemData = fromSystemDataDataModel(dm.SystemData)

if tt.err != nil {
require.ErrorIs(t, err, tt.err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, resource)
}
})
}
}
20 changes: 20 additions & 0 deletions pkg/dynamicrp/api/testdata/dynamicresource-datamodel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "/planes/radius/local/resourceGroups/test/providers/Applications.Test/testResources/testResource",
"name": "testResource",
"type": "Applications.Test/testResources",
"location": "global",
"systemData": {
"createdBy": "[email protected]",
"createdByType": "User",
"createdAt": "2021-09-24T19:09:54.2403864Z",
"lastModifiedBy": "[email protected]",
"lastModifiedByType": "User",
"lastModifiedAt": "2021-09-24T20:09:54.2403864Z"
},
"tags": {
"env": "dev"
},
"properties": {
"message": "Hello, world!"
}
}
12 changes: 12 additions & 0 deletions pkg/dynamicrp/api/testdata/dynamicresource-resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "/planes/radius/local/resourceGroups/test/providers/Applications.Test/testResources/testResource",
"name": "testResource",
"type": "Applications.Test/testResources",
"location": "global",
"tags": {
"env": "dev"
},
"properties": {
"message": "Hello, world!"
}
}
Loading

0 comments on commit 710509a

Please sign in to comment.