diff --git a/pkg/cli/clients/errors.go b/pkg/cli/clients/errors.go index ea81e286e8..d615517bdb 100644 --- a/pkg/cli/clients/errors.go +++ b/pkg/cli/clients/errors.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" ) @@ -69,3 +70,52 @@ func Is404Error(err error) bool { return false } + +// ConvertAzureErrorResponse converts Azure SDK's ErrorResponse to Radius ErrorDetails. +// This function handles nested error details recursively. +func ConvertAzureErrorResponse(azErr *armresources.ErrorResponse) *v1.ErrorDetails { + if azErr == nil { + return nil + } + + errDetails := &v1.ErrorDetails{} + + // Convert basic fields + if azErr.Code != nil { + errDetails.Code = *azErr.Code + } + if azErr.Message != nil { + errDetails.Message = *azErr.Message + } + if azErr.Target != nil { + errDetails.Target = *azErr.Target + } + + // Convert additional info if present + if len(azErr.AdditionalInfo) > 0 { + errDetails.AdditionalInfo = make([]*v1.ErrorAdditionalInfo, len(azErr.AdditionalInfo)) + for i, info := range azErr.AdditionalInfo { + additionalInfo := &v1.ErrorAdditionalInfo{} + if info.Type != nil { + additionalInfo.Type = *info.Type + } + if info.Info != nil { + // Info is an 'any' type from Azure SDK, we need to handle it carefully + if infoMap, ok := info.Info.(map[string]any); ok { + additionalInfo.Info = infoMap + } + } + errDetails.AdditionalInfo[i] = additionalInfo + } + } + + // Recursively convert nested error details + if len(azErr.Details) > 0 { + errDetails.Details = make([]*v1.ErrorDetails, len(azErr.Details)) + for i, detail := range azErr.Details { + errDetails.Details[i] = ConvertAzureErrorResponse(detail) + } + } + + return errDetails +} diff --git a/pkg/cli/clients/errors_test.go b/pkg/cli/clients/errors_test.go index 0a9f3beb13..f16b229dbc 100644 --- a/pkg/cli/clients/errors_test.go +++ b/pkg/cli/clients/errors_test.go @@ -22,7 +22,10 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" + "github.com/radius-project/radius/pkg/to" + "github.com/stretchr/testify/require" ) func TestIs404Error(t *testing.T) { @@ -69,3 +72,138 @@ func TestIs404Error(t *testing.T) { t.Errorf("Expected Is404Error to return true for fake server not found response, but it returned false") } } + +func TestConvertAzureErrorResponse(t *testing.T) { + t.Run("nil error response", func(t *testing.T) { + result := ConvertAzureErrorResponse(nil) + require.Nil(t, result) + }) + + t.Run("simple error without nested details", func(t *testing.T) { + azErr := &armresources.ErrorResponse{ + Code: to.Ptr("DeploymentFailed"), + Message: to.Ptr("The deployment failed"), + Target: to.Ptr("resource"), + } + + result := ConvertAzureErrorResponse(azErr) + require.NotNil(t, result) + require.Equal(t, "DeploymentFailed", result.Code) + require.Equal(t, "The deployment failed", result.Message) + require.Equal(t, "resource", result.Target) + require.Empty(t, result.Details) + require.Empty(t, result.AdditionalInfo) + }) + + t.Run("error with nested details", func(t *testing.T) { + azErr := &armresources.ErrorResponse{ + Code: to.Ptr("DeploymentFailed"), + Message: to.Ptr("The deployment failed"), + Details: []*armresources.ErrorResponse{ + { + Code: to.Ptr("InvalidTemplate"), + Message: to.Ptr("Template validation failed"), + }, + { + Code: to.Ptr("ResourceNotFound"), + Message: to.Ptr("Resource does not exist"), + }, + }, + } + + result := ConvertAzureErrorResponse(azErr) + require.NotNil(t, result) + require.Equal(t, "DeploymentFailed", result.Code) + require.Equal(t, "The deployment failed", result.Message) + require.Len(t, result.Details, 2) + require.Equal(t, "InvalidTemplate", result.Details[0].Code) + require.Equal(t, "Template validation failed", result.Details[0].Message) + require.Equal(t, "ResourceNotFound", result.Details[1].Code) + require.Equal(t, "Resource does not exist", result.Details[1].Message) + }) + + t.Run("error with additional info", func(t *testing.T) { + azErr := &armresources.ErrorResponse{ + Code: to.Ptr("DeploymentFailed"), + Message: to.Ptr("The deployment failed"), + AdditionalInfo: []*armresources.ErrorAdditionalInfo{ + { + Type: to.Ptr("PolicyViolation"), + Info: map[string]any{ + "policyName": "RequiredTags", + "severity": "High", + }, + }, + }, + } + + result := ConvertAzureErrorResponse(azErr) + require.NotNil(t, result) + require.Equal(t, "DeploymentFailed", result.Code) + require.Len(t, result.AdditionalInfo, 1) + require.Equal(t, "PolicyViolation", result.AdditionalInfo[0].Type) + require.Equal(t, "RequiredTags", result.AdditionalInfo[0].Info["policyName"]) + require.Equal(t, "High", result.AdditionalInfo[0].Info["severity"]) + }) + + t.Run("deeply nested error details", func(t *testing.T) { + azErr := &armresources.ErrorResponse{ + Code: to.Ptr("DeploymentFailed"), + Message: to.Ptr("The deployment failed"), + Details: []*armresources.ErrorResponse{ + { + Code: to.Ptr("InvalidTemplate"), + Message: to.Ptr("Template validation failed"), + Details: []*armresources.ErrorResponse{ + { + Code: to.Ptr("MissingParameter"), + Message: to.Ptr("Required parameter not provided"), + }, + }, + }, + }, + } + + result := ConvertAzureErrorResponse(azErr) + require.NotNil(t, result) + require.Equal(t, "DeploymentFailed", result.Code) + require.Len(t, result.Details, 1) + require.Equal(t, "InvalidTemplate", result.Details[0].Code) + require.Len(t, result.Details[0].Details, 1) + require.Equal(t, "MissingParameter", result.Details[0].Details[0].Code) + require.Equal(t, "Required parameter not provided", result.Details[0].Details[0].Message) + }) + + t.Run("error with all fields populated", func(t *testing.T) { + azErr := &armresources.ErrorResponse{ + Code: to.Ptr("DeploymentFailed"), + Message: to.Ptr("The deployment failed"), + Target: to.Ptr("Microsoft.Resources/deployments"), + AdditionalInfo: []*armresources.ErrorAdditionalInfo{ + { + Type: to.Ptr("PolicyViolation"), + Info: map[string]any{ + "policyName": "RequiredTags", + }, + }, + }, + Details: []*armresources.ErrorResponse{ + { + Code: to.Ptr("InvalidTemplate"), + Message: to.Ptr("Template validation failed"), + Target: to.Ptr("template"), + }, + }, + } + + result := ConvertAzureErrorResponse(azErr) + require.NotNil(t, result) + require.Equal(t, "DeploymentFailed", result.Code) + require.Equal(t, "The deployment failed", result.Message) + require.Equal(t, "Microsoft.Resources/deployments", result.Target) + require.Len(t, result.AdditionalInfo, 1) + require.Len(t, result.Details, 1) + require.Equal(t, "InvalidTemplate", result.Details[0].Code) + require.Equal(t, "template", result.Details[0].Target) + }) +} diff --git a/pkg/cli/cmd/app/list/list.go b/pkg/cli/cmd/app/list/list.go index 4f09bca917..72c620c978 100644 --- a/pkg/cli/cmd/app/list/list.go +++ b/pkg/cli/cmd/app/list/list.go @@ -26,6 +26,7 @@ import ( "github.com/radius-project/radius/pkg/cli/objectformats" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" + corerp "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/spf13/cobra" ) @@ -110,8 +111,9 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { // Run runs the `rad app list` command. // -// Run() creates an ApplicationsManagementClient using the provided ConnectionFactory, then lists the applications and -// writes the output in the specified format, returning an error if any of these steps fail. +// Run() creates an ApplicationsManagementClient using the provided ConnectionFactory, then lists the applications, +// filters them by the default environment if one is set, and writes the output in the specified format, +// returning an error if any of these steps fail. func (r *Runner) Run(ctx context.Context) error { client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) if err != nil { @@ -123,5 +125,57 @@ func (r *Runner) Run(ctx context.Context) error { return err } + // Filter applications by the default environment if one is configured in the workspace + if r.Workspace.Environment != "" { + apps = filterApplicationsByEnvironment(apps, r.Workspace.Environment) + } + return r.Output.WriteFormatted(r.Format, apps, objectformats.GetResourceTableFormat()) } + +// filterApplicationsByEnvironment filters the list of applications to only include those +// associated with the specified environment. The environment can be either a resource ID +// or just an environment name. +func filterApplicationsByEnvironment(apps []corerp.ApplicationResource, environment string) []corerp.ApplicationResource { + var filtered []corerp.ApplicationResource + + for _, app := range apps { + if app.Properties != nil && app.Properties.Environment != nil { + // Match either by full resource ID or by environment name at the end of the ID + if matchesEnvironment(*app.Properties.Environment, environment) { + filtered = append(filtered, app) + } + } + } + + return filtered +} + +// matchesEnvironment checks if the application's environment matches the filter environment. +// It handles both full resource IDs and simple environment names. +func matchesEnvironment(appEnvironment, filterEnvironment string) bool { + // Direct match (full resource ID) + if appEnvironment == filterEnvironment { + return true + } + + // Check if the filter environment appears at the end of the app's environment resource ID + // This handles cases where the workspace has just the name but the app has the full ID + if len(appEnvironment) > len(filterEnvironment) { + // Check if it ends with /environments/{name} + suffix := "/" + filterEnvironment + if len(appEnvironment) >= len(suffix) && appEnvironment[len(appEnvironment)-len(suffix):] == suffix { + return true + } + } + + // Check if the app environment ends with the filter (for cases where filter is a full ID but app is shorter) + if len(filterEnvironment) > len(appEnvironment) { + suffix := "/" + appEnvironment + if len(filterEnvironment) >= len(suffix) && filterEnvironment[len(filterEnvironment)-len(suffix):] == suffix { + return true + } + } + + return false +} diff --git a/pkg/cli/cmd/app/list/list_test.go b/pkg/cli/cmd/app/list/list_test.go index 007b9bdb16..b9f2a10945 100644 --- a/pkg/cli/cmd/app/list/list_test.go +++ b/pkg/cli/cmd/app/list/list_test.go @@ -128,3 +128,224 @@ func Test_Run(t *testing.T) { require.Equal(t, expected, outputSink.Writes) } + +func Test_filterApplicationsByEnvironment(t *testing.T) { +testCases := []struct { +name string +apps []v20231001preview.ApplicationResource +environment string +expected []v20231001preview.ApplicationResource +}{ +{ +name: "filter by environment name - single match", +apps: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +{ +Name: to.Ptr("app2"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/prod"), +}, +}, +}, +environment: "dev", +expected: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +}, +}, +{ +name: "filter by full environment ID", +apps: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +{ +Name: to.Ptr("app2"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/prod"), +}, +}, +}, +environment: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev", +expected: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +}, +}, +{ +name: "no matches", +apps: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +}, +environment: "staging", +expected: []v20231001preview.ApplicationResource{}, +}, +{ +name: "app with nil environment property", +apps: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app1"), +Properties: &v20231001preview.ApplicationProperties{}, +}, +{ +Name: to.Ptr("app2"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +}, +environment: "dev", +expected: []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app2"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +}, +}, +} + +for _, tc := range testCases { +t.Run(tc.name, func(t *testing.T) { +result := filterApplicationsByEnvironment(tc.apps, tc.environment) +require.Equal(t, len(tc.expected), len(result), "number of filtered applications should match") +for i := range tc.expected { +require.Equal(t, *tc.expected[i].Name, *result[i].Name) +if tc.expected[i].Properties != nil && tc.expected[i].Properties.Environment != nil { +require.Equal(t, *tc.expected[i].Properties.Environment, *result[i].Properties.Environment) +} +} +}) +} +} + +func Test_matchesEnvironment(t *testing.T) { +testCases := []struct { +name string +appEnvironment string +filterEnvironment string +expected bool +}{ +{ +name: "exact match", +appEnvironment: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev", +filterEnvironment: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev", +expected: true, +}, +{ +name: "match by environment name", +appEnvironment: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev", +filterEnvironment: "dev", +expected: true, +}, +{ +name: "no match", +appEnvironment: "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev", +filterEnvironment: "prod", +expected: false, +}, +{ +name: "partial match but not at end", +appEnvironment: "/planes/radius/local/resourceGroups/dev/providers/Applications.Core/environments/prod", +filterEnvironment: "dev", +expected: false, +}, +} + +for _, tc := range testCases { +t.Run(tc.name, func(t *testing.T) { +result := matchesEnvironment(tc.appEnvironment, tc.filterEnvironment) +require.Equal(t, tc.expected, result) +}) +} +} + +func Test_Run_WithEnvironmentFiltering(t *testing.T) { +ctrl := gomock.NewController(t) +defer ctrl.Finish() + +// All applications from different environments +allApplications := []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app-dev"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +{ +Name: to.Ptr("app-prod"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/prod"), +}, +}, +} + +// Expected filtered applications (only dev) +filteredApplications := []v20231001preview.ApplicationResource{ +{ +Name: to.Ptr("app-dev"), +Properties: &v20231001preview.ApplicationProperties{ +Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/dev"), +}, +}, +} + +appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) +appManagementClient.EXPECT(). +ListApplications(gomock.Any()). +Return(allApplications, nil). +Times(1) + +workspace := &workspaces.Workspace{ +Connection: map[string]any{ +"kind": "kubernetes", +"context": "kind-kind", +}, +Name: "kind-kind", +Scope: "/planes/radius/local/resourceGroups/test-group", +Environment: "dev", // Default environment is set +} +outputSink := &output.MockOutput{} +runner := &Runner{ +ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, +Workspace: workspace, +Format: "table", +Output: outputSink, +} + +err := runner.Run(context.Background()) +require.NoError(t, err) + +// Verify only filtered applications are output +expected := []any{ +output.FormattedOutput{ +Format: "table", +Obj: filteredApplications, +Options: objectformats.GetResourceTableFormat(), +}, +} + +require.Equal(t, expected, outputSink.Writes) +} diff --git a/pkg/cli/deployment/deploy.go b/pkg/cli/deployment/deploy.go index c22b7e7404..e840bd061c 100644 --- a/pkg/cli/deployment/deploy.go +++ b/pkg/cli/deployment/deploy.go @@ -163,7 +163,20 @@ func (dc *ResourceDeploymentClient) GetProviderConfigs(options clients.Deploymen } func (dc *ResourceDeploymentClient) createSummary(deployment *armresources.DeploymentExtended) (clients.DeploymentResult, error) { - if deployment.Properties == nil || deployment.Properties.OutputResources == nil { + if deployment.Properties == nil { + return clients.DeploymentResult{}, nil + } + + // Check for deployment errors first + if deployment.Properties.Error != nil { + errDetails := clients.ConvertAzureErrorResponse(deployment.Properties.Error) + if errDetails != nil { + // Return the error details directly (it implements error interface) + return clients.DeploymentResult{}, errDetails + } + } + + if deployment.Properties.OutputResources == nil { return clients.DeploymentResult{}, nil } diff --git a/pkg/cli/deployment/deploy_test.go b/pkg/cli/deployment/deploy_test.go index aaab3acf3f..859cf1c5db 100644 --- a/pkg/cli/deployment/deploy_test.go +++ b/pkg/cli/deployment/deploy_test.go @@ -19,8 +19,11 @@ package deployment import ( "testing" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/cli/clients" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/to" "github.com/stretchr/testify/require" ) @@ -89,3 +92,92 @@ func Test_GetProviderConfigsWithAzProvider(t *testing.T) { providerConfig := resourceDeploymentClient.GetProviderConfigs(options) require.Equal(t, providerConfig, expectedConfig) } + +func Test_createSummary_WithDeploymentError(t *testing.T) { +dc := &ResourceDeploymentClient{ +RadiusResourceGroup: "testrg", +} + +t.Run("deployment with simple error", func(t *testing.T) { +deployment := &armresources.DeploymentExtended{ +Properties: &armresources.DeploymentPropertiesExtended{ +Error: &armresources.ErrorResponse{ +Code: to.Ptr("DeploymentFailed"), +Message: to.Ptr("The deployment failed"), +}, +}, +} + +result, err := dc.createSummary(deployment) +require.Error(t, err) +require.Empty(t, result.Resources) +require.Empty(t, result.Outputs) + +// Verify error details +errDetails, ok := err.(*v1.ErrorDetails) +require.True(t, ok, "Expected error to be *v1.ErrorDetails") +require.Equal(t, "DeploymentFailed", errDetails.Code) +require.Equal(t, "The deployment failed", errDetails.Message) +}) + +t.Run("deployment with nested error details", func(t *testing.T) { +deployment := &armresources.DeploymentExtended{ +Properties: &armresources.DeploymentPropertiesExtended{ +Error: &armresources.ErrorResponse{ +Code: to.Ptr("DeploymentFailed"), +Message: to.Ptr("The deployment failed"), +Details: []*armresources.ErrorResponse{ +{ +Code: to.Ptr("InvalidTemplate"), +Message: to.Ptr("Template validation failed"), +}, +}, +}, +}, +} + +result, err := dc.createSummary(deployment) +require.Error(t, err) +require.Empty(t, result.Resources) + +errDetails, ok := err.(*v1.ErrorDetails) +require.True(t, ok) +require.Len(t, errDetails.Details, 1) +require.Equal(t, "InvalidTemplate", errDetails.Details[0].Code) +require.Equal(t, "Template validation failed", errDetails.Details[0].Message) +}) + +t.Run("successful deployment without error", func(t *testing.T) { +deployment := &armresources.DeploymentExtended{ +Properties: &armresources.DeploymentPropertiesExtended{ +OutputResources: []*armresources.ResourceReference{ +{ +ID: to.Ptr("/planes/radius/local/resourceGroups/testrg/providers/Applications.Core/containers/testcontainer"), +}, +}, +Outputs: map[string]any{ +"output1": map[string]any{ +"type": "string", +"value": "test", +}, +}, +}, +} + +result, err := dc.createSummary(deployment) +require.NoError(t, err) +require.Len(t, result.Resources, 1) +require.Len(t, result.Outputs, 1) +}) + +t.Run("deployment with nil properties", func(t *testing.T) { +deployment := &armresources.DeploymentExtended{ +Properties: nil, +} + +result, err := dc.createSummary(deployment) +require.NoError(t, err) +require.Empty(t, result.Resources) +require.Empty(t, result.Outputs) +}) +} diff --git a/pkg/recipes/driver/bicep/bicep.go b/pkg/recipes/driver/bicep/bicep.go index 951bf116ff..3830c62f93 100644 --- a/pkg/recipes/driver/bicep/bicep.go +++ b/pkg/recipes/driver/bicep/bicep.go @@ -29,6 +29,7 @@ import ( "golang.org/x/sync/errgroup" "oras.land/oras-go/v2/registry/remote" + cliclient "github.com/radius-project/radius/pkg/cli/clients" "github.com/radius-project/radius/pkg/components/metrics" coredm "github.com/radius-project/radius/pkg/corerp/datamodel" "github.com/radius-project/radius/pkg/portableresources/datamodel" @@ -168,6 +169,14 @@ func (d *bicepDriver) Execute(ctx context.Context, opts driver.ExecuteOptions) ( return nil, recipes.NewRecipeError(recipes.RecipeDeploymentFailed, fmt.Sprintf("failed to deploy recipe %s of type %s", opts.BaseOptions.Recipe.Name, opts.BaseOptions.Definition.ResourceType), recipes_util.ExecutionError, recipes.GetErrorDetails(err)) } + // Check if deployment completed with errors in properties + if resp.Properties != nil && resp.Properties.Error != nil { + errDetails := cliclient.ConvertAzureErrorResponse(resp.Properties.Error) + if errDetails != nil { + return nil, recipes.NewRecipeError(recipes.RecipeDeploymentFailed, fmt.Sprintf("deployment of recipe %s failed: %s", opts.BaseOptions.Recipe.Name, errDetails.Message), recipes_util.ExecutionError, errDetails) + } + } + recipeResponse, err := d.prepareRecipeResponse(opts.BaseOptions.Definition.TemplatePath, resp.Properties.Outputs, resp.Properties.OutputResources) if err != nil { return nil, recipes.NewRecipeError(recipes.InvalidRecipeOutputs, fmt.Sprintf("failed to read the recipe output %q: %s", recipes.ResultPropertyName, err.Error()), recipes_util.ExecutionError, recipes.GetErrorDetails(err))