-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement resource provider API for dynamic rp (#8177)
# 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
Showing
18 changed files
with
864 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!" | ||
} | ||
} |
Oops, something went wrong.