From 506acfd7c572d5d47d0cd8606e4fd8ad3223c44a Mon Sep 17 00:00:00 2001 From: Alec13355 Date: Tue, 6 Jan 2026 09:01:18 -0600 Subject: [PATCH 1/5] Fixes issue where 'rad recipe show' displayed 'null' instead of actual configured parameter values like 'eastus' and 'my-rg'. Signed-off-by: Alec13355 --- pkg/cli/cmd/recipe/show/show.go | 28 ++++ pkg/cli/cmd/recipe/show/show_test.go | 185 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/pkg/cli/cmd/recipe/show/show.go b/pkg/cli/cmd/recipe/show/show.go index edcdaae7b8..13462ec5ad 100644 --- a/pkg/cli/cmd/recipe/show/show.go +++ b/pkg/cli/cmd/recipe/show/show.go @@ -152,11 +152,19 @@ func (r *Runner) Run(ctx context.Context) error { return err } + // Get the recipe metadata (template schema) recipeDetails, err := client.GetRecipeMetadata(ctx, r.Workspace.Environment, v20231001preview.RecipeGetMetadata{Name: &r.RecipeName, ResourceType: &r.ResourceType}) if err != nil { return err } + // Try to get the environment to retrieve configured parameter values (optional) + // If this fails, we'll just continue with the original behavior + var environment *v20231001preview.EnvironmentResource + if env, err := client.GetEnvironment(ctx, r.Workspace.Environment); err == nil { + environment = &env + } + recipe := types.EnvironmentRecipe{ Name: r.RecipeName, ResourceType: r.ResourceType, @@ -180,6 +188,18 @@ func (r *Runner) Run(ctx context.Context) error { var recipeParams []types.RecipeParameter + // Get environment-specific parameter values for this recipe (if environment is available) + var environmentParameters map[string]any + if environment != nil && environment.Properties.Recipes != nil { + if resourceTypeRecipes, exists := environment.Properties.Recipes[r.ResourceType]; exists { + if recipeProps, exists := resourceTypeRecipes[r.RecipeName]; exists { + if recipePropsActual := recipeProps.GetRecipeProperties(); recipePropsActual != nil { + environmentParameters = recipePropsActual.Parameters + } + } + } + } + for parameter := range recipeDetails.Parameters { values := recipeDetails.Parameters[parameter].(map[string]any) @@ -190,6 +210,7 @@ func (r *Runner) Run(ctx context.Context) error { MinValue: "-", } + // Process template schema information for paramDetailName, paramDetailValue := range values { switch paramDetailName { case "type": @@ -203,6 +224,13 @@ func (r *Runner) Run(ctx context.Context) error { } } + // Override with environment-specific value if it exists + if environmentParameters != nil { + if envValue, exists := environmentParameters[parameter]; exists { + paramItem.DefaultValue = envValue + } + } + recipeParams = append(recipeParams, paramItem) } diff --git a/pkg/cli/cmd/recipe/show/show_test.go b/pkg/cli/cmd/recipe/show/show_test.go index d4021a0794..b120ef69e6 100644 --- a/pkg/cli/cmd/recipe/show/show_test.go +++ b/pkg/cli/cmd/recipe/show/show_test.go @@ -18,6 +18,7 @@ package show import ( "context" + "fmt" "testing" "go.uber.org/mock/gomock" @@ -322,4 +323,188 @@ func Test_Run(t *testing.T) { } require.Equal(t, expected, outputSink.Writes) }) + + t.Run("Show recipe details with environment parameter values - Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + envRecipe := v20231001preview.RecipeGetMetadataResponse{ + TemplateKind: to.Ptr(recipes.TemplateKindBicep), + TemplatePath: to.Ptr("ghcr.io/testpublicrecipe/bicep/modules/openai:v1"), + Parameters: map[string]any{ + "location": map[string]any{ + "type": "string", + "defaultValue": "null", + }, + "resource_group_name": map[string]any{ + "type": "string", + "defaultValue": "null", + }, + }, + } + recipe := types.EnvironmentRecipe{ + Name: "default", + ResourceType: "Radius.Resources/openAI", + TemplateKind: recipes.TemplateKindBicep, + TemplatePath: "ghcr.io/testpublicrecipe/bicep/modules/openai:v1", + } + + // Environment has configured parameter values + envResource := v20231001preview.EnvironmentResource{ + Properties: &v20231001preview.EnvironmentProperties{ + Recipes: map[string]map[string]v20231001preview.RecipePropertiesClassification{ + "Radius.Resources/openAI": { + "default": &v20231001preview.BicepRecipeProperties{ + TemplateKind: to.Ptr(recipes.TemplateKindBicep), + TemplatePath: to.Ptr("ghcr.io/testpublicrecipe/bicep/modules/openai:v1"), + Parameters: map[string]any{ + "location": "eastus", + "resource_group_name": "my-rg", + }, + }, + }, + }, + }, + } + + // Expected parameters should show environment values, not template defaults + recipeParams := []types.RecipeParameter{ + { + Name: "resource_group_name", + Type: "string", + MaxValue: "-", + MinValue: "-", + DefaultValue: "my-rg", // Environment value overrides template default + }, + { + Name: "location", + Type: "string", + MaxValue: "-", + MinValue: "-", + DefaultValue: "eastus", // Environment value overrides template default + }, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). + Return(envRecipe, nil).Times(1) + + appManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), gomock.Any()). + Return(envResource, nil).Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + Format: "table", + RecipeName: "default", + ResourceType: "Radius.Resources/openAI", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: recipe, + Options: common.RecipeFormat(), + }, + output.LogOutput{ + Format: "", + }, + output.FormattedOutput{ + Format: "table", + Obj: recipeParams, + Options: common.RecipeParametersFormat(), + }, + } + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Show recipe details when GetEnvironment fails - Success", func(t *testing.T) { + ctrl := gomock.NewController(t) + envRecipe := v20231001preview.RecipeGetMetadataResponse{ + TemplateKind: to.Ptr(recipes.TemplateKindBicep), + TemplatePath: to.Ptr("ghcr.io/testpublicrecipe/bicep/modules/openai:v1"), + Parameters: map[string]any{ + "location": map[string]any{ + "type": "string", + "defaultValue": "westus", + }, + "resource_group_name": map[string]any{ + "type": "string", + "defaultValue": "null", + }, + }, + } + recipe := types.EnvironmentRecipe{ + Name: "default", + ResourceType: "Radius.Resources/openAI", + TemplateKind: recipes.TemplateKindBicep, + TemplatePath: "ghcr.io/testpublicrecipe/bicep/modules/openai:v1", + } + + // Expected parameters should show template defaults since environment call fails + recipeParams := []types.RecipeParameter{ + { + Name: "resource_group_name", + Type: "string", + MaxValue: "-", + MinValue: "-", + DefaultValue: "null", // Template default since environment fails + }, + { + Name: "location", + Type: "string", + MaxValue: "-", + MinValue: "-", + DefaultValue: "westus", // Template default since environment fails + }, + } + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). + Return(envRecipe, nil).Times(1) + + // Mock GetEnvironment to fail - this should be handled gracefully + // The environment will be nil, so we get original behavior + appManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), gomock.Any()). + Return(v20231001preview.EnvironmentResource{}, fmt.Errorf("environment not found")).Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + Format: "table", + RecipeName: "default", + ResourceType: "Radius.Resources/openAI", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.FormattedOutput{ + Format: "table", + Obj: recipe, + Options: common.RecipeFormat(), + }, + output.LogOutput{ + Format: "", + }, + output.FormattedOutput{ + Format: "table", + Obj: recipeParams, + Options: common.RecipeParametersFormat(), + }, + } + require.Equal(t, expected, outputSink.Writes) + }) } From 4b59285f6ed8ca62cc27dc902b18588b1318b378 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:40:55 -0800 Subject: [PATCH 2/5] chore(deps): bump securego/gosec from 538a05cc5d6eb7bb41624e48f6e5019cccb1a2b8 to 082deb6cee063d5b8ce740fbee614460d2c2211b in the github-actions group (#11001) Bumps the github-actions group with 1 update: [securego/gosec](https://github.com/securego/gosec). Updates `securego/gosec` from 538a05cc5d6eb7bb41624e48f6e5019cccb1a2b8 to 082deb6cee063d5b8ce740fbee614460d2c2211b
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Alec13355 --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7e24a87296..4185ac46a1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -129,7 +129,7 @@ jobs: - name: Perform GoSec Analysis if: matrix.language == 'custom-gosec' - uses: securego/gosec@538a05cc5d6eb7bb41624e48f6e5019cccb1a2b8 + uses: securego/gosec@082deb6cee063d5b8ce740fbee614460d2c2211b with: args: -no-fail -fmt sarif -out gosec-results.sarif ./... continue-on-error: true From 1f6878e23a7e47d63c579b34438af6bfa17419b0 Mon Sep 17 00:00:00 2001 From: Lakshmi Javadekar <103459615+lakshmimsft@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:16:48 -0800 Subject: [PATCH 3/5] Add support for new 'x-radius-sensitive' annotation (#10999) # Description This pull request adds support for a new `x-radius-sensitive` annotation in schema definitions, allowing string and object types to be marked as sensitive for downstream processing. Updates for manifest schema validation to follow in separate PR. ref: Design Doc: [2025-11-11-secrets-redactdata](https://github.com/radius-project/design-notes/blob/main/resources/2025-11-11-secrets-redactdata.md) - This pull request adds or changes features of Radius and has an approved issue (issue link #10421). Fixes: #10421 ## 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. - [] Yes - [x] Not applicable - A design document PR is created in the [design-notes repository](https://github.com/radius-project/design-notes/), if new APIs are being introduced. - [x] Yes - [] Not applicable - The design document has been reviewed and approved by Radius maintainers/approvers. - [x] Yes - [] Not applicable - 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. - [] Yes - [x] Not applicable - 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. - [] Yes - [x] Not applicable - 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. - [] Yes - [x] Not applicable Signed-off-by: lakshmimsft Signed-off-by: Alec13355 --- bicep-tools/pkg/converter/converter.go | 28 +++- bicep-tools/pkg/converter/converter_test.go | 173 ++++++++++++++++++++ bicep-tools/pkg/manifest/manifest.go | 8 + bicep-tools/pkg/manifest/manifest_test.go | 153 +++++++++++++++++ 4 files changed, 358 insertions(+), 4 deletions(-) diff --git a/bicep-tools/pkg/converter/converter.go b/bicep-tools/pkg/converter/converter.go index 7f31d4f648..bb641906e6 100644 --- a/bicep-tools/pkg/converter/converter.go +++ b/bicep-tools/pkg/converter/converter.go @@ -204,15 +204,28 @@ func addSchemaTypeInternal(schema *manifest.Schema, name string, typeFactory *fa if len(schema.Enum) > 0 { var enumTypeRefs []types.ITypeReference for _, value := range schema.Enum { - stringLiteralType := typeFactory.CreateStringLiteralType(value) + var stringLiteralType *types.StringLiteralType + // Check if parent string type is marked sensitive + if schema.IsSensitive != nil && *schema.IsSensitive { + stringLiteralType = typeFactory.CreateSensitiveStringLiteralType(value) + } else { + stringLiteralType = typeFactory.CreateStringLiteralType(value) + } enumTypeRefs = append(enumTypeRefs, typeFactory.GetReference(stringLiteralType)) } unionType := typeFactory.CreateUnionType(enumTypeRefs) return typeFactory.GetReference(unionType), nil } - // Regular string without constraints - stringType := typeFactory.CreateStringType() + // Regular string - check if it should be sensitive + var stringType *types.StringType + if schema.IsSensitive != nil && *schema.IsSensitive { + // Use CreateStringTypeWithConstraints with sensitive=true + stringType = typeFactory.CreateStringTypeWithConstraints(nil, nil, "", true) + } else { + // Regular non-sensitive string + stringType = typeFactory.CreateStringType() + } return typeFactory.GetReference(stringType), nil case "enum": @@ -256,7 +269,14 @@ func addSchemaTypeInternal(schema *manifest.Schema, name string, typeFactory *fa return nil, fmt.Errorf("failed to add object properties: %w", err) } - objectType := typeFactory.CreateObjectType(name, nil, nil, nil) + // Determine sensitive flag for object + var sensitive *bool + if schema.IsSensitive != nil && *schema.IsSensitive { + trueVal := true + sensitive = &trueVal + } + + objectType := typeFactory.CreateObjectType(name, nil, nil, sensitive) objectType.Properties = objectProperties // Handle additionalProperties if specified diff --git a/bicep-tools/pkg/converter/converter_test.go b/bicep-tools/pkg/converter/converter_test.go index cd32829fa4..0a00e9fad9 100644 --- a/bicep-tools/pkg/converter/converter_test.go +++ b/bicep-tools/pkg/converter/converter_test.go @@ -1116,3 +1116,176 @@ func TestAddResourceTypeForAPIVersion_WithAdditionalProperties(t *testing.T) { t.Errorf("Expected status union to have 2 elements, got %d", len(statusType.Elements)) } } + +func TestAddSchemaType_SensitiveString(t *testing.T) { + sensitive := true + schema := &manifest.Schema{ + Type: "string", + IsSensitive: &sensitive, + } + typeFactory := factory.NewTypeFactory() + + result, err := addSchemaType(schema, "password", typeFactory) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + allTypes := typeFactory.GetTypes() + typeRef, ok := result.(types.TypeReference) + if !ok { + t.Fatal("Expected result to be a TypeReference") + } + addedType, ok := allTypes[typeRef.Ref].(*types.StringType) + if !ok { + t.Fatal("Expected result to be a StringType") + } + + if !addedType.Sensitive { + t.Error("Expected string to be marked as sensitive") + } +} + +func TestAddSchemaType_NonSensitiveString(t *testing.T) { + schema := &manifest.Schema{Type: "string"} + typeFactory := factory.NewTypeFactory() + + result, err := addSchemaType(schema, "username", typeFactory) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + allTypes := typeFactory.GetTypes() + typeRef, ok := result.(types.TypeReference) + if !ok { + t.Fatal("Expected result to be a TypeReference") + } + addedType, ok := allTypes[typeRef.Ref].(*types.StringType) + if !ok { + t.Fatal("Expected result to be a StringType") + } + + if addedType.Sensitive { + t.Error("Expected string to NOT be marked as sensitive by default") + } +} + +func TestAddSchemaType_SensitiveObject(t *testing.T) { + sensitive := true + schema := &manifest.Schema{ + Type: "object", + Properties: map[string]manifest.Schema{ + "username": {Type: "string"}, + "password": {Type: "string"}, + }, + IsSensitive: &sensitive, + } + typeFactory := factory.NewTypeFactory() + + result, err := addSchemaType(schema, "credentials", typeFactory) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + allTypes := typeFactory.GetTypes() + typeRef, ok := result.(types.TypeReference) + if !ok { + t.Fatal("Expected result to be a TypeReference") + } + addedType, ok := allTypes[typeRef.Ref].(*types.ObjectType) + if !ok { + t.Fatal("Expected result to be an ObjectType") + } + + if addedType.Sensitive == nil || !*addedType.Sensitive { + t.Error("Expected object to be marked as sensitive") + } +} + +func TestAddSchemaType_SensitiveStringEnum(t *testing.T) { + sensitive := true + schema := &manifest.Schema{ + Type: "string", + Enum: []string{"secret1", "secret2", "secret3"}, + IsSensitive: &sensitive, + } + typeFactory := factory.NewTypeFactory() + + result, err := addSchemaType(schema, "secretType", typeFactory) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + allTypes := typeFactory.GetTypes() + typeRef, ok := result.(types.TypeReference) + if !ok { + t.Fatal("Expected result to be a TypeReference") + } + unionType, ok := allTypes[typeRef.Ref].(*types.UnionType) + if !ok { + t.Fatal("Expected result to be a UnionType") + } + + // Verify each literal is marked sensitive + if len(unionType.Elements) != 3 { + t.Errorf("Expected 3 enum elements, got %d", len(unionType.Elements)) + } + + for i, element := range unionType.Elements { + elementRef, ok := element.(types.TypeReference) + if !ok { + t.Fatalf("Expected element %d to be a TypeReference", i) + } + stringLiteral, ok := allTypes[elementRef.Ref].(*types.StringLiteralType) + if !ok { + t.Fatalf("Expected element %d to be a StringLiteralType", i) + } + if !stringLiteral.Sensitive { + t.Errorf("Expected enum literal '%s' to be marked as sensitive", stringLiteral.Value) + } + } +} + +func TestAddSchemaType_ArrayOfSensitiveObjects(t *testing.T) { + sensitive := true + schema := &manifest.Schema{ + Type: "array", + Items: &manifest.Schema{ + Type: "object", + Properties: map[string]manifest.Schema{ + "key": {Type: "string"}, + }, + IsSensitive: &sensitive, + }, + } + typeFactory := factory.NewTypeFactory() + + result, err := addSchemaType(schema, "secretsList", typeFactory) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + allTypes := typeFactory.GetTypes() + typeRef, ok := result.(types.TypeReference) + if !ok { + t.Fatal("Expected result to be a TypeReference") + } + arrayType, ok := allTypes[typeRef.Ref].(*types.ArrayType) + if !ok { + t.Fatal("Expected result to be an ArrayType") + } + + // Get the item type + itemRef, ok := arrayType.ItemType.(types.TypeReference) + if !ok { + t.Fatal("Expected array item type to be a TypeReference") + } + itemObject, ok := allTypes[itemRef.Ref].(*types.ObjectType) + if !ok { + t.Fatal("Expected array item to be an ObjectType") + } + + // Item object should be sensitive + if itemObject.Sensitive == nil || !*itemObject.Sensitive { + t.Error("Expected array item object to be marked as sensitive") + } +} diff --git a/bicep-tools/pkg/manifest/manifest.go b/bicep-tools/pkg/manifest/manifest.go index fa8d0a6bf5..4f31a76a74 100644 --- a/bicep-tools/pkg/manifest/manifest.go +++ b/bicep-tools/pkg/manifest/manifest.go @@ -38,6 +38,7 @@ type Schema struct { ReadOnly *bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"` Items *Schema `yaml:"items,omitempty" json:"items,omitempty"` Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"` + IsSensitive *bool `yaml:"x-radius-sensitive,omitempty" json:"x-radius-sensitive,omitempty"` } // ParseManifest parses a YAML manifest string into a ResourceProvider struct @@ -120,6 +121,13 @@ func (s *Schema) Validate(context string) error { return fmt.Errorf("invalid schema type '%s' in %s", s.Type, context) } + // Validate x-radius-sensitive is only used on valid types + if s.IsSensitive != nil && *s.IsSensitive { + if s.Type != "string" && s.Type != "object" { + return fmt.Errorf("x-radius-sensitive annotation is only supported on string and object types, got '%s' in %s", s.Type, context) + } + } + // Validate nested properties if this is an object type if s.Type == "object" && s.Properties != nil { for propName, propSchema := range s.Properties { diff --git a/bicep-tools/pkg/manifest/manifest_test.go b/bicep-tools/pkg/manifest/manifest_test.go index d4eac10b4e..d866368771 100644 --- a/bicep-tools/pkg/manifest/manifest_test.go +++ b/bicep-tools/pkg/manifest/manifest_test.go @@ -3,6 +3,7 @@ package manifest import ( "os" "path/filepath" + "strings" "testing" ) @@ -409,3 +410,155 @@ types: t.Errorf("Expected description to be 'A map of key-value pairs', got: %v", mymapProp.AdditionalProperties.Description) } } + +func TestSchema_Validate_SensitiveOnInvalidType(t *testing.T) { + sensitive := true + + // Test that x-radius-sensitive on integer fails validation + intSchema := Schema{ + Type: "integer", + IsSensitive: &sensitive, + } + if err := intSchema.Validate("test"); err == nil { + t.Error("Expected validation error for x-radius-sensitive on integer type") + } else if !strings.Contains(err.Error(), "only supported on string and object types") { + t.Errorf("Expected error message about supported types, got: %v", err) + } + + // Test that x-radius-sensitive on boolean fails validation + boolSchema := Schema{ + Type: "boolean", + IsSensitive: &sensitive, + } + if err := boolSchema.Validate("test"); err == nil { + t.Error("Expected validation error for x-radius-sensitive on boolean type") + } else if !strings.Contains(err.Error(), "only supported on string and object types") { + t.Errorf("Expected error message about supported types, got: %v", err) + } + + // Test that x-radius-sensitive on array fails validation + arraySchema := Schema{ + Type: "array", + IsSensitive: &sensitive, + Items: &Schema{ + Type: "string", + }, + } + if err := arraySchema.Validate("test"); err == nil { + t.Error("Expected validation error for x-radius-sensitive on array type") + } else if !strings.Contains(err.Error(), "only supported on string and object types") { + t.Errorf("Expected error message about supported types, got: %v", err) + } + + // Test that x-radius-sensitive on enum fails validation + enumSchema := Schema{ + Type: "enum", + Enum: []string{"value1", "value2"}, + IsSensitive: &sensitive, + } + if err := enumSchema.Validate("test"); err == nil { + t.Error("Expected validation error for x-radius-sensitive on enum type") + } else if !strings.Contains(err.Error(), "only supported on string and object types") { + t.Errorf("Expected error message about supported types, got: %v", err) + } +} + +func TestSchema_Validate_SensitiveOnValidTypes(t *testing.T) { + sensitive := true + + // Test that x-radius-sensitive on string passes validation + stringSchema := Schema{ + Type: "string", + IsSensitive: &sensitive, + } + if err := stringSchema.Validate("test"); err != nil { + t.Errorf("Expected no validation error for x-radius-sensitive on string, got: %v", err) + } + + // Test that x-radius-sensitive on object passes validation + objectSchema := Schema{ + Type: "object", + Properties: map[string]Schema{ + "prop1": {Type: "string"}, + }, + IsSensitive: &sensitive, + } + if err := objectSchema.Validate("test"); err != nil { + t.Errorf("Expected no validation error for x-radius-sensitive on object, got: %v", err) + } +} + +func TestParseManifest_WithSensitiveAnnotation(t *testing.T) { + input := ` +namespace: MyCompany.Resources +types: + testResources: + apiVersions: + '2025-01-01-preview': + schema: + type: object + properties: + username: + type: string + description: "Public username" + password: + type: string + description: "User password" + x-radius-sensitive: true + apiKey: + type: string + x-radius-sensitive: true + credentials: + type: object + x-radius-sensitive: true + properties: + key: + type: string + value: + type: string + capabilities: ['Recipes'] +` + + result, err := ParseManifest(input) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + schema := result.Types["testResources"].APIVersions["2025-01-01-preview"].Schema + + // Test that username is NOT sensitive (default) + usernameProp, exists := schema.Properties["username"] + if !exists { + t.Fatal("Expected property 'username' to exist") + } + if usernameProp.IsSensitive != nil && *usernameProp.IsSensitive { + t.Error("Expected username to NOT be marked as sensitive") + } + + // Test that password IS sensitive + passwordProp, exists := schema.Properties["password"] + if !exists { + t.Fatal("Expected property 'password' to exist") + } + if passwordProp.IsSensitive == nil || !*passwordProp.IsSensitive { + t.Error("Expected password to be marked as sensitive") + } + + // Test that apiKey IS sensitive + apiKeyProp, exists := schema.Properties["apiKey"] + if !exists { + t.Fatal("Expected property 'apiKey' to exist") + } + if apiKeyProp.IsSensitive == nil || !*apiKeyProp.IsSensitive { + t.Error("Expected apiKey to be marked as sensitive") + } + + // Test that credentials object IS sensitive + credentialsProp, exists := schema.Properties["credentials"] + if !exists { + t.Fatal("Expected property 'credentials' to exist") + } + if credentialsProp.IsSensitive == nil || !*credentialsProp.IsSensitive { + t.Error("Expected credentials object to be marked as sensitive") + } +} From fd12a3771a6693f5fd78d0ea8577dc2597400aad Mon Sep 17 00:00:00 2001 From: Alec13355 Date: Tue, 6 Jan 2026 23:10:21 -0600 Subject: [PATCH 4/5] Mocking framework all needs method registered or bombs out on test Signed-off-by: Alec13355 --- pkg/cli/cmd/recipe/show/show_test.go | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pkg/cli/cmd/recipe/show/show_test.go b/pkg/cli/cmd/recipe/show/show_test.go index b120ef69e6..9c44c997f7 100644 --- a/pkg/cli/cmd/recipe/show/show_test.go +++ b/pkg/cli/cmd/recipe/show/show_test.go @@ -94,6 +94,19 @@ func Test_Validate(t *testing.T) { radcli.SharedValidateValidation(t, NewCommand, testcases) } +// setupTestClient creates a mock client with default behavior for common calls +func setupTestClient(ctrl *gomock.Controller) *clients.MockApplicationsManagementClient { + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + + // Default behavior: GetEnvironment returns "not found" error (most common case) + appManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), gomock.Any()). + Return(v20231001preview.EnvironmentResource{}, fmt.Errorf("not found")). + AnyTimes() + + return appManagementClient +} + func Test_Run(t *testing.T) { t.Run("Show bicep recipe details - Success", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -133,7 +146,7 @@ func Test_Run(t *testing.T) { }, } - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient := setupTestClient(ctrl) appManagementClient.EXPECT(). GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). Return(envRecipe, nil).Times(1) @@ -210,7 +223,7 @@ func Test_Run(t *testing.T) { }, } - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient := setupTestClient(ctrl) appManagementClient.EXPECT(). GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). Return(envRecipe, nil).Times(1) @@ -247,7 +260,7 @@ func Test_Run(t *testing.T) { require.Equal(t, expected, outputSink.Writes) }) - t.Run("Show terraformn recipe details - Success", func(t *testing.T) { + t.Run("Show terraform recipe details - Success", func(t *testing.T) { ctrl := gomock.NewController(t) envRecipe := v20231001preview.RecipeGetMetadataResponse{ TemplateKind: to.Ptr(recipes.TemplateKindTerraform), @@ -287,7 +300,7 @@ func Test_Run(t *testing.T) { }, } - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient := setupTestClient(ctrl) appManagementClient.EXPECT(). GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). Return(envRecipe, nil).Times(1) @@ -465,17 +478,11 @@ func Test_Run(t *testing.T) { }, } - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient := setupTestClient(ctrl) appManagementClient.EXPECT(). GetRecipeMetadata(gomock.Any(), gomock.Any(), gomock.Any()). Return(envRecipe, nil).Times(1) - // Mock GetEnvironment to fail - this should be handled gracefully - // The environment will be nil, so we get original behavior - appManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), gomock.Any()). - Return(v20231001preview.EnvironmentResource{}, fmt.Errorf("environment not found")).Times(1) - outputSink := &output.MockOutput{} runner := &Runner{ From d0beb81a207dacac263d3a0a29fb17fe273577b3 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 11:21:31 -0800 Subject: [PATCH 5/5] Fix DE functional test workflow to be consistent with publish workflow (#10963) # Description * Update Radius DE sync: radius side (https://github.com/azure-octo/deployment-engine/pull/514) ## Type of change - This pull request is a minor refactor, code cleanup, test improvement, or other maintenance task and doesn't change the functionality of Radius (issue link optional). ## 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. - [ ] Yes - [x] Not applicable - A design document PR is created in the [design-notes repository](https://github.com/radius-project/design-notes/), if new APIs are being introduced. - [ ] Yes - [x] Not applicable - The design document has been reviewed and approved by Radius maintainers/approvers. - [ ] Yes - [x] Not applicable - 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. - [ ] Yes - [x] Not applicable - 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. - [ ] Yes - [x] Not applicable - 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. - [ ] Yes - [x] Not applicable --------- Signed-off-by: willdavsmith Signed-off-by: Alec13355 --- .../copy-deployment-engine-image/action.yml | 4 ++- .github/workflows/functional-test-cloud.yaml | 21 ++++---------- .../workflows/functional-test-noncloud.yaml | 28 ++++--------------- 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/.github/actions/copy-deployment-engine-image/action.yml b/.github/actions/copy-deployment-engine-image/action.yml index e8c097a314..5ef95ca64f 100644 --- a/.github/actions/copy-deployment-engine-image/action.yml +++ b/.github/actions/copy-deployment-engine-image/action.yml @@ -37,7 +37,9 @@ runs: env: SRC_IMAGE: ${{ inputs.src_image }} run: | - # Example: SRC_REGISTRY_NAME=myregistry + # Example: + # SRC_IMAGE=radiusdeploymentengine.azurecr.io/deployment-engine + # SRC_REGISTRY_NAME=radiusdeploymentengine SRC_REGISTRY_NAME=$(echo "${SRC_IMAGE}" | cut -d'.' -f1) az acr login --name "${SRC_REGISTRY_NAME}" diff --git a/.github/workflows/functional-test-cloud.yaml b/.github/workflows/functional-test-cloud.yaml index c93741cb4b..fc92d7cebf 100644 --- a/.github/workflows/functional-test-cloud.yaml +++ b/.github/workflows/functional-test-cloud.yaml @@ -32,6 +32,10 @@ on: # Dispatch on external events repository_dispatch: types: [deployment-engine.run-functional-tests] + # Expected client_payload: + # src_image: Source image in ACR (e.g. radiusdeploymentengine.azurecr.io/deployment-engine) + # dest_image: Destination image in GHCR (e.g. ghcr.io/radius-project/deployment-engine) + # tag: Tag for the image (e.g. latest, 0.1, 0.1.0-rc1, pr-123) workflow_run: workflows: [Approve Functional Tests] types: @@ -241,11 +245,11 @@ jobs: if: github.event_name == 'repository_dispatch' shell: bash env: - IMAGE_NAME: ${{ github.event.client_payload.image_name }} + DEST_IMAGE: ${{ github.event.client_payload.dest_image }} TAG: ${{ github.event.client_payload.tag }} run: | { - echo "DE_IMAGE=${IMAGE_NAME}" + echo "DE_IMAGE=${DEST_IMAGE}" echo "DE_TAG=${TAG}" } >> "$GITHUB_ENV" @@ -324,19 +328,6 @@ jobs: submodules: recursive persist-credentials: false - - name: Copy Deployment Engine image to GHCR - if: github.event_name == 'repository_dispatch' - uses: ./.github/actions/copy-deployment-engine-image - env: - GHCR_PASSWORD: ${{ secrets.GH_RAD_CI_BOT_PAT }} - DE_CONTAINER_AZURE_CLIENT_ID: ${{ secrets.DE_CONTAINER_AZURE_CLIENT_ID }} - DE_CONTAINER_AZURE_TENANT_ID: ${{ secrets.DE_CONTAINER_AZURE_TENANT_ID }} - DE_CONTAINER_AZURE_SUBSCRIPTION_ID: ${{ secrets.DE_CONTAINER_AZURE_SUBSCRIPTION_ID }} - with: - tag: ${{ env.DE_TAG }} - src_image: ${{ env.DE_IMAGE }} - dest_image: ghcr.io/radius-project/deployment-engine - - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: diff --git a/.github/workflows/functional-test-noncloud.yaml b/.github/workflows/functional-test-noncloud.yaml index 6b5831461d..6363648455 100644 --- a/.github/workflows/functional-test-noncloud.yaml +++ b/.github/workflows/functional-test-noncloud.yaml @@ -32,6 +32,10 @@ on: # Dispatch on external events repository_dispatch: types: [deployment-engine.run-functional-tests] + # Expected client_payload: + # src_image: Source image in ACR (e.g. radiusdeploymentengine.azurecr.io/deployment-engine) + # dest_image: Destination image in GHCR (e.g. ghcr.io/radius-project/deployment-engine) + # tag: Tag for the image (e.g. latest, 0.1, 0.1.0-rc1, pr-123) pull_request: branches: - main @@ -115,34 +119,14 @@ jobs: if: github.event_name == 'repository_dispatch' shell: bash env: - IMAGE_NAME: ${{ github.event.client_payload.image_name }} + DEST_IMAGE: ${{ github.event.client_payload.dest_image }} TAG: ${{ github.event.client_payload.tag }} run: | { - echo "DE_IMAGE=${IMAGE_NAME}" + echo "DE_IMAGE=${DEST_IMAGE}" echo "DE_TAG=${TAG}" } >> "$GITHUB_ENV" - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - repository: ${{ env.CHECKOUT_REPO }} - ref: ${{ env.CHECKOUT_REF }} - persist-credentials: false - - - name: Copy Deployment Engine image to GHCR - if: github.event_name == 'repository_dispatch' - uses: ./.github/actions/copy-deployment-engine-image - env: - GHCR_PASSWORD: ${{ secrets.GH_RAD_CI_BOT_PAT }} - DE_CONTAINER_AZURE_CLIENT_ID: ${{ secrets.DE_CONTAINER_AZURE_CLIENT_ID }} - DE_CONTAINER_AZURE_TENANT_ID: ${{ secrets.DE_CONTAINER_AZURE_TENANT_ID }} - DE_CONTAINER_AZURE_SUBSCRIPTION_ID: ${{ secrets.DE_CONTAINER_AZURE_SUBSCRIPTION_ID }} - with: - tag: ${{ env.DE_TAG }} - src_image: ${{ env.DE_IMAGE }} - dest_image: ghcr.io/radius-project/deployment-engine - - name: Generate ID for release id: gen-id run: |