Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (c *CreateOrUpdateResource[P, T]) executeRecipeIfNeeded(ctx context.Context
if err != nil {
return nil, fmt.Errorf("failed to get connected resource IDs: %w", err)
}
connectedResourcesProperties := make(map[string]map[string]any)
connectedResourcesProperties := make(map[string]recipes.ConnectedResource)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be renamed to just connectedResources? Do we need to have Properties postfix?


// If there are connected resources, we need to fetch their properties and add them to the recipe context.
for connName, connectedResourceID := range connectionsAndSourceIDs {
Expand All @@ -188,12 +188,17 @@ func (c *CreateOrUpdateResource[P, T]) executeRecipeIfNeeded(ctx context.Context
return nil, fmt.Errorf("failed to get connected resource %s: %w", connectedResourceID, err)
}

connectedResourceProperties, err := resourceutil.GetPropertiesFromResource(connectedResource.Data)
connectedResourceMetadata, err := resourceutil.GetAllPropertiesFromResource(connectedResource.Data, connectedResourceID)
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the connectedResourceID here? Is it the same as connectedResource.ID?

Copy link
Contributor Author

@nithyatsu nithyatsu Dec 16, 2025

Choose a reason for hiding this comment

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

No. The data is only the properties. Therefore there is no connectedResource.ID available. ID belongs one lveel outside in our modelling, but zach had a usecase where he needed the name, type and ID.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed GetAllPropertiesFromResource implementation to use resource's ID.

if err != nil {
return nil, fmt.Errorf("failed to get properties from connected resource %s: %w", connectedResourceID, err)
return nil, fmt.Errorf("failed to get metadata from connected resource %s: %w", connectedResourceID, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

The function is named GetAllPropertiesFromResource, why did we change the log to say metadata instead of properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we did since now we get properties + (name, type, id)

}

connectedResourcesProperties[connName] = connectedResourceProperties
connectedResourcesProperties[connName] = recipes.ConnectedResource{
ID: connectedResourceMetadata.ID,
Name: connectedResourceMetadata.Name,
Type: connectedResourceMetadata.Type,
Properties: connectedResourceMetadata.Properties,
}
}

metadata := recipes.ResourceMetadata{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
"p1": "v1",
},
Properties: properties,
ConnectedResourcesProperties: map[string]map[string]any{},
ConnectedResourcesProperties: map[string]recipes.ConnectedResource{},
}

prevState := []string{
Expand Down Expand Up @@ -470,3 +470,34 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
})
}
}

func TestGetAllPropertiesFromResource_Integration(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the _Integration mean here? What is our naming convention for tests like this? Should we have an underscore right after Test prefix?

// Simple integration test for resourceutil.GetAllPropertiesFromResource
connectedResourceID := "/planes/radius/local/resourceGroups/radius-test-rg/providers/Applications.Datastores/sqlDatabases/test-db"
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

Trailing whitespace detected after the resource ID string. Remove the trailing whitespace for cleaner code.

Copilot uses AI. Check for mistakes.

// Create a test resource with properties
testResource := struct {
Properties map[string]any `json:"properties"`
}{
Properties: map[string]any{
"host": "localhost",
"port": 5432,
"database": "testdb",
},
}

// Test the GetAllPropertiesFromResource function
metadata, err := resourceutil.GetAllPropertiesFromResource(testResource, connectedResourceID)

Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

Trailing whitespace detected after the function call. Remove the trailing whitespace for cleaner code.

Suggested change

Copilot uses AI. Check for mistakes.
// Assertions
require.NoError(t, err)
require.NotNil(t, metadata)
require.Equal(t, connectedResourceID, metadata.ID)
require.Equal(t, "test-db", metadata.Name)
require.Equal(t, "Applications.Datastores/sqlDatabases", metadata.Type)
require.Equal(t, map[string]any{
"host": "localhost",
"port": float64(5432), // JSON unmarshaling converts to float64
"database": "testdb",
}, metadata.Properties)
}
9 changes: 7 additions & 2 deletions pkg/recipes/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,14 @@ func Test_Engine_Execute_Success(t *testing.T) {
Parameters: map[string]any{
"resourceName": "resource1",
},
ConnectedResourcesProperties: map[string]map[string]any{
ConnectedResourcesProperties: map[string]recipes.ConnectedResource{
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do some assertions here?

"database": {
"name": "db",
ID: "/planes/radius/local/resourceGroups/radius-test-rg/providers/Applications.Datastores/sqlDatabases/database",
Name: "database",
Type: "Applications.Datastores/sqlDatabases",
Properties: map[string]any{
"name": "db",
},
},
},
}
Expand Down
79 changes: 79 additions & 0 deletions pkg/recipes/recipecontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,82 @@ func TestNewContext_failures(t *testing.T) {
})
}
}

