Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement resource provider API for dynamic rp #8177

Merged
merged 2 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ytimocin - this was missing from the help displayed when running make.


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}"
Copy link
Contributor Author

@rynowak rynowak Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ytimocin --yes should be included here to suppress the prompt.

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
Loading