Skip to content
Open
163 changes: 163 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,14 @@ 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 default recipe pack resource if not found and injects its
// ID into the template.
err = r.setupRecipePack(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 +662,157 @@ func (r *Runner) setupCloudProviders(properties any) {
}
}

// setupRecipePack ensures recipe pack(s) for all Radius.Core/environments resources in the template.
// If a Radius.Core environment resource has no recipe
// packs set by the user, Radius creates(if needed) and fetches the default recipe pack from the default scope and
// injects its ID into the template. If the environment already has any recipe pack
// IDs set (literal or Bicep expression references), no changes are made.
func (r *Runner) setupRecipePack(ctx context.Context, template map[string]any) error {
envResources := findRadiusCoreEnvironmentResources(template)
if len(envResources) == 0 {
return nil
}

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

return nil
}

// setupRecipePackForEnvironment 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.
Comment on lines +687 to +688
Copy link
Member

Choose a reason for hiding this comment

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

nit: we only expect one default recipe pack, right?

Suggested change
// no changes are made. Otherwise, it fetches or creates the default recipe pack from
// the default scope and injects their IDs into the template.
// no changes are made. Otherwise, it fetches or creates the default recipe pack from
// the default scope and injects its ID into the template.

func (r *Runner) setupRecipePackForEnvironment(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
// Bicep expression references), leave it as-is — the user is managing packs explicitly.
if hasAnyRecipePacks(properties) {
return nil
}
Comment on lines +690 to +709
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about moving this as a property on https://github.com/kachawla/radius/blob/main/pkg/cli/bicep/resources.go#L38? Just provider better readability and abstraction.


// 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) {
Copy link
Member

Choose a reason for hiding this comment

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

_, 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 {
Copy link
Member

Choose a reason for hiding this comment

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

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