func TestNewContext_WithConnectedResources(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to pay attention to our naming conventions and rename this test if necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also use table-driven if we have more cases to test?

testMetadata := &recipes.ResourceMetadata{
ResourceID: "/planes/radius/local/resourceGroups/testGroup/providers/applications.datastores/mongodatabases/mongo0",
ApplicationID: "/planes/radius/local/resourceGroups/testGroup/providers/applications.core/applications/testApplication",
EnvironmentID: "/planes/radius/local/resourceGroups/testGroup/providers/applications.core/environments/testEnvironment",
Name: "recipe0",
Properties: map[string]any{
"property1": "value1",
},
ConnectedResourcesProperties: map[string]recipes.ConnectedResource{
"database": {
ID: "/planes/radius/local/resourceGroups/testGroup/providers/applications.datastores/sqldatabases/testdb",
Name: "testdb",
Type: "Applications.Datastores/sqlDatabases",
Properties: map[string]any{
"host": "localhost",
"port": 5432,
"database": "testdb",
},
},
"cache": {
ID: "/planes/radius/local/resourceGroups/testGroup/providers/applications.datastores/redis/testcache",
Name: "testcache",
Type: "Applications.Datastores/redis",
Properties: map[string]any{
"host": "cache-host",
"port": 6379,
},
},
},
}

testConfig := &recipes.Configuration{
Providers: coredm.Providers{
Azure: coredm.ProvidersAzure{
Scope: "/planes/radius/local/resourceGroups/testGroup",
},
AWS: coredm.ProvidersAWS{
Scope: "/planes/aws/aws/accounts/1234567890/regions/us-west-2",
},
},
}

result, err := New(testMetadata, testConfig)
require.NoError(t, err)
require.NotNil(t, result)

// Verify basic resource properties
require.Equal(t, "mongo0", result.Resource.Name)
require.Equal(t, testMetadata.ResourceID, result.Resource.ID)
require.Equal(t, "applications.datastores/mongodatabases", result.Resource.Type)

// Verify connected resources metadata is available (connections are set by recipe engines)
require.NotNil(t, testMetadata.ConnectedResourcesProperties)
require.Len(t, testMetadata.ConnectedResourcesProperties, 2)

// Verify the metadata contains the expected connected resources
dbConn, exists := testMetadata.ConnectedResourcesProperties["database"]
require.True(t, exists)
require.Equal(t, "/planes/radius/local/resourceGroups/testGroup/providers/applications.datastores/sqldatabases/testdb", dbConn.ID)
require.Equal(t, "testdb", dbConn.Name)
require.Equal(t, "Applications.Datastores/sqlDatabases", dbConn.Type)
require.Equal(t, map[string]any{
"host": "localhost",
"port": 5432,
"database": "testdb",
}, dbConn.Properties)

cacheConn, exists := testMetadata.ConnectedResourcesProperties["cache"]
require.True(t, exists)
require.Equal(t, "/planes/radius/local/resourceGroups/testGroup/providers/applications.datastores/redis/testcache", cacheConn.ID)
require.Equal(t, "testcache", cacheConn.Name)
require.Equal(t, "Applications.Datastores/redis", cacheConn.Type)
require.Equal(t, map[string]any{
"host": "cache-host",
"port": 6379,
}, cacheConn.Properties)
}
11 changes: 7 additions & 4 deletions pkg/recipes/recipecontext/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ type Resource struct {
Properties map[string]any `json:"properties,omitempty"`

// Connections represent a map of connections to other resources.
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The comment has inconsistent capitalization. "Connections" should start with a lowercase letter to follow Go documentation conventions for field comments. Change "Connections represent" to "connections represent".

Copilot uses AI. Check for mistakes.
// The key is the connection name, and the value is a map of connected resource properties.
// We enrich the recipe context with this, allowing the recipe to access properties of connected resources using the following format:
// context.resource.connections.[connection-name].[connected-resource-property]
Connections map[string]map[string]any `json:"connections,omitempty"`
// The key is the connection name, and the value contains the connected resource's metadata and properties.
// We enrich the recipe context with this, allowing the recipe to access connected resource info using:
// context.resource.connections.[connection-name].properties.[property-name]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be as follows:

Suggested change
// context.resource.connections.[connection-name].properties.[property-name]
// context.resource.connections.[connection-name].properties[property-name]

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no.. we need the dot.

// context.resource.connections.[connection-name].id
// context.resource.connections.[connection-name].name
// context.resource.connections.[connection-name].type
Connections map[string]recipes.ConnectedResource `json:"connections,omitempty"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zachcasper @sk593 , the comment captures the change to connections. please review.

}

// ResourceInfo represents name and id of the resource
Expand Down
9 changes: 7 additions & 2 deletions pkg/recipes/terraform/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ func TestGenerateConfig(t *testing.T) {
TemplatePath: "test/module/source",
},
ResourceRecipe: &recipes.ResourceMetadata{
ConnectedResourcesProperties: map[string]map[string]any{
ConnectedResourcesProperties: map[string]recipes.ConnectedResource{
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do some assertions?

"conn1": {
"dbName": "db",
ID: "/planes/radius/local/resourceGroups/radius-test-rg/providers/Applications.Datastores/redis/redis",
Name: "redis",
Type: "Applications.Datastores/redis",
Properties: map[string]any{
"dbName": "db",
},
},
},
},
Expand Down
21 changes: 16 additions & 5 deletions pkg/recipes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)

// ConnectedResource represents a connected resource's metadata and properties
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The comment should follow Go documentation conventions by starting with the type name. It should read: "ConnectedResource represents a connected resource's metadata and properties."

Suggested change
// ConnectedResource represents a connected resource's metadata and properties
// ConnectedResource represents a connected resource's metadata and properties.

Copilot uses AI. Check for mistakes.
type ConnectedResource struct {
// ID represents the fully qualified resource id
ID string `json:"id"`
// Name represents the resource name
Name string `json:"name"`
// Type represents the resource type
Type string `json:"type"`
// Properties represents the resource properties
Properties map[string]any `json:"properties,omitempty"`
}

// Configuration represents runtime and cloud provider configuration, which is used by the driver while deploying recipes.
type Configuration struct {
// Kubernetes Runtime configuration for the environment.
Expand Down Expand Up @@ -85,11 +97,10 @@ type ResourceMetadata struct {
ResourceID string
// Properties represents the properties of the resource that the recipe is deploying
Properties map[string]any
// ConnectedResourcesProperties represents the properties of the connected resources that the recipe is deploying.
// the key is connection name and the value is a map of properties for the connected resource.
// properties are inturn a map of key/value pairs, where the key is the property name and the value is the property value.
// these properties are passed into the recipe context.
ConnectedResourcesProperties map[string]map[string]any
// ConnectedResourcesProperties represents the connected resources that the recipe is deploying.
// The key is connection name and the value contains the connected resource's metadata and properties.
// These are passed into the recipe context.
ConnectedResourcesProperties map[string]ConnectedResource
// Parameters represents key/value pairs to pass into the recipe template. Overrides any parameters set by the environment.
Parameters map[string]any
}
Expand Down
31 changes: 31 additions & 0 deletions pkg/resourceutil/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,34 @@ func GetConnectionNameandSourceIDs[P any](resource P) (map[string]string, error)

return connectionNamesAndSourceIDs, nil
}

// ResourceMetadata represents resource metadata including ID, Name, Type and Properties
type ResourceMetadata struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties map[string]any `json:"properties,omitempty"`
}
Comment on lines +96 to +102
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

There are now two different ResourceMetadata types in the codebase with the same name but different purposes:

  1. resourceutil.ResourceMetadata (this struct) - represents resource metadata with ID, Name, Type, and Properties
  2. recipes.ResourceMetadata - represents recipe metadata including resource ID, properties, connected resources, and parameters

This creates naming confusion and ambiguity. Consider renaming this struct to something more specific like ResourceInfo or ExtractedResourceMetadata to clearly differentiate it from the recipes package type and indicate its purpose is for extracting resource information.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be an important feedback.

Copy link
Contributor Author

@nithyatsu nithyatsu Dec 16, 2025

Choose a reason for hiding this comment

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

I will rename the struct to ConnectedResourceMetaData.
recipes.ResourceMetadata is rresourcemetadata too, just used in recipe package. but resourceutil.ResourceMetadata is the metedata of connected resource.


// GetAllPropertiesFromResource extracts the resource metadata including ID, Name, Type and properties
// by parsing the resource ID and extracting properties.
func GetAllPropertiesFromResource[P any](resource P, resourceID string) (*ResourceMetadata, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the resourceID belong to the resource? If so, why do we need to pass in the resourceID? Can't we just do resource.ID?

If not, is it the target resource or somethin like that?

We need clearer argument names, I believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. It is because the properies bag does not include the resource ID. ID is one level higher, as part of trackedresources struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the implementation so it is clearer. Please review.

// Parse resource ID to get name and type
parsedResourceID, err := resources.Parse(resourceID)
if err != nil {
return nil, fmt.Errorf("failed to parse resource ID %s: %w", resourceID, err)
}

// Get properties using existing method
properties, err := GetPropertiesFromResource(resource)
if err != nil {
return nil, err
}

return &ResourceMetadata{
ID: resourceID,
Name: parsedResourceID.Name(),
Type: parsedResourceID.Type(),
Properties: properties,
}, nil
Comment on lines +119 to +124
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the difference between resourceID and parsedResourceID?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ID is a string. parsedResourceID is what we get we we do a resources.Parse(id)

}
Loading
Loading