Skip to content
Draft
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
1 change: 1 addition & 0 deletions bicep-types
Submodule bicep-types added at 556bf5
164 changes: 164 additions & 0 deletions pkg/cli/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/radius-project/radius/pkg/cli/filesystem"
"github.com/radius-project/radius/pkg/cli/framework"
"github.com/radius-project/radius/pkg/cli/output"
"github.com/radius-project/radius/pkg/cli/recipepack"
"github.com/radius-project/radius/pkg/cli/workspaces"
"github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
"github.com/radius-project/radius/pkg/corerp/api/v20250801preview"
Expand Down Expand Up @@ -136,6 +137,9 @@ type Runner struct {
RadiusCoreClientFactory *v20250801preview.ClientFactory
Deploy deploy.Interface
Output output.Interface
// DefaultScopeClientFactory is the client factory scoped to the default resource group.
// Recipe packs are always created/queried in the default scope.
DefaultScopeClientFactory *v20250801preview.ClientFactory

ApplicationName string
EnvironmentNameOrID string
Expand Down Expand Up @@ -345,6 +349,15 @@ func (r *Runner) Run(ctx context.Context) error {
"Deployment In Progress... ", r.FilePath, r.ApplicationName, r.EnvironmentNameOrID, r.Workspace.Name)
}

// Before deploying, set up recipe packs for any Radius.Core environments in the
// template. This creates missing recipe pack resources and injects their
// IDs into the template so that the environment is deployed once with the complete
// set of recipe packs.
err = r.setupRecipePacks(ctx, template)
if err != nil {
return err
}

_, err = r.Deploy.DeployWithProgress(ctx, deploy.Options{
ConnectionFactory: r.ConnectionFactory,
Workspace: *r.Workspace,
Expand Down Expand Up @@ -650,6 +663,157 @@ func (r *Runner) setupCloudProviders(properties any) {
}
}

// setupRecipePacks finds all Radius.Core/environments resources in the template and
// ensures they have recipe packs configured. If an environment resource has no recipe
// packs set, it fetches or creates default recipe packs from the default scope and
// injects their IDs into the template. If the environment already has any recipe pack
// IDs set (literal or ARM expression references), no changes are made.
func (r *Runner) setupRecipePacks(ctx context.Context, template map[string]any) error {
envResources := findRadiusCoreEnvironmentResources(template)
if len(envResources) == 0 {
return nil
}

for _, envResource := range envResources {
if err := r.setupRecipePacksForEnvironment(ctx, envResource); err != nil {
return err
}
}

return nil
}

// setupRecipePacksForEnvironment sets up recipe packs for a single Radius.Core/environments resource.
// If the environment already has any recipe packs set (literal IDs or ARM expression references),
// no changes are made. Otherwise, it fetches or creates the default recipe pack from
// the default scope and injects their IDs into the template.
func (r *Runner) setupRecipePacksForEnvironment(ctx context.Context, envResource map[string]any) error {
// The compiled ARM template has a double-nested properties structure:
// envResource["properties"]["properties"] is where resource-level fields live.
// Navigate to the inner (resource) properties map.
outerProps, ok := envResource["properties"].(map[string]any)
if !ok {
outerProps = map[string]any{}
envResource["properties"] = outerProps
}

properties, ok := outerProps["properties"].(map[string]any)
if !ok {
properties = map[string]any{}
outerProps["properties"] = properties
}

// If the environment already has any recipe packs configured (literal IDs or
// ARM expression references), leave it as-is — the user is managing packs explicitly.
if hasAnyRecipePacks(properties) {
return nil
}

// No recipe packs set — provide defaults from the default scope.

// Ensure the default resource group exists before accessing recipe packs.
mgmtClient, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace)
if err != nil {
return err
}
if err := recipepack.EnsureDefaultResourceGroup(ctx, mgmtClient.CreateOrUpdateResourceGroup); err != nil {
return err
}

// Initialize the default scope client factory so we can access default recipe packs.
if r.DefaultScopeClientFactory == nil {
defaultFactory, err := cmd.InitializeRadiusCoreClientFactory(ctx, r.Workspace, recipepack.DefaultResourceGroupScope)
if err != nil {
return err
}
r.DefaultScopeClientFactory = defaultFactory
}

recipePackDefaultClient := r.DefaultScopeClientFactory.NewRecipePacksClient()

// Try to GET the default recipe pack from the default scope.
// If it doesn't exist, create it.
packID, err := getOrCreateDefaultRecipePack(ctx, recipePackDefaultClient)
if err != nil {
return err
}

// Inject the default recipe pack ID into the template.
properties["recipePacks"] = []any{packID}

return nil
}

// hasAnyRecipePacks returns true if the environment properties have any recipe packs
// configured, including both literal string IDs and ARM expression references.
func hasAnyRecipePacks(properties map[string]any) bool {
recipePacks, ok := properties["recipePacks"]
if !ok {
return false
}
packsArray, ok := recipePacks.([]any)
if !ok {
return false
}
return len(packsArray) > 0
}

// getOrCreateDefaultRecipePack attempts to GET the default recipe pack from
// the default scope. If it doesn't exist (404), it creates it with all core
// resource type recipes. Returns the full resource ID.
func getOrCreateDefaultRecipePack(ctx context.Context, client *v20250801preview.RecipePacksClient) (string, error) {
_, err := client.Get(ctx, recipepack.DefaultRecipePackResourceName, nil)
if err != nil {
if !clients.Is404Error(err) {
return "", fmt.Errorf("failed to get default recipe pack from default scope: %w", err)
}
// Not found — create the default recipe pack with all core types.
resource := recipepack.NewDefaultRecipePackResource()
_, err = client.CreateOrUpdate(ctx, recipepack.DefaultRecipePackResourceName, resource, nil)
if err != nil {
return "", fmt.Errorf("failed to create default recipe pack: %w", err)
}
}
return recipepack.DefaultRecipePackID(), nil
}

// findRadiusCoreEnvironmentResources walks the template's resources and returns
// all Radius.Core/environments resources found (as mutable maps).
func findRadiusCoreEnvironmentResources(template map[string]any) []map[string]any {
if template == nil {
return nil
}

resourcesValue, ok := template["resources"]
if !ok {
return nil
}

resourcesMap, ok := resourcesValue.(map[string]any)
if !ok {
return nil
}

var envResources []map[string]any
for _, resourceValue := range resourcesMap {
resource, ok := resourceValue.(map[string]any)
if !ok {
continue
}

resourceType, ok := resource["type"].(string)
if !ok {
continue
}

if strings.HasPrefix(strings.ToLower(resourceType), "radius.core/environments") {
envResources = append(envResources, resource)
}
}

return envResources
}

// configureProviders configures environment and cloud providers based on the environment and provider type
func (r *Runner) configureProviders() error {
var env any
Expand Down
Loading
Loading