From 381c178bf96dc32a6f6afd820f9b4703f3c1198f Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Mon, 18 May 2026 16:52:00 -0500 Subject: [PATCH 01/13] Update rad commands for concise output (#11773) This pull request standardizes and simplifies log messages across several CLI commands related to applications and environments. The changes focus on making log output more consistent, concise, and aligned with resource identifiers, while also cleaning up unnecessary progress or verbose messages. Corresponding test cases have been updated to match the new output formats. **Standardization of log messages:** * Application delete commands now log messages using resource-style identifiers, such as `"Applications.Core/applications/%s deleted"` and `"Applications.Core/applications/%s not found"`, instead of more verbose or variable messages. [[1]](diffhunk://#diff-87907dd86f66c5e0992fef2ea8d8bc09cf79e4cf4e6d9108cb137a536da03c10L166-R166) [[2]](diffhunk://#diff-87907dd86f66c5e0992fef2ea8d8bc09cf79e4cf4e6d9108cb137a536da03c10L197-R205) [[3]](diffhunk://#diff-0d19f5b0c83b43f0f60b644a588f32e118296e1c8232e239d3dcc164c53d8f0bL174-R174) [[4]](diffhunk://#diff-0d19f5b0c83b43f0f60b644a588f32e118296e1c8232e239d3dcc164c53d8f0bL243-R243) [[5]](diffhunk://#diff-0d19f5b0c83b43f0f60b644a588f32e118296e1c8232e239d3dcc164c53d8f0bL357-R357) [[6]](diffhunk://#diff-0d19f5b0c83b43f0f60b644a588f32e118296e1c8232e239d3dcc164c53d8f0bL525-R525) * Environment create, delete, and update commands (including preview variants) now use consistent messages like `"Applications.Core/environments/%s created"`, `"Applications.Core/environments/%s deleted"`, `"Applications.Core/environments/%s updated"`, and their `"Radius.Core/environments/%s ..."` equivalents for preview commands. [[1]](diffhunk://#diff-870b0a9ddae1c288c449a9832bae630c5ac691849c4638f97db83a3aff1474bfL173-R171) [[2]](diffhunk://#diff-8714424570a548de543f7c2439e120756644a66bef1e65ffde34deb9c79182b9L191-L197) [[3]](diffhunk://#diff-6e4ca37857043430dd23825866827a35d340a873d66f164441763816b3138c3bL156-R154) [[4]](diffhunk://#diff-623c5339f1462c2be84f649e1df69d614cbd04fe523e854be5c8c51ff1930581L170-L176) [[5]](diffhunk://#diff-d332ce038a22ec95a48f0600648e5b6dadb8d358e8414b4285c4a2d938780439L34-R35) [[6]](diffhunk://#diff-d332ce038a22ec95a48f0600648e5b6dadb8d358e8414b4285c4a2d938780439L169-R177) [[7]](diffhunk://#diff-a9278b5777296d730e5b9d7a571cabec7045ac3e4a0d431b9cb1943ec0452317L36-R37) [[8]](diffhunk://#diff-a9278b5777296d730e5b9d7a571cabec7045ac3e4a0d431b9cb1943ec0452317L157-R151) [[9]](diffhunk://#diff-cc0d9ae95866ee9bc6780d1c664234d1860da36517a870828696934d72b31ec0L315-R320) [[10]](diffhunk://#diff-5666f913c4913108ef57c2f23dec3f4b6a4fc6bc653f2b0c51982b9619fb6e12L241-R239) **Removal of unnecessary or redundant log output:** * Progress messages such as "Creating Environment...", "Updating Environment...", and resource deletion counts have been removed for a cleaner CLI experience. [[1]](diffhunk://#diff-870b0a9ddae1c288c449a9832bae630c5ac691849c4638f97db83a3aff1474bfL153-L154) [[2]](diffhunk://#diff-6e4ca37857043430dd23825866827a35d340a873d66f164441763816b3138c3bL144-L145) [[3]](diffhunk://#diff-d332ce038a22ec95a48f0600648e5b6dadb8d358e8414b4285c4a2d938780439L169-R177) [[4]](diffhunk://#diff-a9278b5777296d730e5b9d7a571cabec7045ac3e4a0d431b9cb1943ec0452317L141-L148) [[5]](diffhunk://#diff-cc0d9ae95866ee9bc6780d1c664234d1860da36517a870828696934d72b31ec0L315-R320) [[6]](diffhunk://#diff-5666f913c4913108ef57c2f23dec3f4b6a4fc6bc653f2b0c51982b9619fb6e12L231-L232) **Test updates for new log formats:** * All affected test cases have been updated to expect the new standardized log messages and to remove checks for now-absent progress or verbose messages. [[1]](diffhunk://#diff-8714424570a548de543f7c2439e120756644a66bef1e65ffde34deb9c79182b9L191-L197) [[2]](diffhunk://#diff-8714424570a548de543f7c2439e120756644a66bef1e65ffde34deb9c79182b9L250-R249) [[3]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL148-R150) [[4]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL204-R203) [[5]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL263-R259) [[6]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL326-R315) [[7]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL379-R359) [[8]](diffhunk://#diff-a595cf515e0d0ce861a0710659d126b2dd7c0748647bebc245c72575ff1a7b7eL431-L434) [[9]](diffhunk://#diff-b832a493d5cce453ba312bc50cf533a7750c16b300c068739395600b543fe20cL101-R103) [[10]](diffhunk://#diff-f666f217fc3999475063fd8344f9f6cff023f1c98f27ce8bff6d6999b76ed049L111-R111) [[11]](diffhunk://#diff-c8b9a3315a934ca0d8e0b750b2872603b1c504239f5d310251e87e5fb6a8e4a2L344-R348) These changes improve the clarity and consistency of CLI output, making it easier for users to understand the results of their actions and for automated tools to parse log output.# Description ## Type of change - This pull request fixes a bug in Radius and has an approved issue Fixes: https://github.com/radius-project/radius/issues/11769 ## 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: Zach Casper Co-authored-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Signed-off-by: Zach Casper --- pkg/cli/cmd/app/delete/delete.go | 8 +- pkg/cli/cmd/app/delete/delete_test.go | 72 +- pkg/cli/cmd/env/create/create.go | 4 +- pkg/cli/cmd/env/create/create_test.go | 14 +- pkg/cli/cmd/env/create/preview/create.go | 6 +- pkg/cli/cmd/env/create/preview/create_test.go | 19 +- pkg/cli/cmd/env/delete/delete.go | 15 +- pkg/cli/cmd/env/delete/delete_test.go | 42 +- pkg/cli/cmd/env/delete/preview/delete.go | 12 +- pkg/cli/cmd/env/delete/preview/delete_test.go | 5 +- pkg/cli/cmd/env/update/objectformats.go | 51 -- pkg/cli/cmd/env/update/objectformats_test.go | 41 - .../cmd/env/update/preview/objectformats.go | 46 -- pkg/cli/cmd/env/update/preview/update.go | 30 +- pkg/cli/cmd/env/update/preview/update_test.go | 14 +- pkg/cli/cmd/env/update/update.go | 33 +- pkg/cli/cmd/env/update/update_test.go | 44 +- pkg/cli/cmd/group/create/create.go | 4 +- pkg/cli/cmd/group/create/create_test.go | 6 +- pkg/cli/cmd/group/delete/delete.go | 14 +- pkg/cli/cmd/group/delete/delete_test.go | 101 +-- pkg/cli/cmd/recipepack/delete/delete.go | 11 +- pkg/cli/cmd/recipepack/delete/delete_test.go | 8 +- pkg/cli/cmd/resource/create/create.go | 9 +- pkg/cli/cmd/resource/create/create_test.go | 6 + pkg/cli/cmd/resource/delete/delete.go | 7 +- pkg/cli/cmd/resource/delete/delete_test.go | 733 +++++++++--------- pkg/cli/cmd/resourceprovider/create/create.go | 26 +- .../resourceprovider/create/create_test.go | 44 +- pkg/cli/cmd/resourceprovider/delete/delete.go | 4 +- .../resourceprovider/delete/delete_test.go | 6 +- pkg/cli/cmd/resourcetype/create/create.go | 26 +- .../cmd/resourcetype/create/create_test.go | 66 +- pkg/cli/cmd/resourcetype/delete/delete.go | 4 +- .../cmd/resourcetype/delete/delete_test.go | 6 +- pkg/cli/cmd/workspace/create/create.go | 3 +- pkg/cli/cmd/workspace/create/create_test.go | 6 + pkg/cli/cmd/workspace/delete/delete.go | 1 + pkg/cli/cmd/workspace/delete/delete_test.go | 14 +- 39 files changed, 615 insertions(+), 946 deletions(-) delete mode 100644 pkg/cli/cmd/env/update/objectformats.go delete mode 100644 pkg/cli/cmd/env/update/objectformats_test.go delete mode 100644 pkg/cli/cmd/env/update/preview/objectformats.go diff --git a/pkg/cli/cmd/app/delete/delete.go b/pkg/cli/cmd/app/delete/delete.go index 6a90ebd29f..b09489c124 100644 --- a/pkg/cli/cmd/app/delete/delete.go +++ b/pkg/cli/cmd/app/delete/delete.go @@ -173,7 +173,7 @@ func (r *Runner) Run(ctx context.Context) error { app, err := client.GetApplication(ctx, r.ApplicationName) if clients.Is404Error(err) { - r.Output.LogInfo("Application '%s' does not exist or has already been deleted.", r.ApplicationName) + r.Output.LogInfo("Applications.Core/applications/%s not found", r.ApplicationName) return nil } else if err != nil { return err @@ -210,15 +210,15 @@ func (r *Runner) Run(ctx context.Context) error { }) if err != nil { if strings.Contains(err.Error(), "not found") { - r.Output.LogInfo("Application '%s' does not exist or has already been deleted.", r.ApplicationName) + r.Output.LogInfo("Applications.Core/applications/%s not found", r.ApplicationName) return nil } return clierrors.Message("Failed to delete application '%s': %v", r.ApplicationName, err) } if deleted { - r.Output.LogInfo("Application %s deleted successfully", r.ApplicationName) + r.Output.LogInfo("Applications.Core/applications/%s deleted", r.ApplicationName) } else { - r.Output.LogInfo("Application '%s' does not exist or has already been deleted.", r.ApplicationName) + r.Output.LogInfo("Applications.Core/applications/%s not found", r.ApplicationName) return nil } diff --git a/pkg/cli/cmd/app/delete/delete_test.go b/pkg/cli/cmd/app/delete/delete_test.go index f7c2d765b8..9b80da15e0 100644 --- a/pkg/cli/cmd/app/delete/delete_test.go +++ b/pkg/cli/cmd/app/delete/delete_test.go @@ -171,7 +171,7 @@ func Test_Delete(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Application %s deleted successfully", + Format: "Applications.Core/applications/%s deleted", Params: []any{"test-app"}, }, } @@ -240,7 +240,7 @@ func Test_Delete(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Application %s deleted successfully", + Format: "Applications.Core/applications/%s deleted", Params: []any{"test-app"}, }, } @@ -354,7 +354,7 @@ func Test_Delete(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Application '%s' does not exist or has already been deleted.", + Format: "Applications.Core/applications/%s not found", Params: []any{"test-app"}, }, } @@ -522,7 +522,69 @@ func Test_Delete(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Application '%s' does not exist or has already been deleted.", + Format: "Applications.Core/applications/%s not found", + Params: []any{"test-app"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Delete Returns False (already gone)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + deleteMock := delete.NewMockInterface(ctrl) + + appManagementClient.EXPECT(). + GetApplication(gomock.Any(), "test-app"). + Return(v20231001preview.ApplicationResource{ + Properties: &v20231001preview.ApplicationProperties{ + Environment: new("/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default"), + }, + }, nil). + Times(1) + + progressText := fmt.Sprintf("Deleting application '%s' from environment '%s'...", "test-app", "default") + deleteMock.EXPECT(). + DeleteApplicationWithProgress( + gomock.Any(), + appManagementClient, + clients.DeleteOptions{ + ApplicationNameOrID: "test-app", + ProgressText: progressText, + }, + ). + Return(false, 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: "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + Delete: deleteMock, + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Workspace: workspace, + Output: outputSink, + ApplicationName: "test-app", + EnvironmentName: "default", + Confirm: true, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "Applications.Core/applications/%s not found", Params: []any{"test-app"}, }, } @@ -591,6 +653,6 @@ func Test_Delete(t *testing.T) { require.Contains(t, warningOutput.Format, "WARNING") lastOutput, ok := outputSink.Writes[len(outputSink.Writes)-1].(output.LogOutput) require.True(t, ok) - require.Equal(t, "Application %s deleted successfully", lastOutput.Format) + require.Equal(t, "Applications.Core/applications/%s deleted", lastOutput.Format) }) } diff --git a/pkg/cli/cmd/env/create/create.go b/pkg/cli/cmd/env/create/create.go index de04f3acb0..548d0ad1a3 100644 --- a/pkg/cli/cmd/env/create/create.go +++ b/pkg/cli/cmd/env/create/create.go @@ -150,8 +150,6 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { // Run creates an environment in the specified resource group using the provided environment name and namespace, and // returns an error if unsuccessful. func (r *Runner) Run(ctx context.Context) error { - r.Output.LogInfo("Creating Environment...") - client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) if err != nil { return err @@ -170,7 +168,7 @@ func (r *Runner) Run(ctx context.Context) error { if err != nil { return err } - r.Output.LogInfo("Successfully created environment %q in resource group %q", r.EnvironmentName, r.ResourceGroupName) + r.Output.LogInfo("Applications.Core/environments/%s created", r.EnvironmentName) return nil } diff --git a/pkg/cli/cmd/env/create/create_test.go b/pkg/cli/cmd/env/create/create_test.go index ed5df50d23..2450ff707a 100644 --- a/pkg/cli/cmd/env/create/create_test.go +++ b/pkg/cli/cmd/env/create/create_test.go @@ -188,13 +188,9 @@ func Test_Run(t *testing.T) { expectedOutput := []any{ output.LogOutput{ - Format: "Creating Environment...", - }, - output.LogOutput{ - Format: "Successfully created environment %q in resource group %q", + Format: "Applications.Core/environments/%s created", Params: []any{ "default", - "test-group", }, }, } @@ -247,16 +243,10 @@ func Test_Run(t *testing.T) { ConfigFileInterface: configFileInterface, } - expectedOutput := []any{ - output.LogOutput{ - Format: "Creating Environment...", - }, - } - err := runner.Run(context.Background()) require.Error(t, err) require.Equal(t, expectedError, err) - require.Equal(t, expectedOutput, outputSink.Writes) + require.Empty(t, outputSink.Writes) }) } diff --git a/pkg/cli/cmd/env/create/preview/create.go b/pkg/cli/cmd/env/create/preview/create.go index 52e090c0f1..7f490a4199 100644 --- a/pkg/cli/cmd/env/create/preview/create.go +++ b/pkg/cli/cmd/env/create/preview/create.go @@ -145,8 +145,6 @@ func (r *Runner) Run(ctx context.Context) error { r.RadiusCoreClientFactory = clientFactory } - r.Output.LogInfo("Creating Radius Core Environment %q...", r.EnvironmentName) - // Ensure the default resource group exists before creating recipe pack in it. mgmtClient, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace) if err != nil { @@ -185,6 +183,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } - r.Output.LogInfo("Successfully created environment %q in resource group %q with default recipe pack.", r.EnvironmentName, r.ResourceGroupName) - return nil + r.Output.LogInfo("Radius.Core/environments/%s created", r.EnvironmentName) + return nil } diff --git a/pkg/cli/cmd/env/create/preview/create_test.go b/pkg/cli/cmd/env/create/preview/create_test.go index f874640f5b..a9bbfcda94 100644 --- a/pkg/cli/cmd/env/create/preview/create_test.go +++ b/pkg/cli/cmd/env/create/preview/create_test.go @@ -189,17 +189,18 @@ func Test_Run(t *testing.T) { ResourceGroupName: "test-resource-group", } + expectedOutput := []any{ + output.LogOutput{ + Format: "Radius.Core/environments/%s created", + Params: []any{ + "testenv", + }, + }, + } + err = runner.Run(context.Background()) require.NoError(t, err) - - require.Contains(t, outputSink.Writes, output.LogOutput{ - Format: "Creating Radius Core Environment %q...", - Params: []interface{}{"testenv"}, - }) - require.Contains(t, outputSink.Writes, output.LogOutput{ - Format: "Successfully created environment %q in resource group %q with default recipe pack.", - Params: []interface{}{"testenv", "test-resource-group"}, - }) + require.Equal(t, expectedOutput, outputSink.Writes) }) t.Run("creates default recipe pack when not found", func(t *testing.T) { diff --git a/pkg/cli/cmd/env/delete/delete.go b/pkg/cli/cmd/env/delete/delete.go index 520937643a..b9b1d98d42 100644 --- a/pkg/cli/cmd/env/delete/delete.go +++ b/pkg/cli/cmd/env/delete/delete.go @@ -31,10 +31,8 @@ import ( ) const ( - msgEnvironmentDeleted = "Environment deleted" - msgEnvironmentNotFound = "Environment '%s' does not exist or has already been deleted." - msgDeletingEnvironment = "Deleting environment %s...\n" - msgDeletingResourceCount = "Deleting %d resource(s) in environment %s...\n" + msgEnvironmentDeleted = "Applications.Core/environments/%s deleted" + msgEnvironmentNotFound = "Applications.Core/environments/%s not found" ) // NewCommand creates an instance of the command and runner for the `rad env delete` command. @@ -166,24 +164,17 @@ func (r *Runner) Run(ctx context.Context) error { return err } if !confirmed { - r.Output.LogInfo("Environment %q NOT deleted", r.EnvironmentName) return nil } } - // Show progress messages - if totalResourceCount > 0 { - r.Output.LogInfo(msgDeletingResourceCount, totalResourceCount, r.EnvironmentName) - } - r.Output.LogInfo(msgDeletingEnvironment, r.EnvironmentName) - deleted, err := client.DeleteEnvironment(ctx, r.EnvironmentName) if err != nil { return err } if deleted { - r.Output.LogInfo(msgEnvironmentDeleted) + r.Output.LogInfo(msgEnvironmentDeleted, r.EnvironmentName) } else { r.Output.LogInfo(msgEnvironmentNotFound, r.EnvironmentName) } diff --git a/pkg/cli/cmd/env/delete/delete_test.go b/pkg/cli/cmd/env/delete/delete_test.go index 0c528d9b16..9c3aebf5b5 100644 --- a/pkg/cli/cmd/env/delete/delete_test.go +++ b/pkg/cli/cmd/env/delete/delete_test.go @@ -39,7 +39,7 @@ func Test_CommandValidation(t *testing.T) { } const ( - deleteConfirmationEmpty = "The environment %s is empty. Are you sure you want to delete the environment?" + deleteConfirmationEmpty = "The environment %s is empty. Are you sure you want to delete the environment?" deleteConfirmationWithResources = "The environment %s contains %d deployed resource(s). Are you sure you want to delete the environment and its resources?" ) @@ -145,12 +145,9 @@ func Test_Show(t *testing.T) { require.NoError(t, err) expected := []any{ - output.LogOutput{ - Format: msgDeletingEnvironment, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentDeleted, + Params: []any{"test-env"}, }, } @@ -201,12 +198,9 @@ func Test_Show(t *testing.T) { require.NoError(t, err) expected := []any{ - output.LogOutput{ - Format: msgDeletingEnvironment, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentDeleted, + Params: []any{"test-env"}, }, } @@ -260,16 +254,9 @@ func Test_Show(t *testing.T) { require.NoError(t, err) expected := []any{ - output.LogOutput{ - Format: msgDeletingResourceCount, - Params: []any{1, "test-env"}, - }, - output.LogOutput{ - Format: msgDeletingEnvironment, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentDeleted, + Params: []any{"test-env"}, }, } @@ -323,16 +310,9 @@ func Test_Show(t *testing.T) { require.NoError(t, err) expected := []any{ - output.LogOutput{ - Format: msgDeletingResourceCount, - Params: []any{1, "test-env"}, - }, - output.LogOutput{ - Format: msgDeletingEnvironment, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentDeleted, + Params: []any{"test-env"}, }, } @@ -376,13 +356,7 @@ func Test_Show(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) - expected := []any{ - output.LogOutput{ - Format: "Environment %q NOT deleted", - Params: []any{"test-env"}, - }, - } - require.Equal(t, expected, outputSink.Writes) + require.Empty(t, outputSink.Writes) }) // YES, this is a success case. Delete means "make it be gone", so if the environment is already @@ -428,10 +402,6 @@ func Test_Show(t *testing.T) { require.NoError(t, err) expected := []any{ - output.LogOutput{ - Format: msgDeletingEnvironment, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentNotFound, Params: []any{"test-env"}, diff --git a/pkg/cli/cmd/env/delete/preview/delete.go b/pkg/cli/cmd/env/delete/preview/delete.go index 3d27dbf01c..8733a6fb0a 100644 --- a/pkg/cli/cmd/env/delete/preview/delete.go +++ b/pkg/cli/cmd/env/delete/preview/delete.go @@ -33,10 +33,8 @@ import ( ) const ( - msgEnvironmentDeletedPreview = "Environment deleted" - msgEnvironmentNotFoundPreview = "Environment '%s' does not exist or has already been deleted." - msgDeletingEnvironmentPreview = "Deleting environment %s...\n" - msgDeletingResourceCountPreview = "Deleting %d resource(s) in environment %s...\n" + msgEnvironmentDeletedPreview = "Radius.Core/environments/%s deleted" + msgEnvironmentNotFoundPreview = "Radius.Core/environments/%s not found" ) // NewCommand creates an instance of the command and runner for the `rad env delete --preview` command. @@ -138,14 +136,10 @@ func (r *Runner) Run(ctx context.Context) error { return err } if !confirmed { - r.Output.LogInfo("Environment %q NOT deleted", r.EnvironmentName) return nil } } - // Show progress messages (without resource count for preview, since we don't enumerate here) - r.Output.LogInfo(msgDeletingEnvironmentPreview, r.EnvironmentName) - client := r.RadiusCoreClientFactory.NewEnvironmentsClient() _, err := client.Delete(ctx, r.EnvironmentName, &corerpv20250801.EnvironmentsClientDeleteOptions{}) if err != nil { @@ -154,7 +148,7 @@ func (r *Runner) Run(ctx context.Context) error { return err } - r.Output.LogInfo(msgEnvironmentDeletedPreview) + r.Output.LogInfo(msgEnvironmentDeletedPreview, r.EnvironmentName) return nil } diff --git a/pkg/cli/cmd/env/delete/preview/delete_test.go b/pkg/cli/cmd/env/delete/preview/delete_test.go index 320cb5a9ea..463075273a 100644 --- a/pkg/cli/cmd/env/delete/preview/delete_test.go +++ b/pkg/cli/cmd/env/delete/preview/delete_test.go @@ -98,12 +98,9 @@ func Test_Run(t *testing.T) { name: "Success: environment deleted", serverFactory: test_client_factory.WithEnvironmentServerNoError, expectedLogs: []any{ - output.LogOutput{ - Format: msgDeletingEnvironmentPreview, - Params: []any{"test-env"}, - }, output.LogOutput{ Format: msgEnvironmentDeletedPreview, + Params: []any{"test-env"}, }, }, }, diff --git a/pkg/cli/cmd/env/update/objectformats.go b/pkg/cli/cmd/env/update/objectformats.go deleted file mode 100644 index 5db2208194..0000000000 --- a/pkg/cli/cmd/env/update/objectformats.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package update - -import "github.com/radius-project/radius/pkg/cli/output" - -type environmentForDisplay struct { - Name string - ComputeKind string - Recipes int - Providers int -} - -// environmentFormat returns a FormatterOptions object containing the column headings and JSONPaths for the -// environment table. -func environmentFormat() output.FormatterOptions { - return output.FormatterOptions{ - Columns: []output.Column{ - { - Heading: "NAME", - JSONPath: "{ .Name }", - }, - { - Heading: "COMPUTE", - JSONPath: "{ .ComputeKind }", - }, - { - Heading: "RECIPES", - JSONPath: "{ .Recipes }", - }, - { - Heading: "PROVIDERS", - JSONPath: "{ .Providers }", - }, - }, - } -} diff --git a/pkg/cli/cmd/env/update/objectformats_test.go b/pkg/cli/cmd/env/update/objectformats_test.go deleted file mode 100644 index b8ffecc228..0000000000 --- a/pkg/cli/cmd/env/update/objectformats_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package update - -import ( - "bytes" - "testing" - - "github.com/radius-project/radius/pkg/cli/output" - "github.com/stretchr/testify/require" -) - -func Test_environmentFormat(t *testing.T) { - obj := environmentForDisplay{ - Name: "test_env_resource", - ComputeKind: "kubernetes", - Recipes: 3, - Providers: 2, - } - - buffer := &bytes.Buffer{} - err := output.Write(output.FormatTable, obj, buffer, environmentFormat()) - require.NoError(t, err) - - expected := "NAME COMPUTE RECIPES PROVIDERS\ntest_env_resource kubernetes 3 2\n" - require.Equal(t, expected, buffer.String()) -} diff --git a/pkg/cli/cmd/env/update/preview/objectformats.go b/pkg/cli/cmd/env/update/preview/objectformats.go deleted file mode 100644 index 4b250d7c69..0000000000 --- a/pkg/cli/cmd/env/update/preview/objectformats.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preview - -import "github.com/radius-project/radius/pkg/cli/output" - -type environmentForDisplay struct { - Name string - RecipePacks int - Providers int -} - -// environmentFormat returns a FormatterOptions object containing the column headings and JSONPaths for the -// environment table. -func environmentFormat() output.FormatterOptions { - return output.FormatterOptions{ - Columns: []output.Column{ - { - Heading: "NAME", - JSONPath: "{ .Name }", - }, - { - Heading: "RECIPE PACKS", - JSONPath: "{ .RecipePacks }", - }, - { - Heading: "PROVIDERS", - JSONPath: "{ .Providers }", - }, - }, - } -} diff --git a/pkg/cli/cmd/env/update/preview/update.go b/pkg/cli/cmd/env/update/preview/update.go index 4ce8221e2a..80b2e3d39e 100644 --- a/pkg/cli/cmd/env/update/preview/update.go +++ b/pkg/cli/cmd/env/update/preview/update.go @@ -356,40 +356,12 @@ func (r *Runner) Run(ctx context.Context) error { env.Properties.RecipePacks = newRecipePacks } - r.Output.LogInfo("Updating Environment...") _, err = envClient.CreateOrUpdate(ctx, r.EnvironmentName, env, &corerpv20250801.EnvironmentsClientCreateOrUpdateOptions{}) if err != nil { return clierrors.MessageWithCause(err, "Failed to update environment %q.", r.EnvironmentName) } - recipePackCount := 0 - if env.Properties.RecipePacks != nil { - recipePackCount = len(env.Properties.RecipePacks) - } - providerCount := 0 - if env.Properties.Providers != nil { - if env.Properties.Providers.Azure != nil { - providerCount++ - } - if env.Properties.Providers.Aws != nil { - providerCount++ - } - if env.Properties.Providers.Kubernetes != nil { - providerCount++ - } - } - obj := environmentForDisplay{ - Name: *env.Name, - RecipePacks: recipePackCount, - Providers: providerCount, - } - - err = r.Output.WriteFormatted("table", obj, environmentFormat()) - if err != nil { - return err - } - - r.Output.LogInfo("Successfully updated environment %q.", r.EnvironmentName) + r.Output.LogInfo("Radius.Core/environments/%s updated", r.EnvironmentName) return nil } diff --git a/pkg/cli/cmd/env/update/preview/update_test.go b/pkg/cli/cmd/env/update/preview/update_test.go index 5011a3153e..437ce1013a 100644 --- a/pkg/cli/cmd/env/update/preview/update_test.go +++ b/pkg/cli/cmd/env/update/preview/update_test.go @@ -306,19 +306,7 @@ func Test_Run(t *testing.T) { Format: "WARNING: The existing recipe pack list will be replaced with the specified packs.", }, output.LogOutput{ - Format: "Updating Environment...", - }, - output.FormattedOutput{ - Format: "table", - Obj: environmentForDisplay{ - Name: "test-env", - RecipePacks: 2, // rp1 and rp2 replace the old pack - Providers: 3, - }, - Options: environmentFormat(), - }, - output.LogOutput{ - Format: "Successfully updated environment %q.", + Format: "Radius.Core/environments/%s updated", Params: []any{"test-env"}, }, }, diff --git a/pkg/cli/cmd/env/update/update.go b/pkg/cli/cmd/env/update/update.go index 3187bee205..86259c79fb 100644 --- a/pkg/cli/cmd/env/update/update.go +++ b/pkg/cli/cmd/env/update/update.go @@ -228,8 +228,6 @@ func (r *Runner) Run(ctx context.Context) error { env.Properties.Providers.Aws = r.providers.Aws } - r.Output.LogInfo("Updating Environment...") - err = client.CreateOrUpdateEnvironment(ctx, r.EnvName, &corerp.EnvironmentResource{ Location: to.Ptr(v1.LocationGlobal), Properties: env.Properties, @@ -238,36 +236,7 @@ func (r *Runner) Run(ctx context.Context) error { return clierrors.MessageWithCause(err, "Failed to apply cloud provider scope to the environment %q.", r.EnvName) } - recipeCount := 0 - if env.Properties.Recipes != nil { - recipeCount = len(env.Properties.Recipes) - } - providerCount := 0 - if env.Properties.Providers != nil { - if env.Properties.Providers.Azure != nil { - providerCount++ - } - if env.Properties.Providers.Aws != nil { - providerCount++ - } - } - computeKind := "" - if env.Properties.Compute != nil { - computeKind = *env.Properties.Compute.GetEnvironmentCompute().Kind - } - obj := environmentForDisplay{ - Name: *env.Name, - ComputeKind: computeKind, - Recipes: recipeCount, - Providers: providerCount, - } - - err = r.Output.WriteFormatted("table", obj, environmentFormat()) - if err != nil { - return err - } - - r.Output.LogInfo("Successfully updated environment %q.", r.EnvName) + r.Output.LogInfo("Applications.Core/environments/%s updated", r.EnvName) return nil } diff --git a/pkg/cli/cmd/env/update/update_test.go b/pkg/cli/cmd/env/update/update_test.go index 44f7b7fb40..7f6a5a97fe 100644 --- a/pkg/cli/cmd/env/update/update_test.go +++ b/pkg/cli/cmd/env/update/update_test.go @@ -341,24 +341,11 @@ func Test_Update(t *testing.T) { require.NoError(t, err) environment.Properties.Providers = testProviders - obj := environmentForDisplay{ - Name: "test-env", - Recipes: 0, - Providers: 2, - ComputeKind: "kubernetes", - } + _ = environment expected := []any{ output.LogOutput{ - Format: "Updating Environment...", - }, - output.FormattedOutput{ - Format: "table", - Obj: obj, - Options: environmentFormat(), - }, - output.LogOutput{ - Format: "Successfully updated environment %q.", + Format: "Applications.Core/environments/%s updated", Params: []any{"test-env"}, }, } @@ -510,34 +497,9 @@ func Test_Update(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) - numberOfProviders := func() int { - numberOfProviders := 0 - if tc.expectedProviders.Azure != nil { - numberOfProviders++ - } - if tc.expectedProviders.Aws != nil { - numberOfProviders++ - } - return numberOfProviders - } - - obj := environmentForDisplay{ - Name: "test-env", - Recipes: 0, - Providers: numberOfProviders(), - } - expected := []any{ output.LogOutput{ - Format: "Updating Environment...", - }, - output.FormattedOutput{ - Format: "table", - Obj: obj, - Options: environmentFormat(), - }, - output.LogOutput{ - Format: "Successfully updated environment %q.", + Format: "Applications.Core/environments/%s updated", Params: []any{"test-env"}, }, } diff --git a/pkg/cli/cmd/group/create/create.go b/pkg/cli/cmd/group/create/create.go index cdcca08f1d..35f3218a4b 100644 --- a/pkg/cli/cmd/group/create/create.go +++ b/pkg/cli/cmd/group/create/create.go @@ -119,8 +119,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } - r.Output.LogInfo("creating resource group %q in workspace %q...\n", r.UCPResourceGroupName, r.Workspace.Name) - err = client.CreateOrUpdateResourceGroup(ctx, "local", r.UCPResourceGroupName, &v20231001preview.ResourceGroupResource{ Location: to.Ptr(v1.LocationGlobal), }) @@ -128,7 +126,7 @@ func (r *Runner) Run(ctx context.Context) error { return err } - r.Output.LogInfo("resource group %q created", r.UCPResourceGroupName) + r.Output.LogInfo("System.Resources/resourceGroups/%s created", r.UCPResourceGroupName) return nil } diff --git a/pkg/cli/cmd/group/create/create_test.go b/pkg/cli/cmd/group/create/create_test.go index df31fbca74..1a3514162c 100644 --- a/pkg/cli/cmd/group/create/create_test.go +++ b/pkg/cli/cmd/group/create/create_test.go @@ -116,11 +116,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "creating resource group %q in workspace %q...\n", - Params: []any{"testrg", "kind-kind"}, - }, - output.LogOutput{ - Format: "resource group %q created", + Format: "System.Resources/resourceGroups/%s created", Params: []any{"testrg"}, }, } diff --git a/pkg/cli/cmd/group/delete/delete.go b/pkg/cli/cmd/group/delete/delete.go index 005b628822..724b2f0741 100644 --- a/pkg/cli/cmd/group/delete/delete.go +++ b/pkg/cli/cmd/group/delete/delete.go @@ -36,11 +36,8 @@ const ( scopeLocal = "local" // Message templates - msgResourceGroupDeleted = "Resource group %s deleted." - msgResourceGroupNotFound = "Resource group %s does not exist or has already been deleted." - msgResourceGroupNotDeleted = "Resource group %q NOT deleted" - msgDeletingResourceGroup = "Deleting resource group %s...\n" - msgDeletingResourcesWithCount = "Deleting %d resource(s) in group %s..." + msgResourceGroupDeleted = "System.Resources/resourceGroups/%s deleted" + msgResourceGroupNotFound = "System.Resources/resourceGroups/%s not found" ) // NewCommand creates an instance of the command and runner for the `rad group delete` command. @@ -153,17 +150,10 @@ func (r *Runner) Run(ctx context.Context) error { } if !confirmed { - r.Output.LogInfo(msgResourceGroupNotDeleted, r.UCPResourceGroupName) return nil } } - // Show appropriate progress messages with resource count when available - if hasResources { - r.Output.LogInfo(msgDeletingResourcesWithCount, len(resources), r.UCPResourceGroupName) - } - r.Output.LogInfo(msgDeletingResourceGroup, r.UCPResourceGroupName) - // Actually delete the resource group (which will now handle resource deletion internally) deleted, err := client.DeleteResourceGroup(ctx, scopeLocal, r.UCPResourceGroupName) if err != nil { diff --git a/pkg/cli/cmd/group/delete/delete_test.go b/pkg/cli/cmd/group/delete/delete_test.go index 298630166a..891e17963c 100644 --- a/pkg/cli/cmd/group/delete/delete_test.go +++ b/pkg/cli/cmd/group/delete/delete_test.go @@ -97,11 +97,7 @@ func Test_Run(t *testing.T) { skipPrompt: true, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s deleted.", + Format: "System.Resources/resourceGroups/%s deleted", Params: []any{"testrg"}, }, }, @@ -117,15 +113,7 @@ func Test_Run(t *testing.T) { skipPrompt: true, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting %d resource(s) in group %s...", - Params: []any{2, "testrg"}, - }, - output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s deleted.", + Format: "System.Resources/resourceGroups/%s deleted", Params: []any{"testrg"}, }, }, @@ -138,11 +126,7 @@ func Test_Run(t *testing.T) { skipPrompt: true, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s does not exist or has already been deleted.", + Format: "System.Resources/resourceGroups/%s not found", Params: []any{"testrg"}, }, }, @@ -156,28 +140,19 @@ func Test_Run(t *testing.T) { deleteResult: true, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s deleted.", + Format: "System.Resources/resourceGroups/%s deleted", Params: []any{"testrg"}, }, }, }, { - name: "Empty group - user cancels deletion", - confirmation: false, - resources: []generated.GenericResource{}, - promptResponse: prompt.ConfirmNo, - expectedPrompt: "The resource group testrg is empty. Are you sure you want to delete the resource group?", - deleteResult: false, // Won't be called - expectedOutputs: []any{ - output.LogOutput{ - Format: "Resource group %q NOT deleted", - Params: []any{"testrg"}, - }, - }, + name: "Empty group - user cancels deletion", + confirmation: false, + resources: []generated.GenericResource{}, + promptResponse: prompt.ConfirmNo, + expectedPrompt: "The resource group testrg is empty. Are you sure you want to delete the resource group?", + deleteResult: false, // Won't be called + expectedOutputs: nil, }, { name: "Group with resources - user confirms deletion", @@ -191,15 +166,7 @@ func Test_Run(t *testing.T) { deleteResult: true, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting %d resource(s) in group %s...", - Params: []any{2, "testrg"}, - }, - output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s deleted.", + Format: "System.Resources/resourceGroups/%s deleted", Params: []any{"testrg"}, }, }, @@ -210,15 +177,10 @@ func Test_Run(t *testing.T) { resources: []generated.GenericResource{ {Name: new("resource1"), Type: new("Applications.Core/containers")}, }, - promptResponse: prompt.ConfirmNo, - expectedPrompt: "The resource group testrg contains deployed resources. Are you sure you want to delete the resource group and its resources?", - deleteResult: false, // Won't be called - expectedOutputs: []any{ - output.LogOutput{ - Format: "Resource group %q NOT deleted", - Params: []any{"testrg"}, - }, - }, + promptResponse: prompt.ConfirmNo, + expectedPrompt: "The resource group testrg contains deployed resources. Are you sure you want to delete the resource group and its resources?", + deleteResult: false, // Won't be called + expectedOutputs: nil, }, { name: "List resources fails - should not proceed", @@ -238,18 +200,13 @@ func Test_Run(t *testing.T) { expectedOutputs: nil, // No output expected }, { - name: "Delete operation fails", - confirmation: true, - resources: []generated.GenericResource{}, - deleteError: fmt.Errorf("deletion failed"), - skipPrompt: true, - expectedError: fmt.Errorf("deletion failed"), - expectedOutputs: []any{ - output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - }, + name: "Delete operation fails", + confirmation: true, + resources: []generated.GenericResource{}, + deleteError: fmt.Errorf("deletion failed"), + skipPrompt: true, + expectedError: fmt.Errorf("deletion failed"), + expectedOutputs: nil, }, { name: "List returns 404 - group doesn't exist", @@ -260,11 +217,7 @@ func Test_Run(t *testing.T) { deleteResult: false, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s does not exist or has already been deleted.", + Format: "System.Resources/resourceGroups/%s not found", Params: []any{"testrg"}, }, }, @@ -277,11 +230,7 @@ func Test_Run(t *testing.T) { deleteResult: false, expectedOutputs: []any{ output.LogOutput{ - Format: "Deleting resource group %s...\n", - Params: []any{"testrg"}, - }, - output.LogOutput{ - Format: "Resource group %s does not exist or has already been deleted.", + Format: "System.Resources/resourceGroups/%s not found", Params: []any{"testrg"}, }, }, diff --git a/pkg/cli/cmd/recipepack/delete/delete.go b/pkg/cli/cmd/recipepack/delete/delete.go index 7ee4b75937..5d2f7c9c95 100644 --- a/pkg/cli/cmd/recipepack/delete/delete.go +++ b/pkg/cli/cmd/recipepack/delete/delete.go @@ -38,11 +38,9 @@ import ( ) const ( - deleteConfirmationMsg = "Are you sure you want to delete recipe pack '%s'?" - msgDeletingRecipePack = "Deleting recipe pack %s...\n" - msgRecipePackDeleted = "Recipe pack %s deleted." - msgRecipePackNotFound = "Recipe pack %s does not exist or has already been deleted." - msgRecipePackNotDeleted = "Recipe pack %q NOT deleted" + deleteConfirmationMsg = "Are you sure you want to delete recipe pack '%s'?" + msgRecipePackDeleted = "Radius.Core/recipePacks/%s deleted" + msgRecipePackNotFound = "Radius.Core/recipePacks/%s not found" ) // NewCommand creates a new Cobra command for deleting a recipe pack. @@ -139,13 +137,10 @@ func (r *Runner) Run(ctx context.Context) error { } if !confirmed { - r.Output.LogInfo(msgRecipePackNotDeleted, r.RecipePackName) return nil } } - r.Output.LogInfo(msgDeletingRecipePack, r.RecipePackName) - recipePack, err := client.GetRecipePack(ctx, r.RecipePackName) if clients.Is404Error(err) { return clierrors.Message("The recipe pack %q was not found or has been deleted.", r.RecipePackName) diff --git a/pkg/cli/cmd/recipepack/delete/delete_test.go b/pkg/cli/cmd/recipepack/delete/delete_test.go index 109d73d1fb..73edfe94ba 100644 --- a/pkg/cli/cmd/recipepack/delete/delete_test.go +++ b/pkg/cli/cmd/recipepack/delete/delete_test.go @@ -163,7 +163,6 @@ func Test_Run(t *testing.T) { require.Empty(t, capturedEnv.Properties.RecipePacks) require.Equal(t, []any{ - output.LogOutput{Format: msgDeletingRecipePack, Params: []any{packName}}, output.LogOutput{Format: msgRecipePackDeleted, Params: []any{packName}}, }, outputSink.Writes) }) @@ -199,7 +198,6 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) require.Equal(t, []any{ - output.LogOutput{Format: msgDeletingRecipePack, Params: []any{packName}}, output.LogOutput{Format: msgRecipePackDeleted, Params: []any{packName}}, }, outputSink.Writes) }) @@ -249,7 +247,6 @@ func Test_Run(t *testing.T) { err = runner.Run(context.Background()) require.NoError(t, err) require.Equal(t, []any{ - output.LogOutput{Format: msgDeletingRecipePack, Params: []any{packName}}, output.LogOutput{Format: msgRecipePackDeleted, Params: []any{packName}}, }, outputSink.Writes) }) @@ -277,9 +274,7 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) - require.Equal(t, []any{ - output.LogOutput{Format: msgRecipePackNotDeleted, Params: []any{packName}}, - }, outputSink.Writes) + require.Empty(t, outputSink.Writes) }) t.Run("get recipe pack returns 404 — returns error", func(t *testing.T) { @@ -461,7 +456,6 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) require.Equal(t, []any{ - output.LogOutput{Format: msgDeletingRecipePack, Params: []any{packName}}, output.LogOutput{Format: msgRecipePackNotFound, Params: []any{packName}}, }, outputSink.Writes) }) diff --git a/pkg/cli/cmd/resource/create/create.go b/pkg/cli/cmd/resource/create/create.go index 004bacf88c..23db0377da 100644 --- a/pkg/cli/cmd/resource/create/create.go +++ b/pkg/cli/cmd/resource/create/create.go @@ -99,6 +99,7 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } r.Format = format + resourceProviderName, resourceTypeName, err := cli.RequireFullyQualifiedResourceType(args) if err != nil { return err @@ -144,10 +145,12 @@ func (r *Runner) Run(ctx context.Context) error { return err } - err = r.Output.WriteFormatted(r.Format, response, common.GetResourceProviderTableFormat()) - if err != nil { - return err + // For non-default output formats (json), emit the full resource representation + // so callers can script against it. For the default/table format, emit a concise line. + if r.Format == output.FormatJson { + return r.Output.WriteFormatted(r.Format, response, common.GetResourceProviderTableFormat()) } + r.Output.LogInfo("%s/%s created", r.FullyQualifiedResourceTypeName, r.ResourceName) return nil } diff --git a/pkg/cli/cmd/resource/create/create_test.go b/pkg/cli/cmd/resource/create/create_test.go index 1beab31ebb..e4a43048fc 100644 --- a/pkg/cli/cmd/resource/create/create_test.go +++ b/pkg/cli/cmd/resource/create/create_test.go @@ -116,5 +116,11 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) + require.Equal(t, []any{ + output.LogOutput{ + Format: "%s/%s created", + Params: []any{"Applications.Test/exampleResources", "my-example"}, + }, + }, outputSink.Writes) }) } diff --git a/pkg/cli/cmd/resource/delete/delete.go b/pkg/cli/cmd/resource/delete/delete.go index c4e764f39d..8f8797153f 100644 --- a/pkg/cli/cmd/resource/delete/delete.go +++ b/pkg/cli/cmd/resource/delete/delete.go @@ -156,7 +156,7 @@ func (r *Runner) Run(ctx context.Context) error { environmentID, applicationID, err := r.extractEnvironmentAndApplicationIDs(ctx, client) if clients.Is404Error(err) { - r.Output.LogInfo("Resource '%s' of type '%s' does not exist or has already been deleted", r.ResourceName, r.FullyQualifiedResourceTypeName) + r.Output.LogInfo("%s/%s not found", r.FullyQualifiedResourceTypeName, r.ResourceName) return nil } else if err != nil { return err @@ -182,7 +182,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } if !confirmed { - r.Output.LogInfo("resource %q of type %q NOT deleted", r.ResourceName, r.FullyQualifiedResourceTypeName) return nil } } @@ -193,9 +192,9 @@ func (r *Runner) Run(ctx context.Context) error { } if deleted { - r.Output.LogInfo("Resource deleted") + r.Output.LogInfo("%s/%s deleted", r.FullyQualifiedResourceTypeName, r.ResourceName) } else { - r.Output.LogInfo("Resource '%s' of type '%s' does not exist or has already been deleted", r.ResourceName, r.FullyQualifiedResourceTypeName) + r.Output.LogInfo("%s/%s not found", r.FullyQualifiedResourceTypeName, r.ResourceName) } return nil diff --git a/pkg/cli/cmd/resource/delete/delete_test.go b/pkg/cli/cmd/resource/delete/delete_test.go index af03bedc6e..fd597677bc 100644 --- a/pkg/cli/cmd/resource/delete/delete_test.go +++ b/pkg/cli/cmd/resource/delete/delete_test.go @@ -18,6 +18,7 @@ package delete import ( "context" + "fmt" "net/http" "net/url" "testing" @@ -28,7 +29,10 @@ import ( "github.com/radius-project/radius/pkg/cli/connections" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/prompt" "github.com/radius-project/radius/pkg/cli/workspaces" + v20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/radcli" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -132,376 +136,374 @@ func Test_Run(t *testing.T) { require.Error(t, err) require.Equal(t, responseError, err) }) - /* - - t.Run("Success (non-existent)", func(t *testing.T) { - ctrl := gomock.NewController(t) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", - "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", - }, - }, nil). - Times(1) - - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(false, nil). - Times(1) - - outputSink := &output.MockOutput{} - - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: &workspaces.Workspace{}, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - Confirm: true, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource '%s' of type '%s' does not exist or has already been deleted", - Params: []any{"test-container", "Applications.Core/containers"}, - }, - } - require.Equal(t, expected, outputSink.Writes) - }) - - t.Run("Success (deleted)", func(t *testing.T) { - ctrl := gomock.NewController(t) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", - "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", - }, - }, nil). - Times(1) - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(true, nil). - Times(1) - - outputSink := &output.MockOutput{} - - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: &workspaces.Workspace{}, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - Confirm: true, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource deleted", - }, - } - require.Equal(t, expected, outputSink.Writes) - }) - - t.Run("Success: Prompt Confirmed (case 1: application-scoped standard resource)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - promptMock := prompt.NewMockInterface(ctrl) - promptMock.EXPECT(). - GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). - Return(prompt.ConfirmYes, nil). - Times(1) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", - "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", - }, - }, nil). - Times(1) - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(true, 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", - } - outputSink := &output.MockOutput{} - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: workspace, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - InputPrompter: promptMock, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource deleted", - }, - } - - require.Equal(t, expected, outputSink.Writes) - }) - - t.Run("Success: Prompt Confirmed (case 2: environment-scoped standard resource)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - promptMock := prompt.NewMockInterface(ctrl) - promptMock.EXPECT(). - GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithoutApplication, "test-container", "Applications.Core/containers", "my-test-env")). - Return(prompt.ConfirmYes, nil). - Times(1) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", - }, - }, nil). - Times(1) - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(true, 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", - } - - outputSink := &output.MockOutput{} - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: workspace, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - InputPrompter: promptMock, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource deleted", + + t.Run("Success (non-existent)", func(t *testing.T) { + ctrl := gomock.NewController(t) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", + "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", }, - } - - require.Equal(t, expected, outputSink.Writes) - }) - - // NOTE: this case requires an extra lookup to get the environment name. - t.Run("Success: Prompt Confirmed (case 3: application-scoped core resource)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - promptMock := prompt.NewMockInterface(ctrl) - promptMock.EXPECT(). - GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). - Return(prompt.ConfirmYes, nil). - Times(1) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", - }, - }, nil). - Times(1) - - appManagementClient.EXPECT(). - GetApplication(gomock.Any(), "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app"). - Return(v20231001preview.ApplicationResource{ - Properties: &v20231001preview.ApplicationProperties{ - Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env"), - }, - }, nil). - Times(1) - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(true, nil). - Times(1) - - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", + }, nil). + Times(1) + + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(false, nil). + Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + Confirm: true, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s not found", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success (deleted)", func(t *testing.T) { + ctrl := gomock.NewController(t) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", + "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", }, - Name: "kind-kind", - Scope: "/planes/radius/local/resourceGroups/test-group", - } - - outputSink := &output.MockOutput{} - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: workspace, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - InputPrompter: promptMock, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource deleted", + }, nil). + Times(1) + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(true, nil). + Times(1) + + outputSink := &output.MockOutput{} + + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + Confirm: true, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Confirmed (case 1: application-scoped standard resource)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). + Return(prompt.ConfirmYes, nil). + Times(1) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", + "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", }, - } - - require.Equal(t, expected, outputSink.Writes) - }) - - t.Run("Success: Prompt Confirmed (case 4: no application or environment)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - promptMock := prompt.NewMockInterface(ctrl) - promptMock.EXPECT(). - GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithoutApplicationOrEnvironment, "test-container", "Applications.Core/containers")). - Return(prompt.ConfirmYes, nil). - Times(1) - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{}, - }, nil). - Times(1) - - appManagementClient.EXPECT(). - DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(true, nil). - Times(1) - - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", + }, nil). + Times(1) + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(true, 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", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: workspace, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + InputPrompter: promptMock, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Confirmed (case 2: environment-scoped standard resource)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithoutApplication, "test-container", "Applications.Core/containers", "my-test-env")). + Return(prompt.ConfirmYes, nil). + Times(1) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", }, - Name: "kind-kind", - Scope: "/planes/radius/local/resourceGroups/test-group", - } - outputSink := &output.MockOutput{} - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - Output: outputSink, - Workspace: workspace, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - Format: "table", - InputPrompter: promptMock, - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "Resource deleted", + }, nil). + Times(1) + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(true, 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", + } + + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: workspace, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + InputPrompter: promptMock, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + // NOTE: this case requires an extra lookup to get the environment name. + t.Run("Success: Prompt Confirmed (case 3: application-scoped core resource)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). + Return(prompt.ConfirmYes, nil). + Times(1) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", }, - } - - require.Equal(t, expected, outputSink.Writes) - }) - - t.Run("Success: Prompt Cancelled", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) - appManagementClient.EXPECT(). - GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). - Return(generated.GenericResource{ - Properties: map[string]interface{}{ - "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", - "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", - }, - }, nil). - Times(1) - - promptMock := prompt.NewMockInterface(ctrl) - promptMock.EXPECT(). - GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). - Return(prompt.ConfirmNo, nil). - Times(1) - - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", + }, nil). + Times(1) + + appManagementClient.EXPECT(). + GetApplication(gomock.Any(), "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app"). + Return(v20231001preview.ApplicationResource{ + Properties: &v20231001preview.ApplicationProperties{ + Environment: to.Ptr("/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env"), }, - Name: "kind-kind", - Scope: "/planes/radius/local/resourceGroups/test-group", - } - - outputSink := &output.MockOutput{} - runner := &Runner{ - ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, - InputPrompter: promptMock, - Workspace: workspace, - Format: "table", - Output: outputSink, - FullyQualifiedResourceTypeName: "Applications.Core/containers", - ResourceName: "test-container", - } - - err := runner.Run(context.Background()) - require.NoError(t, err) - - expected := []any{ - output.LogOutput{ - Format: "resource %q of type %q NOT deleted", - Params: []any{"test-container", "Applications.Core/containers"}, + }, nil). + Times(1) + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(true, 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", + } + + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: workspace, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + InputPrompter: promptMock, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Confirmed (case 4: no application or environment)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithoutApplicationOrEnvironment, "test-container", "Applications.Core/containers")). + Return(prompt.ConfirmYes, nil). + Times(1) + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{}, + }, nil). + Times(1) + + appManagementClient.EXPECT(). + DeleteResource(gomock.Any(), "Applications.Core/containers", "test-container", false). + Return(true, 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", + } + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + Output: outputSink, + Workspace: workspace, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + Format: "table", + InputPrompter: promptMock, + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + expected := []any{ + output.LogOutput{ + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, + }, + } + + require.Equal(t, expected, outputSink.Writes) + }) + + t.Run("Success: Prompt Cancelled", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + appManagementClient := clients.NewMockApplicationsManagementClient(ctrl) + appManagementClient.EXPECT(). + GetResource(gomock.Any(), "Applications.Core/containers", "test-container"). + Return(generated.GenericResource{ + Properties: map[string]interface{}{ + "environment": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/environments/my-test-env", + "application": "/planes/radius/local/resourceGroups/test-group/providers/Applications.Core/applications/my-test-app", }, - } - require.Equal(t, expected, outputSink.Writes) - })*/ + }, nil). + Times(1) + + promptMock := prompt.NewMockInterface(ctrl) + promptMock.EXPECT(). + GetListInput([]string{prompt.ConfirmNo, prompt.ConfirmYes}, fmt.Sprintf(deleteConfirmationWithApplication, "test-container", "Applications.Core/containers", "my-test-app", "my-test-env")). + Return(prompt.ConfirmNo, 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", + } + + outputSink := &output.MockOutput{} + runner := &Runner{ + ConnectionFactory: &connections.MockFactory{ApplicationsManagementClient: appManagementClient}, + InputPrompter: promptMock, + Workspace: workspace, + Format: "table", + Output: outputSink, + FullyQualifiedResourceTypeName: "Applications.Core/containers", + ResourceName: "test-container", + } + + err := runner.Run(context.Background()) + require.NoError(t, err) + + require.Empty(t, outputSink.Writes) + }) t.Run("Success (force deleted)", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -542,11 +544,12 @@ func Test_Run(t *testing.T) { Format: "WARNING: Force deleting a resource in a non-terminal state may leave orphaned external resources that require manual cleanup.", }, output.LogOutput{ - Format: "Resource deleted", + Format: "%s/%s deleted", + Params: []any{"Applications.Core/containers", "test-container"}, }, } require.Equal(t, expected, outputSink.Writes) }) }) -} \ No newline at end of file +} diff --git a/pkg/cli/cmd/resourceprovider/create/create.go b/pkg/cli/cmd/resourceprovider/create/create.go index befc928e8d..e6692d0b87 100644 --- a/pkg/cli/cmd/resourceprovider/create/create.go +++ b/pkg/cli/cmd/resourceprovider/create/create.go @@ -22,7 +22,6 @@ import ( aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/cmd/commonflags" - "github.com/radius-project/radius/pkg/cli/cmd/resourceprovider/common" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/manifest" "github.com/radius-project/radius/pkg/cli/output" @@ -77,18 +76,13 @@ type Runner struct { ResourceProviderManifestFilePath string ResourceProvider *manifest.ResourceProvider - Logger func(format string, args ...any) } // NewRunner creates an instance of the runner for the `rad resource-provider create` command. func NewRunner(factory framework.Factory) *Runner { - output := factory.GetOutput() return &Runner{ ConfigHolder: factory.GetConfigHolder(), - Output: output, - Logger: func(format string, args ...any) { - output.LogInfo(format, args...) - }, + Output: factory.GetOutput(), } } @@ -126,23 +120,13 @@ func (r *Runner) Run(ctx context.Context) error { } } - // Proceed with registering manifests - if err := manifest.RegisterFile(ctx, r.UCPClientFactory, "local", r.ResourceProviderManifestFilePath, r.Logger); err != nil { - return err - } - - response, err := r.UCPClientFactory.NewResourceProvidersClient().Get(ctx, "local", r.ResourceProvider.Namespace, nil) - if err != nil { - return err - } - - r.Output.LogInfo("") - - err = r.Output.WriteFormatted(r.Format, response, common.GetResourceProviderTableFormat()) - if err != nil { + // Proceed with registering manifests. Use a nil logger to suppress verbose + // progress messages; the concise success line is emitted below. + if err := manifest.RegisterFile(ctx, r.UCPClientFactory, "local", r.ResourceProviderManifestFilePath, nil); err != nil { return err } + r.Output.LogInfo("System.Resources/resourceProviders/%s created", r.ResourceProvider.Namespace) return nil } diff --git a/pkg/cli/cmd/resourceprovider/create/create_test.go b/pkg/cli/cmd/resourceprovider/create/create_test.go index 35db8b574f..8f394f8230 100644 --- a/pkg/cli/cmd/resourceprovider/create/create_test.go +++ b/pkg/cli/cmd/resourceprovider/create/create_test.go @@ -17,9 +17,7 @@ limitations under the License. package create import ( - "bytes" "context" - "fmt" "testing" "github.com/radius-project/radius/pkg/cli/framework" @@ -73,32 +71,50 @@ func Test_Run(t *testing.T) { resourceProviderData, err := manifest.ReadFile("testdata/valid.yaml") require.NoError(t, err) - expectedResourceType := "testResources" - expectedAPIVersion := "2025-01-01-preview" - clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerNoError) require.NoError(t, err) - var logBuffer bytes.Buffer - logger := func(format string, args ...any) { - fmt.Fprintf(&logBuffer, format+"\n", args...) - } + outputSink := &output.MockOutput{} runner := &Runner{ UCPClientFactory: clientFactory, - Output: &output.MockOutput{}, + Output: outputSink, Workspace: &workspaces.Workspace{}, ResourceProvider: resourceProviderData, Format: "table", - Logger: logger, ResourceProviderManifestFilePath: "testdata/valid.yaml", } err = runner.Run(context.Background()) require.NoError(t, err) - logOutput := logBuffer.String() - require.Contains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", resourceProviderData.Namespace, expectedResourceType)) - require.Contains(t, logOutput, fmt.Sprintf("Creating API Version %s/%s@%s", resourceProviderData.Namespace, expectedResourceType, expectedAPIVersion)) + require.Equal(t, []any{ + output.LogOutput{ + Format: "System.Resources/resourceProviders/%s created", + Params: []any{resourceProviderData.Namespace}, + }, + }, outputSink.Writes) + }) + + t.Run("Failure: RegisterFile returns error", func(t *testing.T) { + resourceProviderData, err := manifest.ReadFile("testdata/valid.yaml") + require.NoError(t, err) + + clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerInternalError) + require.NoError(t, err) + + outputSink := &output.MockOutput{} + runner := &Runner{ + UCPClientFactory: clientFactory, + Output: outputSink, + Workspace: &workspaces.Workspace{}, + ResourceProvider: resourceProviderData, + Format: "table", + ResourceProviderManifestFilePath: "testdata/valid.yaml", + } + + err = runner.Run(context.Background()) + require.Error(t, err) + require.Empty(t, outputSink.Writes) }) } diff --git a/pkg/cli/cmd/resourceprovider/delete/delete.go b/pkg/cli/cmd/resourceprovider/delete/delete.go index 248f5b7e65..1aefb01fcc 100644 --- a/pkg/cli/cmd/resourceprovider/delete/delete.go +++ b/pkg/cli/cmd/resourceprovider/delete/delete.go @@ -140,9 +140,9 @@ func (r *Runner) Run(ctx context.Context) error { } if deleted { - r.Output.LogInfo("Resource provider %q deleted.", r.ResourceProviderNamespace) + r.Output.LogInfo("System.Resources/resourceProviders/%s deleted", r.ResourceProviderNamespace) } else { - r.Output.LogInfo("Resource provider %q does not exist or has already been deleted.", r.ResourceProviderNamespace) + r.Output.LogInfo("System.Resources/resourceProviders/%s not found", r.ResourceProviderNamespace) } return nil diff --git a/pkg/cli/cmd/resourceprovider/delete/delete_test.go b/pkg/cli/cmd/resourceprovider/delete/delete_test.go index d427343db9..cec586691d 100644 --- a/pkg/cli/cmd/resourceprovider/delete/delete_test.go +++ b/pkg/cli/cmd/resourceprovider/delete/delete_test.go @@ -95,7 +95,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource provider %q deleted.", + Format: "System.Resources/resourceProviders/%s deleted", Params: []any{"Applications.Test"}, }, } @@ -136,7 +136,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource provider %q does not exist or has already been deleted.", + Format: "System.Resources/resourceProviders/%s not found", Params: []any{"Applications.Test"}, }, } @@ -183,7 +183,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource provider %q deleted.", + Format: "System.Resources/resourceProviders/%s deleted", Params: []any{"Applications.Test"}, }, } diff --git a/pkg/cli/cmd/resourcetype/create/create.go b/pkg/cli/cmd/resourcetype/create/create.go index da631f0052..635844db7c 100644 --- a/pkg/cli/cmd/resourcetype/create/create.go +++ b/pkg/cli/cmd/resourcetype/create/create.go @@ -32,9 +32,7 @@ import ( ) const ( - defaultPlaneName = "local" - msgNoResourceTypeNameProvided = "No resource type name provided. Creating all resource types in the manifest." - msgAllResourceTypesCreated = "All resource types in the manifest created successfully" + defaultPlaneName = "local" ) // NewCommand creates an instance of the `rad resource-type create` command and runner. @@ -89,7 +87,6 @@ type Runner struct { ResourceProviderManifestFilePath string ResourceProvider *manifest.ResourceProvider ResourceTypeName string - Logger func(format string, args ...any) } // NewRunner creates an instance of the runner for the `rad resource-type create` command. @@ -97,9 +94,6 @@ func NewRunner(factory framework.Factory) *Runner { return &Runner{ ConfigHolder: factory.GetConfigHolder(), Output: factory.GetOutput(), - Logger: func(format string, args ...any) { - output.LogInfo(format, args...) - }, } } @@ -149,7 +143,6 @@ func (r *Runner) Run(ctx context.Context) error { } if r.ResourceTypeName == "" { - r.Output.LogInfo(msgNoResourceTypeNameProvided) return r.registerTypes(ctx, nil) // Register all types } @@ -158,8 +151,9 @@ func (r *Runner) Run(ctx context.Context) error { // registerTypes registers the specified resource types (or all types if typeNames is nil) func (r *Runner) registerTypes(ctx context.Context, typeNames []string) error { - // Always ensure the resource provider exists first - err := manifest.EnsureResourceProviderExists(ctx, r.UCPClientFactory, defaultPlaneName, *r.ResourceProvider, r.Logger) + // Always ensure the resource provider exists first. Use a nil logger to suppress + // verbose progress messages; the concise success line is emitted below. + err := manifest.EnsureResourceProviderExists(ctx, r.UCPClientFactory, defaultPlaneName, *r.ResourceProvider, nil) if err != nil { return err } @@ -176,19 +170,13 @@ func (r *Runner) registerTypes(ctx context.Context, typeNames []string) error { } } - // Register each type individually using the unified approach + // Register each type individually and emit a single concise line per type. for _, typeName := range typesToRegister { - err = manifest.RegisterType(ctx, r.UCPClientFactory, defaultPlaneName, r.ResourceProviderManifestFilePath, typeName, r.Logger) + err = manifest.RegisterType(ctx, r.UCPClientFactory, defaultPlaneName, r.ResourceProviderManifestFilePath, typeName, nil) if err != nil { return err } - } - - // Provide appropriate success message - if len(typesToRegister) == 1 { - // Single type - success message already logged by RegisterType - } else { - r.Output.LogInfo(msgAllResourceTypesCreated) + r.Output.LogInfo("%s/%s created", r.ResourceProvider.Namespace, typeName) } return nil diff --git a/pkg/cli/cmd/resourcetype/create/create_test.go b/pkg/cli/cmd/resourcetype/create/create_test.go index c56fb66547..f4aafb5495 100644 --- a/pkg/cli/cmd/resourcetype/create/create_test.go +++ b/pkg/cli/cmd/resourcetype/create/create_test.go @@ -17,9 +17,7 @@ limitations under the License. package create import ( - "bytes" "context" - "fmt" "testing" "github.com/radius-project/radius/pkg/cli/framework" @@ -73,11 +71,6 @@ func Test_Run(t *testing.T) { clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerNoError) require.NoError(t, err) - var logBuffer bytes.Buffer - logger := func(format string, args ...any) { - fmt.Fprintf(&logBuffer, format+"\n", args...) - } - outputSink := &output.MockOutput{} runner := &Runner{ UCPClientFactory: clientFactory, @@ -85,7 +78,6 @@ func Test_Run(t *testing.T) { Workspace: &workspaces.Workspace{}, ResourceProvider: resourceProviderData, Format: "table", - Logger: logger, ResourceProviderManifestFilePath: "testdata/valid.yaml", ResourceTypeName: "testResources", } @@ -93,9 +85,10 @@ func Test_Run(t *testing.T) { err = runner.Run(context.Background()) require.NoError(t, err) - // Verify RegisterType was called (should see specific log messages) - logOutput := logBuffer.String() - require.Contains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", runner.ResourceProvider.Namespace, "testResources")) + require.Contains(t, outputSink.Writes, output.LogOutput{ + Format: "%s/%s created", + Params: []any{runner.ResourceProvider.Namespace, "testResources"}, + }) }) t.Run("No resource type name provided - registers entire manifest", func(t *testing.T) { @@ -108,11 +101,6 @@ func Test_Run(t *testing.T) { clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerNoError) require.NoError(t, err) - var logBuffer bytes.Buffer - logger := func(format string, args ...any) { - fmt.Fprintf(&logBuffer, format+"\n", args...) - } - outputSink := &output.MockOutput{} runner := &Runner{ UCPClientFactory: clientFactory, @@ -120,7 +108,6 @@ func Test_Run(t *testing.T) { Workspace: &workspaces.Workspace{}, ResourceProvider: resourceProviderData, Format: "table", - Logger: logger, ResourceProviderManifestFilePath: "testdata/valid.yaml", ResourceTypeName: "", // Empty resource type name } @@ -128,17 +115,15 @@ func Test_Run(t *testing.T) { err = runner.Run(context.Background()) require.NoError(t, err) - // Verify the correct log message is output - expectedLog := output.LogOutput{ - Format: "No resource type name provided. Creating all resource types in the manifest.", - Params: nil, - } - require.Contains(t, outputSink.Writes, expectedLog, "Expected log message for no resource type name provided") - - // Verify RegisterResourceProvider was called - logOutput := logBuffer.String() - require.Contains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", runner.ResourceProvider.Namespace, "testResources")) - require.Contains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", runner.ResourceProvider.Namespace, "prodResources")) + // Verify the concise success log lines for both resource types are emitted + require.Contains(t, outputSink.Writes, output.LogOutput{ + Format: "%s/%s created", + Params: []any{runner.ResourceProvider.Namespace, "testResources"}, + }) + require.Contains(t, outputSink.Writes, output.LogOutput{ + Format: "%s/%s created", + Params: []any{runner.ResourceProvider.Namespace, "prodResources"}, + }) }) t.Run("Resource provider does not exist - registers resource provider with single type", func(t *testing.T) { @@ -153,11 +138,6 @@ func Test_Run(t *testing.T) { clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerNotFoundError) require.NoError(t, err) - var logBuffer bytes.Buffer - logger := func(format string, args ...any) { - fmt.Fprintf(&logBuffer, format+"\n", args...) - } - outputSink := &output.MockOutput{} runner := &Runner{ UCPClientFactory: clientFactory, @@ -165,17 +145,21 @@ func Test_Run(t *testing.T) { Workspace: &workspaces.Workspace{}, ResourceProvider: resourceProviderData, Format: "table", - Logger: logger, ResourceProviderManifestFilePath: "testdata/valid.yaml", ResourceTypeName: expectedResourceType, } _ = runner.Run(context.Background()) - // Verify RegisterResourceProvider was called with only the specified resource type - logOutput := logBuffer.String() - require.Contains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", runner.ResourceProvider.Namespace, "testResources")) - require.NotContains(t, logOutput, fmt.Sprintf("Creating resource type %s/%s", runner.ResourceProvider.Namespace, "prodResources")) + // Verify only the specified resource type produced a success line. + require.Contains(t, outputSink.Writes, output.LogOutput{ + Format: "%s/%s created", + Params: []any{runner.ResourceProvider.Namespace, "testResources"}, + }) + require.NotContains(t, outputSink.Writes, output.LogOutput{ + Format: "%s/%s created", + Params: []any{runner.ResourceProvider.Namespace, "prodResources"}, + }) }) t.Run("Get Resource provider Internal Error", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -189,18 +173,12 @@ func Test_Run(t *testing.T) { clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerInternalError) require.NoError(t, err) - var logBuffer bytes.Buffer - logger := func(format string, args ...any) { - fmt.Fprintf(&logBuffer, format+"\n", args...) - } - runner := &Runner{ UCPClientFactory: clientFactory, Output: &output.MockOutput{}, Workspace: &workspaces.Workspace{}, ResourceProvider: resourceProviderData, Format: "table", - Logger: logger, ResourceProviderManifestFilePath: "testdata/valid.yaml", ResourceTypeName: expectedResourceType, } diff --git a/pkg/cli/cmd/resourcetype/delete/delete.go b/pkg/cli/cmd/resourcetype/delete/delete.go index 0c3533029a..e7fd7c5d2d 100644 --- a/pkg/cli/cmd/resourcetype/delete/delete.go +++ b/pkg/cli/cmd/resourcetype/delete/delete.go @@ -145,9 +145,9 @@ func (r *Runner) Run(ctx context.Context) error { } if deleted { - r.Output.LogInfo("Resource type %q deleted.", r.ResourceTypeName) + r.Output.LogInfo("%s deleted", r.ResourceTypeName) } else { - r.Output.LogInfo("Resource type %q does not exist or has already been deleted.", r.ResourceTypeName) + r.Output.LogInfo("%s not found", r.ResourceTypeName) } return nil diff --git a/pkg/cli/cmd/resourcetype/delete/delete_test.go b/pkg/cli/cmd/resourcetype/delete/delete_test.go index 331d2b560d..686fd3bb4a 100644 --- a/pkg/cli/cmd/resourcetype/delete/delete_test.go +++ b/pkg/cli/cmd/resourcetype/delete/delete_test.go @@ -103,7 +103,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource type %q deleted.", + Format: "%s deleted", Params: []any{"Applications.Test/testResources"}, }, } @@ -146,7 +146,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource type %q does not exist or has already been deleted.", + Format: "%s not found", Params: []any{"Applications.Test/testResources"}, }, } @@ -195,7 +195,7 @@ func Test_Run(t *testing.T) { expected := []any{ output.LogOutput{ - Format: "Resource type %q deleted.", + Format: "%s deleted", Params: []any{"Applications.Test/testResources"}, }, } diff --git a/pkg/cli/cmd/workspace/create/create.go b/pkg/cli/cmd/workspace/create/create.go index 64e77f154b..fe56291b75 100644 --- a/pkg/cli/cmd/workspace/create/create.go +++ b/pkg/cli/cmd/workspace/create/create.go @@ -208,12 +208,11 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { // Run creates a workspace and sets it as the current workspace, returning an error if any occurs during the process." func (r *Runner) Run(ctx context.Context) error { - r.Output.LogInfo("Creating workspace...") err := r.ConfigFileInterface.EditWorkspaces(ctx, r.ConfigHolder.Config, r.Workspace) if err != nil { return err } - output.LogInfo("Set %q as current workspace", r.Workspace.Name) + r.Output.LogInfo("Local workspace %s created (current)", r.Workspace.Name) return nil } diff --git a/pkg/cli/cmd/workspace/create/create_test.go b/pkg/cli/cmd/workspace/create/create_test.go index 7929acfbc8..02940cf4de 100644 --- a/pkg/cli/cmd/workspace/create/create_test.go +++ b/pkg/cli/cmd/workspace/create/create_test.go @@ -161,6 +161,12 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) + require.Equal(t, []any{ + output.LogOutput{ + Format: "Local workspace %s created (current)", + Params: []any{"defaultWorkspace"}, + }, + }, outputSink.Writes) }) } diff --git a/pkg/cli/cmd/workspace/delete/delete.go b/pkg/cli/cmd/workspace/delete/delete.go index 29c83b9eb3..4c8f6a2a33 100644 --- a/pkg/cli/cmd/workspace/delete/delete.go +++ b/pkg/cli/cmd/workspace/delete/delete.go @@ -131,5 +131,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } + r.Output.LogInfo("Local workspace %s deleted", r.Workspace.Name) return nil } diff --git a/pkg/cli/cmd/workspace/delete/delete_test.go b/pkg/cli/cmd/workspace/delete/delete_test.go index 37b9051774..deb23ae7ad 100644 --- a/pkg/cli/cmd/workspace/delete/delete_test.go +++ b/pkg/cli/cmd/workspace/delete/delete_test.go @@ -115,7 +115,12 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) - require.Empty(t, outputSink.Writes) + require.Equal(t, []any{ + output.LogOutput{ + Format: "Local workspace %s deleted", + Params: []any{"test-workspace"}, + }, + }, outputSink.Writes) }) t.Run("Delete workspace bypass confirmation", func(t *testing.T) { outputSink := &output.MockOutput{} @@ -147,7 +152,12 @@ func Test_Run(t *testing.T) { err := runner.Run(context.Background()) require.NoError(t, err) - require.Empty(t, outputSink.Writes) + require.Equal(t, []any{ + output.LogOutput{ + Format: "Local workspace %s deleted", + Params: []any{"test-workspace"}, + }, + }, outputSink.Writes) }) t.Run("Delete workspace not confirmed", func(t *testing.T) { outputSink := &output.MockOutput{} From c8ebbe78f11a8e858c5c6ece73f497d5d406fc1c Mon Sep 17 00:00:00 2001 From: Asish Kumar <87874775+officialasishkumar@users.noreply.github.com> Date: Tue, 19 May 2026 04:18:29 +0530 Subject: [PATCH 02/13] Resource type not found error cleanup (#11849) # Description `rad resource-type show` returned provider-focused errors when a user asked for a resource type that did not exist. If the provider namespace was missing, the CLI said the resource provider was not found; if the provider existed but the type was missing, the CLI used a different resource-provider-specific message. This change returns one resource-type-focused message for both cases: `The resource type "/" does not exist.` The behavior is covered in the shared resource type lookup tests and the `rad resource-type show` command tests. ## Type of change - This pull request fixes a bug in Radius and has an approved issue (issue link required). Fixes: #9897 Fixes: #9755 ## 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 is added or updated under `eng/design-notes/` in this repository, 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 [resource-types-contrib](https://github.com/radius-project/resource-types-contrib/) is created, if resource types or recipes are affected by the changes in this PR. - [ ] Yes - [x] Not applicable - A PR for [dashboard](https://github.com/radius-project/dashboard/) is created, if the Radius Dashboard is 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 --------- Signed-off-by: Asish Kumar Co-authored-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Signed-off-by: Zach Casper --- pkg/cli/cmd/resourcetype/common/resourcetype.go | 8 +++++--- .../cmd/resourcetype/common/resourcetype_test.go | 14 +++++++++++++- pkg/cli/cmd/resourcetype/show/show_test.go | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/cli/cmd/resourcetype/common/resourcetype.go b/pkg/cli/cmd/resourcetype/common/resourcetype.go index dd043f10d8..02c7216870 100644 --- a/pkg/cli/cmd/resourcetype/common/resourcetype.go +++ b/pkg/cli/cmd/resourcetype/common/resourcetype.go @@ -134,20 +134,22 @@ func GetResourceTypeShowSchemaTableFormat() output.FormatterOptions { // GetResourceTypeDetails retrieves the details of a resource provider's resource type using the UCP client. // It returns the resource type details or an error if the resource type is not found. func GetResourceTypeDetails(ctx context.Context, resourceProviderName string, resourceTypeName string, clientFactory *v20231001preview.ClientFactory) (ResourceType, error) { + fullyQualifiedResourceType := resourceProviderName + "/" + resourceTypeName + response, err := clientFactory.NewResourceProvidersClient().GetProviderSummary(ctx, "local", resourceProviderName, nil) if clients.Is404Error(err) { - return ResourceType{}, clierrors.Message("The resource provider %q was not found or has been deleted.", resourceProviderName) + return ResourceType{}, clierrors.Message("The resource type %q does not exist.", fullyQualifiedResourceType) } else if err != nil { return ResourceType{}, err } resourceTypes := ResourceTypesForProvider(&response.ResourceProviderSummary) idx := slices.IndexFunc(resourceTypes, func(rt ResourceType) bool { - return rt.Name == resourceProviderName+"/"+resourceTypeName + return rt.Name == fullyQualifiedResourceType }) if idx < 0 { - return ResourceType{}, clierrors.Message("Resource type %q not found in resource provider %q.", resourceTypeName, *response.ResourceProviderSummary.Name) + return ResourceType{}, clierrors.Message("The resource type %q does not exist.", fullyQualifiedResourceType) } return resourceTypes[idx], nil diff --git a/pkg/cli/cmd/resourcetype/common/resourcetype_test.go b/pkg/cli/cmd/resourcetype/common/resourcetype_test.go index 27d0c24ab5..c6fd902472 100644 --- a/pkg/cli/cmd/resourcetype/common/resourcetype_test.go +++ b/pkg/cli/cmd/resourcetype/common/resourcetype_test.go @@ -48,7 +48,19 @@ func Test_GetResourceTypeDetails(t *testing.T) { _, err = GetResourceTypeDetails(context.Background(), "MyCompany.Resources", "testResources", clientFactory) require.Error(t, err) - require.Equal(t, "The resource provider \"MyCompany.Resources\" was not found or has been deleted.", err.Error()) + require.Equal(t, "The resource type \"MyCompany.Resources/testResources\" does not exist.", err.Error()) + }) + + t.Run("Get Resource Details Failure - Resource Type Not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clientFactory, err := manifest.NewTestClientFactory(manifest.WithResourceProviderServerNoError) + require.NoError(t, err) + + _, err = GetResourceTypeDetails(context.Background(), "MyCompany.Resources", "missingResources", clientFactory) + require.Error(t, err) + require.Equal(t, "The resource type \"MyCompany.Resources/missingResources\" does not exist.", err.Error()) }) t.Run("Get Resource Details Failures Other Than Not Found", func(t *testing.T) { diff --git a/pkg/cli/cmd/resourcetype/show/show_test.go b/pkg/cli/cmd/resourcetype/show/show_test.go index 62faf6de03..642a238062 100644 --- a/pkg/cli/cmd/resourcetype/show/show_test.go +++ b/pkg/cli/cmd/resourcetype/show/show_test.go @@ -208,7 +208,7 @@ func Test_Run(t *testing.T) { err = runner.Run(context.Background()) require.Error(t, err) - require.Equal(t, clierrors.Message("The resource provider \"Applications.AnotherTest\" was not found or has been deleted."), err) + require.Equal(t, clierrors.Message("The resource type \"Applications.AnotherTest/exampleResources\" does not exist."), err) require.Empty(t, outputSink.Writes) }) @@ -240,7 +240,7 @@ func Test_Run(t *testing.T) { err = runner.Run(context.Background()) require.Error(t, err) - require.Equal(t, clierrors.Message("Resource type \"anotherResources\" not found in resource provider \"Applications.Test\"."), err) + require.Equal(t, clierrors.Message("The resource type \"Applications.Test/anotherResources\" does not exist."), err) require.Empty(t, outputSink.Writes) }) From f7113deeed8ab203a0cc243d447c1c447224a1a8 Mon Sep 17 00:00:00 2001 From: Karishma Chawla Date: Tue, 19 May 2026 09:43:40 -0700 Subject: [PATCH 03/13] Fix resource types missing from rad resource-type list with per-type manifest files (#11933) ## Background PR #11911 introduced automated synchronization of default resource type manifests from `resource-types-contrib`. As part of that change, the manifest file layout in `deploy/manifest/built-in-providers/` changed from one file per namespace (e.g., `radius_compute.yaml` containing `containers`, `routes`, and `persistentVolumes`) to one file per type (e.g., `containers.yaml`, `routes.yaml`, `persistentVolumes.yaml`). This matches the per-type layout in `resource-types-contrib` and keeps diffs scoped to the type that changed. However, the initializer's `registerResourceProviderDirect` function was not designed for multiple files sharing the same namespace. It saves the Location and ResourceProviderSummary per namespace on each file, and each save was a full overwrite rather than a merge. ## Problem When multiple manifest files share the same namespace (e.g., `containers.yaml` and `persistentVolumes.yaml` both under `Radius.Compute`), the initializer was overwriting the Location and ResourceProviderSummary on each file, causing earlier types to disappear from `rad resource-type list`. Only the last file's types (alphabetically) were visible. The individual ResourceType records were saved correctly (one per type), but the Location and Summary - which UCP's routing layer and `rad resource-type list` read from - only contained the last file's types. ## Fix `registerResourceProviderDirect` now reads the existing Location and Summary from the database before saving, merging in new types alongside existing ones. If no existing record is found (first file for this namespace), it behaves as before. Only `ErrNotFound` is treated as "no existing record"; other errors (database outage, decode failure) cause the initializer to fail fast. The fix also preserves the existing location address when a later manifest for the same namespace does not specify one. ## Changes - `pkg/ucp/initializer/service.go` - Read-merge-write for Location and ResourceProviderSummary, with ErrNotFound-only error handling and address preservation - `pkg/ucp/integrationtests/resourceproviders/resourceproviders_test.go` - Updated `Test_ResourceProvider_RegisterManifests_NoLocation` to verify both types from two files sharing a namespace are registered and present in the location's resourceTypes map. Added `Test_ResourceProvider_DefaultsRegistered` that reads `defaults.yaml`, loads real manifests, and verifies all types are registered with correct location entries. - `testdata/manifests-no-location/persistentVolumes.yaml` - Second test manifest sharing the `Radius.Compute` namespace - `test/functional-portable/dynamicrp/noncloud/resources/default_containers_test.go` - End-to-end functional test deploying `Radius.Compute/containers` and `Radius.Compute/routes` (two types from the same namespace) using the default recipes - `test/functional-portable/dynamicrp/noncloud/resources/testdata/default-containers-test.bicep` - Bicep template for the e2e test ## Testing - **Integration test** (`Test_ResourceProvider_RegisterManifests_NoLocation`): Two test manifests sharing `Radius.Compute` namespace, verifies both types appear in the location's resourceTypes map. - **Integration test** (`Test_ResourceProvider_DefaultsRegistered`): Reads `defaults.yaml`, loads manifests from `built-in-providers/dev/`, verifies all types are registered with correct location entries. - **Functional test** (`Test_DefaultContainers_Deploy`): End-to-end deployment of `Radius.Compute/containers` and `Radius.Compute/routes` using default recipes. --------- Signed-off-by: Karishma Chawla Signed-off-by: Zach Casper --- pkg/ucp/initializer/service.go | 41 ++++++- pkg/ucp/initializer/service_test.go | 81 ++++++++++++++ .../resourceproviders_test.go | 102 +++++++++++++++++- .../persistentVolumes.yaml | 13 +++ 4 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 pkg/ucp/integrationtests/resourceproviders/testdata/manifests-no-location/persistentVolumes.yaml diff --git a/pkg/ucp/initializer/service.go b/pkg/ucp/initializer/service.go index cd45a42d23..8a2d5a9fc5 100644 --- a/pkg/ucp/initializer/service.go +++ b/pkg/ucp/initializer/service.go @@ -75,21 +75,54 @@ func (w *Service) Run(ctx context.Context) error { return fmt.Errorf("failed to read manifest directory: %w", err) } + // Read and validate all manifest files, then merge by namespace so that + // multiple per-type files sharing a namespace (e.g., containers.yaml and + // persistentVolumes.yaml both under Radius.Compute) are registered as a + // single resource provider with all types. This makes the on-disk manifest + // directory the source of truth for the database. + merged := map[string]*manifest.ResourceProvider{} for _, fileInfo := range files { if fileInfo.IsDir() { continue } filePath := filepath.Join(manifestDir, fileInfo.Name()) - logger.Info("Registering manifest", "file", filePath) + logger.Info("Loading manifest", "file", filePath) - resourceProvider, err := manifest.ValidateManifest(ctx, filePath) + rp, err := manifest.ValidateManifest(ctx, filePath) if err != nil { return fmt.Errorf("failed to validate manifest %s: %w", filePath, err) } - if err := registerResourceProviderDirect(ctx, dbClient, "local", *resourceProvider); err != nil { - return fmt.Errorf("failed to register manifest %s: %w", filePath, err) + existing, ok := merged[rp.Namespace] + if !ok { + merged[rp.Namespace] = rp + continue + } + + // Merge types from this file into the existing provider for this + // namespace. Error if a type appears in multiple files. + for typeName := range rp.Types { + if _, exists := existing.Types[typeName]; exists { + return fmt.Errorf("duplicate resource type %s/%s found in multiple manifest files", rp.Namespace, typeName) + } + } + for typeName, resourceType := range rp.Types { + existing.Types[typeName] = resourceType + } + + // Preserve location from whichever file specifies one. If multiple + // files set a location, the later file wins. + if len(rp.Location) > 0 { + existing.Location = rp.Location + } + } + + // Register each merged provider. + for _, rp := range merged { + logger.Info("Registering manifest", "namespace", rp.Namespace, "types", len(rp.Types)) + if err := registerResourceProviderDirect(ctx, dbClient, "local", *rp); err != nil { + return fmt.Errorf("failed to register manifest for namespace %s: %w", rp.Namespace, err) } } diff --git a/pkg/ucp/initializer/service_test.go b/pkg/ucp/initializer/service_test.go index 436dd4a0a7..6db9209776 100644 --- a/pkg/ucp/initializer/service_test.go +++ b/pkg/ucp/initializer/service_test.go @@ -213,6 +213,87 @@ types: require.NoError(t, err) require.NotNil(t, obj) }) + + t.Run("errors on duplicate type across manifest files", func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + // Two manifest files with the same namespace and same type name. + manifest1 := ` +namespace: Radius.Compute +types: + containers: + apiVersions: + "2025-08-01-preview": + schema: {} +` + manifest2 := ` +namespace: Radius.Compute +types: + containers: + apiVersions: + "2025-08-01-preview": + schema: {} +` + err := os.WriteFile(filepath.Join(tempDir, "a-containers.yaml"), []byte(manifest1), 0600) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "b-containers.yaml"), []byte(manifest2), 0600) + require.NoError(t, err) + + svc := newTestService(tempDir) + err = svc.Run(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "duplicate resource type Radius.Compute/containers") + }) + + t.Run("merges types from multiple files sharing a namespace", func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + manifest1 := ` +namespace: Radius.Compute +types: + containers: + apiVersions: + "2025-08-01-preview": + schema: {} +` + manifest2 := ` +namespace: Radius.Compute +types: + routes: + apiVersions: + "2025-08-01-preview": + schema: {} +` + err := os.WriteFile(filepath.Join(tempDir, "containers.yaml"), []byte(manifest1), 0600) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "routes.yaml"), []byte(manifest2), 0600) + require.NoError(t, err) + + svc := newTestService(tempDir) + dbClient, err := svc.options.DatabaseProvider.GetClient(context.Background()) + require.NoError(t, err) + + err = svc.Run(context.Background()) + require.NoError(t, err) + + // Verify both types are registered under the same namespace. + _, err = dbClient.Get(context.Background(), "/planes/radius/local/providers/System.Resources/resourceProviders/Radius.Compute/resourceTypes/containers") + require.NoError(t, err) + _, err = dbClient.Get(context.Background(), "/planes/radius/local/providers/System.Resources/resourceProviders/Radius.Compute/resourceTypes/routes") + require.NoError(t, err) + + // Verify the location contains both types. + obj, err := dbClient.Get(context.Background(), "/planes/radius/local/providers/System.Resources/resourceProviders/Radius.Compute/locations/global") + require.NoError(t, err) + location := &datamodel.Location{} + require.NoError(t, obj.As(location)) + assert.Contains(t, location.Properties.ResourceTypes, "containers") + assert.Contains(t, location.Properties.ResourceTypes, "routes") + }) } func Test_saveResource(t *testing.T) { diff --git a/pkg/ucp/integrationtests/resourceproviders/resourceproviders_test.go b/pkg/ucp/integrationtests/resourceproviders/resourceproviders_test.go index 4555bf015e..830e3dbdbc 100644 --- a/pkg/ucp/integrationtests/resourceproviders/resourceproviders_test.go +++ b/pkg/ucp/integrationtests/resourceproviders/resourceproviders_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "net/http" "os" + "strings" "testing" "time" @@ -27,6 +28,7 @@ import ( "github.com/radius-project/radius/pkg/ucp/testhost" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) const ( @@ -166,6 +168,11 @@ func Test_ResourceProvider_RegisterManifests(t *testing.T) { // the code path used by resource type manifests copied from // resource-types-contrib, which omit location so that UCP routes requests via // DefaultDownstreamEndpoint (dynamic-rp). +// +// The test directory contains two manifest files (containers.yaml and +// persistentVolumes.yaml) that share the same namespace (Radius.Compute). +// This verifies that the initializer correctly merges types from multiple +// files into the same resource provider and location. func Test_ResourceProvider_RegisterManifests_NoLocation(t *testing.T) { server := testhost.Start(t, testhost.TestHostOptionFunc(func(options *ucp.Options) { options.Config.Initialization.ManifestDirectory = "testdata/manifests-no-location" @@ -176,7 +183,8 @@ func Test_ResourceProvider_RegisterManifests_NoLocation(t *testing.T) { noLocationNamespace := "Radius.Compute" noLocationResourceProviderURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + noLocationNamespace + radiusAPIVersion - noLocationResourceTypeURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + noLocationNamespace + "/resourcetypes/containers" + radiusAPIVersion + noLocationContainersURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + noLocationNamespace + "/resourcetypes/containers" + radiusAPIVersion + noLocationPersistentVolumesURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + noLocationNamespace + "/resourcetypes/persistentVolumes" + radiusAPIVersion noLocationLocationURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + noLocationNamespace + "/locations/global" + radiusAPIVersion require.EventuallyWithTf(t, func(collect *assert.CollectT) { @@ -184,10 +192,14 @@ func Test_ResourceProvider_RegisterManifests_NoLocation(t *testing.T) { response := server.MakeRequest(http.MethodGet, noLocationResourceProviderURL, nil) assert.Equal(collect, 200, response.Raw.StatusCode, "resource provider Radius.Compute should be registered") - // Verify the resource type was registered. - response = server.MakeRequest(http.MethodGet, noLocationResourceTypeURL, nil) + // Verify both resource types from separate manifest files are registered + // under the same namespace. + response = server.MakeRequest(http.MethodGet, noLocationContainersURL, nil) assert.Equal(collect, 200, response.Raw.StatusCode, "resource type Radius.Compute/containers should be registered") + response = server.MakeRequest(http.MethodGet, noLocationPersistentVolumesURL, nil) + assert.Equal(collect, 200, response.Raw.StatusCode, "resource type Radius.Compute/persistentVolumes should be registered") + // Verify the location was created with no address, so UCP uses // DefaultDownstreamEndpoint for routing. response = server.MakeRequest(http.MethodGet, noLocationLocationURL, nil) @@ -202,9 +214,93 @@ func Test_ResourceProvider_RegisterManifests_NoLocation(t *testing.T) { props, _ := locationBody["properties"].(map[string]any) assert.Nil(collect, props["address"], "location address should be absent for no-location manifests") + + // Verify the location contains both resource types from the two manifest + // files. Without the namespace merge fix, the location would only contain + // the types from the last file processed (alphabetically). + resourceTypes, _ := props["resourceTypes"].(map[string]any) + if !assert.Contains(collect, resourceTypes, "containers", "location should contain containers type") { + return + } + if !assert.Contains(collect, resourceTypes, "persistentVolumes", "location should contain persistentVolumes type") { + return + } }, registerManifestWaitDuration, registerManifestWaitInterval, "no-location manifest registration did not complete in time") } +// Test_ResourceProvider_DefaultsRegistered verifies that all resource types +// listed in deploy/manifest/defaults.yaml are registered after startup when +// the initializer loads the real manifest files from built-in-providers/dev/. +// +// This catches regressions where: +// - A manifest file fails to load at startup +// - Multiple files sharing a namespace overwrite each other's types +// - A type is added to defaults.yaml but its manifest is missing or invalid +func Test_ResourceProvider_DefaultsRegistered(t *testing.T) { + // Read defaults.yaml to get the list of expected resource types. + data, err := os.ReadFile("../../../../deploy/manifest/defaults.yaml") + require.NoError(t, err, "failed to read defaults.yaml") + + var defaults struct { + DefaultRegistration []string `yaml:"defaultRegistration"` + } + require.NoError(t, yaml.Unmarshal(data, &defaults), "failed to parse defaults.yaml") + require.NotEmpty(t, defaults.DefaultRegistration, "defaults.yaml should list at least one resource type") + + // Start the test host with the real manifest directory. + server := testhost.Start(t, testhost.TestHostOptionFunc(func(options *ucp.Options) { + options.Config.Initialization.ManifestDirectory = "../../../../deploy/manifest/built-in-providers/dev" + })) + defer server.Close() + + createRadiusPlane(server) + + // Build expected types grouped by namespace for location verification. + namespaceTypes := map[string][]string{} + for _, entry := range defaults.DefaultRegistration { + parts := strings.SplitN(entry, "/", 2) + require.Len(t, parts, 2, "invalid entry in defaults.yaml: %s", entry) + namespaceTypes[parts[0]] = append(namespaceTypes[parts[0]], parts[1]) + } + + require.EventuallyWithTf(t, func(collect *assert.CollectT) { + // Verify each resource type is queryable via the API. + for _, entry := range defaults.DefaultRegistration { + parts := strings.SplitN(entry, "/", 2) + namespace := parts[0] + typeName := parts[1] + + typeURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + namespace + "/resourcetypes/" + typeName + radiusAPIVersion + response := server.MakeRequest(http.MethodGet, typeURL, nil) + if !assert.Equal(collect, 200, response.Raw.StatusCode, "resource type %s should be registered", entry) { + return + } + } + + // Verify each namespace's location contains all of its types. + for namespace, types := range namespaceTypes { + locationURL := "/planes/radius/local/providers/System.Resources/resourceproviders/" + namespace + "/locations/global" + radiusAPIVersion + response := server.MakeRequest(http.MethodGet, locationURL, nil) + if !assert.Equal(collect, 200, response.Raw.StatusCode, "location for %s should exist", namespace) { + return + } + + var body map[string]any + if !assert.NoError(collect, json.Unmarshal(response.Body.Bytes(), &body)) { + return + } + + props, _ := body["properties"].(map[string]any) + resourceTypes, _ := props["resourceTypes"].(map[string]any) + for _, typeName := range types { + if !assert.Contains(collect, resourceTypes, typeName, "location for %s should contain type %s", namespace, typeName) { + return + } + } + } + }, registerManifestWaitDuration, registerManifestWaitInterval, "default resource type registration did not complete in time") +} + // removeSystemData removes the systemData property from the response body recursively. // This matches the behavior of TestResponse.removeSystemData in the testhost package. func removeSystemData(body map[string]any) { diff --git a/pkg/ucp/integrationtests/resourceproviders/testdata/manifests-no-location/persistentVolumes.yaml b/pkg/ucp/integrationtests/resourceproviders/testdata/manifests-no-location/persistentVolumes.yaml new file mode 100644 index 0000000000..d239c44bdd --- /dev/null +++ b/pkg/ucp/integrationtests/resourceproviders/testdata/manifests-no-location/persistentVolumes.yaml @@ -0,0 +1,13 @@ +namespace: Radius.Compute +types: + persistentVolumes: + apiVersions: + "2025-08-01-preview": + schema: + type: object + properties: + environment: + type: string + application: + type: string + required: [environment, application] From b3bdf182a54dcb50c89562762af1128b7e8ad365 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 09:47:43 -0700 Subject: [PATCH 04/13] build(deps): bump the github-actions group across 1 directory with 2 updates (#11929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 2 updates in the / directory: [github/codeql-action](https://github.com/github/codeql-action) and [dessant/label-actions](https://github.com/dessant/label-actions). Updates `github/codeql-action` from 4.35.4 to 4.35.5
Release notes

Sourced from github/codeql-action's releases.

v4.35.5

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880
Changelog

Sourced from github/codeql-action's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893

4.35.5 - 15 May 2026

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

4.35.4 - 07 May 2026

  • Update default CodeQL bundle version to 2.25.4. #3881

4.35.3 - 01 May 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852
  • Update default CodeQL bundle version to 2.25.3. #3865

4.35.2 - 15 Apr 2026

  • The undocumented TRAP cache cleanup feature that could be enabled using the CODEQL_ACTION_CLEANUP_TRAP_CACHES environment variable is deprecated and will be removed in May 2026. If you are affected by this, we recommend disabling TRAP caching by passing the trap-caching: false input to the init Action. #3795
  • The Git version 2.36.0 requirement for improved incremental analysis now only applies to repositories that contain submodules. #3789
  • Python analysis on GHES no longer extracts the standard library, relying instead on models of the standard library. This should result in significantly faster extraction and analysis times, while the effect on alerts should be minimal. #3794
  • Fixed a bug in the validation of OIDC configurations for private registries that was added in CodeQL Action 4.33.0 / 3.33.0. #3807
  • Update default CodeQL bundle version to 2.25.2. #3823

4.35.1 - 27 Mar 2026

4.35.0 - 27 Mar 2026

4.34.1 - 20 Mar 2026

  • Downgrade default CodeQL bundle version to 2.24.3 due to issues with a small percentage of Actions and JavaScript analyses. #3762

4.34.0 - 20 Mar 2026

... (truncated)

Commits
  • 9e0d7b8 Merge pull request #3905 from github/update-v4.35.5-d4b485515
  • 6d7d599 Add changelog entry for #3899
  • 51f7e38 Update changelog for v4.35.5
  • d4b4855 Merge pull request #3899 from github/mbg/esbuild/split
  • 127de81 Merge remote-tracking branch 'origin/main' into mbg/esbuild/split
  • 7fde13f Use src + basename in header to avoid issues on Windows
  • dfa61e7 Improve pattern matching and error handling
  • 52aafec Import and call runWrapper normally in analyze tests
  • 0d08c01 Auto-generate shared bundle
  • 14085a6 Auto-generate entry points
  • Additional commits viewable in compare view

Updates `dessant/label-actions` from 5.0.0 to 5.0.2
Release notes

Sourced from dessant/label-actions's releases.

v5.0.2

Learn more about this release from the changelog.

v5.0.1

Learn more about this release from the changelog.

Changelog

Sourced from dessant/label-actions's changelog.

Changelog

All notable changes to this project will be documented in this file. See commit-and-tag-version for commit guidelines.

5.0.2 (2026-05-17)

Bug Fixes

  • reference correct variable (eff280f), closes #31

5.0.1 (2026-05-17)

Bug Fixes

5.0.0 (2025-12-12)

⚠ BREAKING CHANGES

  • the action now requires Node.js 24

Bug Fixes

  • update dependencies (eec3541)
  • uppercase default close reason for discussions (af2405b), closes #28

4.0.1 (2023-11-21)

Bug Fixes

  • always apply close and lock reason (a1d1e68)

4.0.0 (2023-11-10)

⚠ BREAKING CHANGES

  • the action now requires Node.js 20

Bug Fixes

  • retry and throttle GitHub API requests (0178594)
  • update dependencies (6b166ca)

3.1.0 (2023-06-08)

... (truncated)

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Zach Casper --- .github/workflows/codeql.yml | 10 +++++----- .github/workflows/scorecard.yaml | 2 +- .github/workflows/triage-bot.yaml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 45c689319f..2e4257dde3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -100,7 +100,7 @@ jobs: - name: Initialize CodeQL if: ${{ !startsWith(matrix.language, 'custom-') }} - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: config-file: .github/configs/.codeql.yml languages: ${{ matrix.language }} @@ -108,7 +108,7 @@ jobs: - name: Auto build if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: working-directory: ${{ matrix.working-directory }} @@ -127,14 +127,14 @@ jobs: - name: Upload GoSec result if: ${{ always() && matrix.language == 'custom-gosec' }} - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: gosec-results.sarif wait-for-processing: true - name: Perform CodeQL Analysis if: ${{ !startsWith(matrix.language, 'custom-') }} - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 id: codeql-analyze with: category: /language:${{matrix.language}} @@ -143,7 +143,7 @@ jobs: - name: Upload CodeQL result if: ${{ always() && !startsWith(matrix.language, 'custom-') }} - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: ${{ format('{0}/{1}.sarif', steps.codeql-analyze.outputs.sarif-output, matrix.language) }} wait-for-processing: true diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index 80b2cc0af4..c1bf7bc880 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: sarif_file: results.sarif diff --git a/.github/workflows/triage-bot.yaml b/.github/workflows/triage-bot.yaml index 24b55c63d0..74ddcda270 100644 --- a/.github/workflows/triage-bot.yaml +++ b/.github/workflows/triage-bot.yaml @@ -44,7 +44,7 @@ jobs: private-key: ${{ secrets.RADIUS_TRIAGE_BOT_PRIVATE_KEY }} permission-issues: write - - uses: dessant/label-actions@9e5fd757ffe1e065abf55e9f74d899dbe012922a # v5.0.0 + - uses: dessant/label-actions@809e0f9bc11ce08744e7ccac7cd10621d00aba2f # v5.0.2 with: github-token: ${{ steps.get_installation_token.outputs.token }} config-path: .github/triage-bot/triage-bot-config.yaml From a21db147910d1e05d86be3a3111f2aa290365a51 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 09:51:23 -0700 Subject: [PATCH 05/13] docs: require signed commits (#11873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Contributor docs only mentioned the DCO `Signed-off-by` line and never told contributors to cryptographically sign commits. Adds the missing guidance, written once and linked from related docs. - **`first-commit-06-creating-a-pr/index.md`**: New "Signing your commits" section — calls out that signing is separate from `Signed-off-by`, lists the three supported formats (GPG / SSH / S/MIME), links to the four relevant GitHub docs, and shows the `git config --global commit.gpgsign true` one-liner. - **`contributing-pull-requests/README.md`**: "Signing your commits" section is a one-line pointer to the canonical section above — no duplicated prose. ## 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 is added or updated under `eng/design-notes/` in this repository, 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 [resource-types-contrib](https://github.com/radius-project/resource-types-contrib/) is created, if resource types or recipes are affected by the changes in this PR. - [ ] Yes - [x] Not applicable - A PR for [dashboard](https://github.com/radius-project/dashboard/) is created, if the Radius Dashboard is 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 --------- Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: brooke-hamilton <45323234+brooke-hamilton@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zach Casper --- .../first-commit-06-creating-a-pr/index.md | 25 +++++++++++++++++++ .../contributing-pull-requests/README.md | 4 +++ 2 files changed, 29 insertions(+) diff --git a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md index 173b15ca63..a5a9883812 100644 --- a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md +++ b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md @@ -46,6 +46,31 @@ Radius leverages the [Developer Certificate of Origin](https://github.com/apps/d Visual Studio Code has a setting, `git.alwaysSignOff` to automatically add a Signed-off-by line to commit messages. Search for "sign-off" in VS Code settings to find it and enable it. +## Signing your commits + +> 💡 Commit signing is **separate from** the DCO `Signed-off-by` line described above. The `Signed-off-by` line is a textual attestation, while commit signing proves that the commit was signed with the private key corresponding to your configured public key. Both are recommended. + +We require all contributors to **cryptographically sign their commits** so that they show as **Verified** on GitHub. On GitHub, **Verified** means GitHub could validate the signature and that the signing key is associated with the account. This gives reviewers and the community additional confidence in the integrity and provenance of commits, which is an important supply-chain safeguard. + +GitHub supports three types of commit signatures: GPG, SSH, and S/MIME. Pick whichever is easiest for you — SSH signing is usually the simplest if you already use an SSH key with GitHub. + +Follow the official GitHub documentation to set this up: + +- [About commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) +- [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key) +- [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) +- [Displaying verification statuses for all of your commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits) + +Once configured, you can have Git sign every commit automatically by setting: + +```sh +git config --global commit.gpgsign true +``` + +(Use the same setting for SSH or S/MIME signing — Git will use whichever signing format you have configured.) + +After pushing, your commits should display a **Verified** badge next to them on GitHub. + ## Creating the pull request Please ensure you are contributing from a fork of the repository. If you have not set this up, please refer to the [forking guide](../../contributing-code-forks/index.md). diff --git a/docs/contributing/contributing-pull-requests/README.md b/docs/contributing/contributing-pull-requests/README.md index 1072251445..c86793332c 100644 --- a/docs/contributing/contributing-pull-requests/README.md +++ b/docs/contributing/contributing-pull-requests/README.md @@ -68,6 +68,10 @@ Fixes: # We **squash** pull-requests as part of the merge process, which means that intermediate commits will have their messages appended. We prefer to have a single commit in the git history for each PR. +## Signing your commits + +See [Signing your commits](../contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md#signing-your-commits) in the first commit guide. + ## Automated tests Our GitHub Actions workflows will run against your pull request to validate the changes. This will run the unit tests, integration tests, and functional tests. From ecab976412e4311dfad6845972456cfc863aeaf5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 19 May 2026 11:10:16 -0700 Subject: [PATCH 06/13] Hydrate Radius.Core schemas from OpenAPI (#11881) ## Summary Fixes #10894 by hydrating Radius.Core resource type metadata from the generated OpenAPI spec during UCP startup. The built-in manifests can stay minimal, while UCP registration fills descriptions and schemas for Radius.Core/applications, Radius.Core/environments, Radius.Core/recipePacks, Radius.Core/terraformConfigs, and Radius.Core/bicepConfigs. ## Root Cause The Radius.Core built-in manifests register these resource types with empty schemas. rad resource-type show reads ResourceProviderSummary, so the CLI and dashboard only saw empty descriptions and null schemas for these built-in types. ## What Changed - Added UCP initializer hydration for Radius.Core metadata. - Reads the embedded generated OpenAPI document from swagger.SpecFiles. - Maps each built-in type to its TypeSpec-generated OpenAPI definitions. - Expands local OpenAPI refs before persisting so existing CLI schema rendering receives concrete property schemas. - Writes hydrated schemas through the existing registration flow into both APIVersion resources and ResourceProviderSummary. - Fails fast if a Radius.Core manifest type is missing an OpenAPI metadata mapping, preventing silent manifest/OpenAPI drift. - Added regression coverage using the real built-in radius_core.yaml manifest. ## User Experience ### Before Users could discover that Radius.Core/applications, Radius.Core/environments, Radius.Core/recipePacks, Radius.Core/terraformConfigs, and Radius.Core/bicepConfigs existed, but the type details were effectively empty. For example: ```bash rad resource-type show Radius.Core/applications ``` returned only the type name and API version: ```text TYPE NAMESPACE Radius.Core/applications Radius.Core DESCRIPTION: API VERSION: 2025-08-01-preview ``` JSON output also showed the missing metadata: ```json "Description": "", "Schema": null ``` In the Dashboard, these built-in Radius.Core types appeared without useful schema/property information, so users could not inspect required fields, read-only fields, nested properties, or property descriptions. ### After Users can inspect the built-in Radius.Core types the same way they inspect other registered resource types. For example: ```bash rad resource-type show Radius.Core/applications ``` now shows the generated TypeSpec/OpenAPI metadata: ```text TYPE NAMESPACE Radius.Core/applications Radius.Core DESCRIPTION: Radius Application resource API VERSION: 2025-08-01-preview TOP-LEVEL PROPERTIES: NAME TYPE REQUIRED READ-ONLY DESCRIPTION environment string true false Fully qualified resource ID for the environment that the application is linked to provisioningState string false true The status of the asynchronous operation. status object false true Status of a resource. ``` The same improvement applies to: - Radius.Core/environments - Radius.Core/recipePacks - Radius.Core/terraformConfigs - Radius.Core/bicepConfigs Dashboard and CLI users now get meaningful descriptions and schema details for these built-in types without duplicating schemas in the manifest YAML. ## Validation - go test ./pkg/ucp/initializer ./pkg/cli/cmd/resourcetype/show ./pkg/cli/cmd/resourcetype/common ./pkg/cli/manifest ./pkg/schema - git diff --check - Live kind smoke test on kind-radius-10894 with patched UCP image verified applications, environments, and recipePacks render schema details. Signed-off-by: Zach Casper --- pkg/ucp/initializer/radius_core_openapi.go | 243 +++++++++++++++++++++ pkg/ucp/initializer/service.go | 4 + pkg/ucp/initializer/service_test.go | 218 ++++++++++++++++++ 3 files changed, 465 insertions(+) create mode 100644 pkg/ucp/initializer/radius_core_openapi.go diff --git a/pkg/ucp/initializer/radius_core_openapi.go b/pkg/ucp/initializer/radius_core_openapi.go new file mode 100644 index 0000000000..8beb76a350 --- /dev/null +++ b/pkg/ucp/initializer/radius_core_openapi.go @@ -0,0 +1,243 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package initializer + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/radius-project/radius/pkg/cli/manifest" + "github.com/radius-project/radius/swagger" +) + +const ( + radiusCoreNamespace = "Radius.Core" + radiusCoreAPIVersion = "2025-08-01-preview" + radiusCoreOpenAPIFile = "specification/radius/resource-manager/Radius.Core/preview/2025-08-01-preview/openapi.json" + openAPIDefinitionRefRoot = "#/definitions/" +) + +var radiusCoreTypeOpenAPIDefinitions = map[string]struct { + resourceDefinition string + propertiesDefinition string +}{ + "applications": { + resourceDefinition: "ApplicationResource", + propertiesDefinition: "ApplicationProperties", + }, + "bicepConfigs": { + resourceDefinition: "BicepConfigResource", + propertiesDefinition: "BicepConfigProperties", + }, + "environments": { + resourceDefinition: "EnvironmentResource", + propertiesDefinition: "EnvironmentProperties", + }, + "recipePacks": { + resourceDefinition: "RecipePackResource", + propertiesDefinition: "RecipePackProperties", + }, + "terraformConfigs": { + resourceDefinition: "TerraformConfigResource", + propertiesDefinition: "TerraformConfigProperties", + }, +} + +type openAPIDocument struct { + Definitions map[string]map[string]any `json:"definitions"` +} + +// hydrateBuiltInResourceProviderMetadata fills built-in manifest metadata that is intentionally not +// duplicated in YAML. Radius.Core schemas are sourced from the generated OpenAPI spec so the CLI and +// dashboard see the same TypeSpec-authored descriptions and property schemas that UCP serves. +func hydrateBuiltInResourceProviderMetadata(rp *manifest.ResourceProvider) error { + if !strings.EqualFold(rp.Namespace, radiusCoreNamespace) { + return nil + } + + doc, err := loadRadiusCoreOpenAPI() + if err != nil { + return err + } + + for typeName, resourceType := range rp.Types { + definitionNames, ok := radiusCoreTypeOpenAPIDefinitions[typeName] + if !ok { + return fmt.Errorf("%s type %s has no OpenAPI metadata mapping", rp.Namespace, typeName) + } + if resourceType == nil { + return fmt.Errorf("mapped %s type %s is nil", rp.Namespace, typeName) + } + + description, err := openAPIDefinitionDescription(doc, definitionNames.resourceDefinition) + if err != nil { + return fmt.Errorf("failed to get description for %s/%s: %w", rp.Namespace, typeName, err) + } + resourceType.Description = &description + + apiVersion, ok := resourceType.APIVersions[radiusCoreAPIVersion] + if !ok { + return fmt.Errorf("mapped %s type %s is missing API version %s", rp.Namespace, typeName, radiusCoreAPIVersion) + } + if apiVersion == nil { + return fmt.Errorf("mapped %s type %s has nil API version %s", rp.Namespace, typeName, radiusCoreAPIVersion) + } + + schema, err := openAPIDefinitionSchema(doc, definitionNames.propertiesDefinition) + if err != nil { + return fmt.Errorf("failed to get schema for %s/%s@%s: %w", rp.Namespace, typeName, radiusCoreAPIVersion, err) + } + apiVersion.Schema = schema + } + + return nil +} + +// loadRadiusCoreOpenAPI reads the embedded TypeSpec-generated Radius.Core OpenAPI document used as +// the source of truth for built-in resource type descriptions and schemas. +func loadRadiusCoreOpenAPI() (*openAPIDocument, error) { + contents, err := swagger.SpecFiles.ReadFile(radiusCoreOpenAPIFile) + if err != nil { + return nil, fmt.Errorf("failed to read embedded Radius.Core OpenAPI spec: %w", err) + } + + var doc openAPIDocument + if err := json.Unmarshal(contents, &doc); err != nil { + return nil, fmt.Errorf("failed to parse embedded Radius.Core OpenAPI spec: %w", err) + } + if len(doc.Definitions) == 0 { + return nil, fmt.Errorf("embedded Radius.Core OpenAPI spec has no definitions") + } + + return &doc, nil +} + +// openAPIDefinitionDescription returns the top-level resource description for a named OpenAPI definition. +func openAPIDefinitionDescription(doc *openAPIDocument, name string) (string, error) { + definition, ok := doc.Definitions[name] + if !ok { + return "", fmt.Errorf("definition %q not found", name) + } + + description, _ := definition["description"].(string) + if description == "" { + return "", fmt.Errorf("definition %q has no description", name) + } + + return description, nil +} + +// openAPIDefinitionSchema returns a fully local-ref-expanded schema for a named OpenAPI definition. +func openAPIDefinitionSchema(doc *openAPIDocument, name string) (map[string]any, error) { + definition, ok := doc.Definitions[name] + if !ok { + return nil, fmt.Errorf("definition %q not found", name) + } + + resolved, err := resolveOpenAPIValue(definition, doc.Definitions, map[string]bool{}) + if err != nil { + return nil, err + } + + schema, ok := resolved.(map[string]any) + if !ok { + return nil, fmt.Errorf("definition %q did not resolve to an object schema", name) + } + + return schema, nil +} + +// resolveOpenAPIValue recursively expands local #/definitions references while preserving external +// references, which are part of the ARM envelope rather than the resource-type property schema. +func resolveOpenAPIValue(value any, definitions map[string]map[string]any, resolving map[string]bool) (any, error) { + switch typed := value.(type) { + case map[string]any: + if ref, ok := typed["$ref"].(string); ok { + if !strings.HasPrefix(ref, openAPIDefinitionRefRoot) { + return cloneMap(typed), nil + } + + definitionName := strings.TrimPrefix(ref, openAPIDefinitionRefRoot) + if resolving[definitionName] { + return nil, fmt.Errorf("circular OpenAPI reference %q", ref) + } + + definition, ok := definitions[definitionName] + if !ok { + return nil, fmt.Errorf("OpenAPI reference %q not found", ref) + } + + resolving[definitionName] = true + resolved, err := resolveOpenAPIValue(definition, definitions, resolving) + delete(resolving, definitionName) + if err != nil { + return nil, err + } + + resolvedMap, ok := resolved.(map[string]any) + if !ok { + return nil, fmt.Errorf("OpenAPI reference %q did not resolve to an object schema", ref) + } + + for key, child := range typed { + if key == "$ref" { + continue + } + + resolvedChild, err := resolveOpenAPIValue(child, definitions, resolving) + if err != nil { + return nil, err + } + resolvedMap[key] = resolvedChild + } + + return resolvedMap, nil + } + + result := map[string]any{} + for key, child := range typed { + resolvedChild, err := resolveOpenAPIValue(child, definitions, resolving) + if err != nil { + return nil, err + } + result[key] = resolvedChild + } + return result, nil + case []any: + result := make([]any, len(typed)) + for i, child := range typed { + resolvedChild, err := resolveOpenAPIValue(child, definitions, resolving) + if err != nil { + return nil, err + } + result[i] = resolvedChild + } + return result, nil + default: + return typed, nil + } +} + +// cloneMap returns a shallow copy so unresolved external refs are not mutated while local refs expand. +func cloneMap(value map[string]any) map[string]any { + result := map[string]any{} + for key, child := range value { + result[key] = child + } + return result +} diff --git a/pkg/ucp/initializer/service.go b/pkg/ucp/initializer/service.go index 8a2d5a9fc5..6a90954cb6 100644 --- a/pkg/ucp/initializer/service.go +++ b/pkg/ucp/initializer/service.go @@ -121,6 +121,10 @@ func (w *Service) Run(ctx context.Context) error { // Register each merged provider. for _, rp := range merged { logger.Info("Registering manifest", "namespace", rp.Namespace, "types", len(rp.Types)) + if err := hydrateBuiltInResourceProviderMetadata(rp); err != nil { + return fmt.Errorf("failed to hydrate built-in resource provider metadata for namespace %s: %w", rp.Namespace, err) + } + if err := registerResourceProviderDirect(ctx, dbClient, "local", *rp); err != nil { return fmt.Errorf("failed to register manifest for namespace %s: %w", rp.Namespace, err) } diff --git a/pkg/ucp/initializer/service_test.go b/pkg/ucp/initializer/service_test.go index 6db9209776..6c1ef98414 100644 --- a/pkg/ucp/initializer/service_test.go +++ b/pkg/ucp/initializer/service_test.go @@ -163,6 +163,7 @@ func Test_registerResourceProviderDirect(t *testing.T) { require.NoError(t, err) require.NotNil(t, obj) }) + } func Test_Run(t *testing.T) { @@ -294,6 +295,159 @@ types: assert.Contains(t, location.Properties.ResourceTypes, "containers") assert.Contains(t, location.Properties.ResourceTypes, "routes") }) + + t.Run("hydrates Radius.Core schemas from embedded OpenAPI", func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + manifestPath := filepath.Join("..", "..", "..", "deploy", "manifest", "built-in-providers", "dev", "radius_core.yaml") + manifestContent, err := os.ReadFile(manifestPath) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "radius_core.yaml"), manifestContent, 0600) + require.NoError(t, err) + + svc := newTestService(tempDir) + dbClient, err := svc.options.DatabaseProvider.GetClient(context.Background()) + require.NoError(t, err) + + err = svc.Run(context.Background()) + require.NoError(t, err) + + obj, err := dbClient.Get(context.Background(), "/planes/radius/local/providers/System.Resources/resourceProviderSummaries/Radius.Core") + require.NoError(t, err) + + summaryModel := &datamodel.ResourceProviderSummary{} + require.NoError(t, obj.As(summaryModel)) + require.Len(t, summaryModel.Properties.ResourceTypes, len(radiusCoreTypeOpenAPIDefinitions)) + + expectedDescriptions := map[string]string{ + "applications": "Radius Application resource", + "bicepConfigs": "The Bicep configuration resource, providing reusable Bicep recipe settings for environments.", + "environments": "The environment resource", + "recipePacks": "The recipe pack resource", + "terraformConfigs": "The Terraform configuration resource, providing reusable Terraform recipe settings for environments.", + } + require.Len(t, expectedDescriptions, len(radiusCoreTypeOpenAPIDefinitions)) + for typeName, expectedDescription := range expectedDescriptions { + resourceType := summaryModel.Properties.ResourceTypes[typeName] + require.NotNil(t, resourceType, "resource type %q should be registered", typeName) + require.NotNil(t, resourceType.Description, "resource type %q should have a description", typeName) + assert.Equal(t, expectedDescription, *resourceType.Description) + + apiVersion := resourceType.APIVersions["2025-08-01-preview"] + require.NotNil(t, apiVersion, "resource type %q should have API version 2025-08-01-preview", typeName) + assert.Equal(t, "object", apiVersion.Schema["type"]) + requireRenderableResourceTypeSchema(t, apiVersion.Schema) + } + + applications := summaryModel.Properties.ResourceTypes["applications"] + applicationSchema := applications.APIVersions["2025-08-01-preview"].Schema + + applicationProperties := requireSchemaProperties(t, applicationSchema) + environmentProperty := requireSchemaProperty(t, applicationProperties, "environment") + assert.Equal(t, "string", environmentProperty["type"]) + + statusProperty := requireSchemaProperty(t, applicationProperties, "status") + assert.NotContains(t, statusProperty, "$ref") + assert.Equal(t, "object", statusProperty["type"]) + + environments := summaryModel.Properties.ResourceTypes["environments"] + environmentSchema := environments.APIVersions["2025-08-01-preview"].Schema + environmentProperties := requireSchemaProperties(t, environmentSchema) + providersProperty := requireSchemaProperty(t, environmentProperties, "providers") + assert.NotContains(t, providersProperty, "$ref") + assert.Equal(t, "object", providersProperty["type"]) + + recipePacks := summaryModel.Properties.ResourceTypes["recipePacks"] + recipePackSchema := recipePacks.APIVersions["2025-08-01-preview"].Schema + recipePackProperties := requireSchemaProperties(t, recipePackSchema) + recipesProperty := requireSchemaProperty(t, recipePackProperties, "recipes") + additionalProperties, ok := recipesProperty["additionalProperties"].(map[string]any) + require.True(t, ok) + recipeDefinitionProperties := requireSchemaProperties(t, additionalProperties) + recipeKindProperty := requireSchemaProperty(t, recipeDefinitionProperties, "recipeKind") + assert.NotContains(t, recipeKindProperty, "$ref") + assert.Equal(t, "string", recipeKindProperty["type"]) + + terraformConfigs := summaryModel.Properties.ResourceTypes["terraformConfigs"] + terraformConfigSchema := terraformConfigs.APIVersions["2025-08-01-preview"].Schema + terraformConfigProperties := requireSchemaProperties(t, terraformConfigSchema) + terraformrcProperty := requireSchemaProperty(t, terraformConfigProperties, "terraformrc") + assert.NotContains(t, terraformrcProperty, "$ref") + assert.Equal(t, "object", terraformrcProperty["type"]) + + bicepConfigs := summaryModel.Properties.ResourceTypes["bicepConfigs"] + bicepConfigSchema := bicepConfigs.APIVersions["2025-08-01-preview"].Schema + bicepConfigProperties := requireSchemaProperties(t, bicepConfigSchema) + registryAuthenticationsProperty := requireSchemaProperty(t, bicepConfigProperties, "registryAuthentications") + assert.NotContains(t, registryAuthenticationsProperty, "$ref") + assert.Equal(t, "object", registryAuthenticationsProperty["type"]) + + obj, err = dbClient.Get(context.Background(), "/planes/radius/local/providers/System.Resources/resourceProviders/Radius.Core/resourceTypes/applications/apiVersions/2025-08-01-preview") + require.NoError(t, err) + + apiVersionModel := &datamodel.APIVersion{} + require.NoError(t, obj.As(apiVersionModel)) + assert.Equal(t, applicationSchema, apiVersionModel.Properties.Schema) + }) +} + +func Test_hydrateBuiltInResourceProviderMetadata(t *testing.T) { + t.Parallel() + + t.Run("fails when mapped Radius.Core type is missing expected API version", func(t *testing.T) { + t.Parallel() + + rp := &manifest.ResourceProvider{ + Namespace: "Radius.Core", + Types: map[string]*manifest.ResourceType{ + "applications": { + APIVersions: map[string]*manifest.ResourceTypeAPIVersion{ + "2024-01-01-preview": {Schema: map[string]any{}}, + }, + }, + }, + } + + err := hydrateBuiltInResourceProviderMetadata(rp) + require.Error(t, err) + require.Contains(t, err.Error(), "mapped Radius.Core type applications is missing API version 2025-08-01-preview") + }) + + t.Run("fails when Radius.Core manifest type has no OpenAPI metadata mapping", func(t *testing.T) { + t.Parallel() + + rp := &manifest.ResourceProvider{ + Namespace: "Radius.Core", + Types: map[string]*manifest.ResourceType{ + "widgets": { + APIVersions: map[string]*manifest.ResourceTypeAPIVersion{ + "2025-08-01-preview": {Schema: map[string]any{}}, + }, + }, + }, + } + + err := hydrateBuiltInResourceProviderMetadata(rp) + require.Error(t, err) + require.Contains(t, err.Error(), "Radius.Core type widgets has no OpenAPI metadata mapping") + }) + + t.Run("ignores non Radius.Core providers", func(t *testing.T) { + t.Parallel() + + rp := &manifest.ResourceProvider{ + Namespace: "MyCompany.Resources", + Types: map[string]*manifest.ResourceType{ + "applications": { + APIVersions: map[string]*manifest.ResourceTypeAPIVersion{}, + }, + }, + } + + err := hydrateBuiltInResourceProviderMetadata(rp) + require.NoError(t, err) + }) } func Test_saveResource(t *testing.T) { @@ -374,3 +528,67 @@ func createTestResourceProviderMultiType() manifest.ResourceProvider { }, } } + +func requireSchemaProperties(t *testing.T, schema map[string]any) map[string]any { + t.Helper() + + properties, ok := schema["properties"].(map[string]any) + require.True(t, ok) + require.NotEmpty(t, properties) + return properties +} + +func requireSchemaProperty(t *testing.T, properties map[string]any, name string) map[string]any { + t.Helper() + + property, ok := properties[name].(map[string]any) + require.True(t, ok) + require.NotEmpty(t, property) + return property +} + +func requireRenderableResourceTypeSchema(t *testing.T, schema map[string]any) { + t.Helper() + + properties := requireSchemaProperties(t, schema) + for name, property := range properties { + propertySchema, ok := property.(map[string]any) + require.True(t, ok, "property %q should be an object schema", name) + require.NotContains(t, propertySchema, "$ref", "property %q should have expanded refs", name) + require.IsType(t, "", propertySchema["type"], "property %q should have a concrete type", name) + + if propertySchema["type"] == "object" { + requireRenderableNestedObjectSchema(t, name, propertySchema) + } + } +} + +func requireRenderableNestedObjectSchema(t *testing.T, path string, schema map[string]any) { + t.Helper() + + nestedSchema := schema + if _, ok := nestedSchema["properties"].(map[string]any); !ok { + additionalProperties, ok := schema["additionalProperties"].(map[string]any) + if !ok { + return + } + nestedSchema = additionalProperties + } + + properties, ok := nestedSchema["properties"].(map[string]any) + if !ok { + return + } + require.NotEmpty(t, properties) + for name, property := range properties { + propertyPath := path + "." + name + propertySchema, ok := property.(map[string]any) + require.True(t, ok, "property %q should be an object schema", propertyPath) + require.NotContains(t, propertySchema, "$ref", "property %q should have expanded refs", propertyPath) + require.IsType(t, "", propertySchema["type"], "property %q should have a concrete type", propertyPath) + + if propertySchema["type"] == "object" { + requireRenderableNestedObjectSchema(t, propertyPath, propertySchema) + } + } +} From 67c19e22b36d2b97f79f80622c7ae4b099c9e6fb Mon Sep 17 00:00:00 2001 From: Karishma Chawla Date: Tue, 19 May 2026 11:59:03 -0700 Subject: [PATCH 07/13] Add workflow to sync contrib resource types and publish Bicep extensions (#11916) ## Overview Today the `radius` Bicep extension is published to `biceptypes.azurecr.io` via the existing [`build-and-push-bicep-types`](https://github.com/radius-project/radius/blob/main/.github/workflows/build.yaml#L356) job in `build.yaml`, which dispatches to the `radius-publisher` pipeline on every push to `main` and on version tag pushes. With [#11915](https://github.com/radius-project/radius/pull/11915) updating `make generate-bicep-types` to include contrib types, the existing publish pipeline automatically produces the combined extension -- no new publish workflow is needed. However, there is no automation to pull updated resource type manifests from `resource-types-contrib` into this repo. When someone merges a schema change or a new resource type in contrib, the manifest copies committed under `deploy/manifest/built-in-providers/` must be refreshed manually via `make update-resource-types` before the next publish picks them up. This PR adds a workflow that closes that gap by automating the manifest sync. ## How it works ``` resource-types-contrib merges to main | +--> notify-radius.yaml (contrib repo, PR 4) fires repository_dispatch | +--> contrib-update-resource-types.yaml (this PR) receives dispatch | +--> Runs 'make update-resource-types' to refresh manifest copies +--> Opens/updates PR on bot/update-resource-types branch | +--> Human reviews and merges the PR | +--> Push to main triggers build.yaml's existing build-and-push-bicep-types job | +--> Dispatches to radius-publisher +--> radius-publisher runs make generate-bicep-types (now includes contrib) and publishes radius:latest to biceptypes.azurecr.io ``` ## What this PR adds ### `contrib-update-resource-types.yaml` Handles `repository_dispatch` events (type: `resource-types-contrib-updated`) from `resource-types-contrib`. **Triggers:** - `repository_dispatch` -- fired by the contrib repo's `notify-radius.yaml` workflow (PR 4) - `workflow_dispatch` -- commented out for production, can be enabled during development **Steps:** 1. Validates `contrib_ref` as a hex commit SHA (informational only -- the actual version fetched is determined by `make update-resource-types` which runs `go get ...@latest`) 2. Installs yq (required by `make update-resource-types` to parse `defaults.yaml`) 3. Runs `make update-resource-types` to bump `go.mod` to latest contrib and copy manifests 4. If changes are detected (using `git status --porcelain` to catch both modified and new untracked files), opens or updates a PR on the `bot/update-resource-types` branch 5. Merging that PR triggers the existing publish pipeline to republish `radius:latest` **Security:** - `contrib_ref` is validated against `^[a-f0-9]{7,40}$` and passed via environment variables (not inline `${{ }}` interpolation) to prevent shell and script injection - Uses `GH_RAD_CI_BOT_PAT` for checkout and PR creation so the resulting push triggers CI checks (the default `GITHUB_TOKEN` cannot trigger workflows on pushes it creates) **Note:** This workflow depends on `make update-resource-types` from [#11911](https://github.com/radius-project/radius/pull/11911). It includes a pre-flight check that fails fast with a descriptive error if the target is not yet available. ## Dependencies - [Integrate contrib types into unified Bicep extension](https://github.com/radius-project/radius/pull/11915) - [Automated default resource type registration](https://github.com/radius-project/radius/pull/11911) (provides `make update-resource-types`) - Required secret: `GH_RAD_CI_BOT_PAT` ## Changes - `.github/workflows/contrib-update-resource-types.yaml`: New workflow ## Part of Unified Bicep extension publishing (PR 3/4). See [design doc](https://github.com/radius-project/radius/pull/11892). --------- Signed-off-by: Karishma Chawla Co-authored-by: Nicole James <101607760+nicolejms@users.noreply.github.com> Signed-off-by: Zach Casper --- .../contrib-update-resource-types.yaml | 226 ++++++++++++++++++ .github/workflows/functional-test-cloud.yaml | 5 +- .../workflows/functional-test-noncloud.yaml | 5 +- .github/workflows/lint.yaml | 5 +- .github/workflows/publish-docs.yaml | 5 +- .github/workflows/verify-resource-types.yaml | 8 +- 6 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/contrib-update-resource-types.yaml diff --git a/.github/workflows/contrib-update-resource-types.yaml b/.github/workflows/contrib-update-resource-types.yaml new file mode 100644 index 0000000000..174662fba7 --- /dev/null +++ b/.github/workflows/contrib-update-resource-types.yaml @@ -0,0 +1,226 @@ +# ------------------------------------------------------------ +# Copyright 2026 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +--- +name: contrib-update-resource-types + +# Triggered via `repository_dispatch` from radius-project/resource-types-contrib +# whenever its `main` branch updates. Opens (or refreshes) a PR that runs +# `make update-resource-types` so the manifest copies committed under +# deploy/manifest/built-in-providers/ stay in sync with the contrib repo. +# +# Merging the resulting PR triggers the existing `build-and-push-bicep-types` +# job in build.yaml, which dispatches to azure-octo/radius-publisher to +# republish `radius:latest` with the refreshed contrib types. +# +# See the design note: +# eng/design-notes/extensibility/2026-05-unified-bicep-extension-publishing.md + +on: + repository_dispatch: + types: + - resource-types-contrib-updated + # workflow_dispatch: # Enable during development for manual testing + # inputs: + # contrib_ref: + # description: "Optional resource-types-contrib commit SHA the update is based on." + # required: false + # default: "" + +permissions: {} + +concurrency: + # Only one in-flight refresh PR at a time. + group: contrib-update-resource-types + cancel-in-progress: false + +env: + PR_BRANCH: bot/update-resource-types + CONTRIB_REPO: radius-project/resource-types-contrib + YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 + +jobs: + open-update-pr: + name: Open update-resource-types PR + if: github.repository == 'radius-project/radius' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read # actions/checkout + steps: + - name: Resolve contrib ref + # Extracts the contrib commit SHA from either the dispatch payload or + # workflow_dispatch input. Validates it as a hex SHA to prevent injection. + # Falls back to "main" if not provided. Note: this value is informational + # only (used in commit messages and PR body). The actual version fetched + # is determined by 'make update-resource-types' which runs 'go get ...@latest'. + id: contrib + env: + DISPATCH_REF: ${{ github.event.client_payload.contrib_ref }} + INPUT_REF: ${{ github.event.inputs.contrib_ref }} + run: | + set -euo pipefail + ref="${DISPATCH_REF:-${INPUT_REF:-}}" + if [ -z "${ref}" ]; then + ref="main" + elif ! [[ "${ref}" =~ ^[a-f0-9]{7,40}$ ]]; then + echo "ERROR: contrib_ref must be a hex commit SHA, got '${ref}'" + exit 1 + fi + echo "ref=${ref}" >> "${GITHUB_OUTPUT}" + echo "Contrib ref: ${ref}" + + - name: Generate App Token + # Uses a GitHub App token instead of the default GITHUB_TOKEN so that + # the bot can push the PR branch and the resulting PR triggers CI checks. + # The default GITHUB_TOKEN cannot trigger workflows on pushes it creates. + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RESOURCE_TYPES_BOT_CLIENT_ID }} + private-key: ${{ secrets.RESOURCE_TYPES_BOT_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: | + radius + permission-metadata: read + permission-contents: write + permission-pull-requests: write + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + + - name: Set up Git identity + # Configure git author for the automated commit on the PR branch. + run: | + git config user.name "Radius CI Bot" + git config user.email "radiuscoreteam@service.microsoft.com" + + - name: Setup Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + cache: true + + - name: Install yq + # Required by make update-resource-types / sync-resource-types to parse + # deploy/manifest/defaults.yaml. + run: | + mkdir -p "${RUNNER_TEMP}/bin" + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" + echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" + + - name: Run make update-resource-types + # Bumps go.mod to the latest resource-types-contrib version and copies + # the manifest files listed in defaults.yaml into + # deploy/manifest/built-in-providers/{dev,self-hosted}/. + run: | + set -euo pipefail + if ! make -n update-resource-types >/dev/null 2>&1; then + echo "ERROR: 'make update-resource-types' target is not yet defined." >&2 + echo "This workflow depends on the target introduced by the default-registration design" >&2 + echo "(eng/design-notes/extensibility/2026-04-automated-default-resource-type-registration.md)." >&2 + exit 1 + fi + make update-resource-types + + - name: Detect changes + # Uses git status --porcelain (not git diff --quiet) because + # make update-resource-types may create new untracked files when new + # resource types are added to defaults.yaml. git diff only detects + # modifications to already-tracked files. + id: changes + run: | + set -euo pipefail + if [ -z "$(git status --porcelain)" ]; then + echo "changed=false" >> "${GITHUB_OUTPUT}" + echo "No changes from 'make update-resource-types'; nothing to do." + else + echo "changed=true" >> "${GITHUB_OUTPUT}" + fi + + - name: Create or update PR branch + # Force-pushes to the bot branch so that if a previous automated PR is + # still open, its content is replaced with the latest manifests rather + # than accumulating stale commits. + if: steps.changes.outputs.changed == 'true' + env: + CONTRIB_REF: ${{ steps.contrib.outputs.ref }} + run: | + set -euo pipefail + git checkout -B "${PR_BRANCH}" + git add -A + git commit -m "chore: refresh resource type manifests from ${CONTRIB_REPO} (${CONTRIB_REF})" + git push --force origin "${PR_BRANCH}" + + - name: Open or update pull request + # Creates a new PR if none exists for the bot branch, or updates the + # existing one's title and body. Uses github-script because we need + # conditional create-or-update logic that gh pr create doesn't support. + if: steps.changes.outputs.changed == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + CONTRIB_REF: ${{ steps.contrib.outputs.ref }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const branch = process.env.PR_BRANCH; + const contribRef = process.env.CONTRIB_REF; + const title = "chore: refresh resource type manifests from resource-types-contrib"; + const body = [ + "Automated refresh of resource type manifests under `deploy/manifest/built-in-providers/`.", + "", + `Triggered by an update to [\`${process.env.CONTRIB_REPO}\`](https://github.com/${process.env.CONTRIB_REPO}) at ref \`${contribRef}\`.`, + "", + "Merging this PR will republish `br:biceptypes.azurecr.io/radius:latest` via the existing `build-and-push-bicep-types` job in `build.yaml`.", + ].join("\n"); + + const { owner, repo } = context.repo; + const { data: existing } = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branch}`, + state: "open", + }); + + if (existing.length === 0) { + const { data: pr } = await github.rest.pulls.create({ + owner, + repo, + title, + head: branch, + base: "main", + body, + }); + core.notice(`Opened PR #${pr.number}: ${pr.html_url}`); + } else { + const pr = existing[0]; + await github.rest.pulls.update({ + owner, + repo, + pull_number: pr.number, + title, + body, + }); + core.notice(`Refreshed existing PR #${pr.number}: ${pr.html_url}`); + } diff --git a/.github/workflows/functional-test-cloud.yaml b/.github/workflows/functional-test-cloud.yaml index f7d3c7ba73..45f6be2f62 100644 --- a/.github/workflows/functional-test-cloud.yaml +++ b/.github/workflows/functional-test-cloud.yaml @@ -53,6 +53,7 @@ env: GOTESTSUM_VER: 1.13.0 # yq version YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 # Helm version HELM_VER: v3.19.4 # KinD cluster version @@ -452,7 +453,9 @@ jobs: # Required by make generate-bicep-types-contrib to parse defaults.yaml. run: | mkdir -p "${RUNNER_TEMP}/bin" - GOBIN="${RUNNER_TEMP}/bin" go install github.com/mikefarah/yq/v4@${{ env.YQ_VERSION }} + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" - name: Generate Bicep extensibility types from OpenAPI specs diff --git a/.github/workflows/functional-test-noncloud.yaml b/.github/workflows/functional-test-noncloud.yaml index f77dcb3485..a69fed177a 100644 --- a/.github/workflows/functional-test-noncloud.yaml +++ b/.github/workflows/functional-test-noncloud.yaml @@ -49,6 +49,7 @@ env: GOTESTSUM_VER: 1.13.0 # yq version YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 # Helm version HELM_VER: v3.19.4 # KinD cluster version @@ -243,7 +244,9 @@ jobs: # Required by make generate-bicep-types-contrib to parse defaults.yaml. run: | mkdir -p "${RUNNER_TEMP}/bin" - GOBIN="${RUNNER_TEMP}/bin" go install github.com/mikefarah/yq/v4@${{ env.YQ_VERSION }} + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" - name: Generate Bicep extensibility types from OpenAPI specs diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d3df728d46..8c12ec5b88 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -32,6 +32,7 @@ concurrency: env: YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 permissions: {} @@ -82,7 +83,9 @@ jobs: # Required by make generate-bicep-types-contrib to parse defaults.yaml. run: | mkdir -p "${RUNNER_TEMP}/bin" - GOBIN="${RUNNER_TEMP}/bin" go install github.com/mikefarah/yq/v4@${{ env.YQ_VERSION }} + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" - name: Install helm diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index 9ae88ead25..8bd5ba2af2 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -26,6 +26,7 @@ permissions: {} env: YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 jobs: changes: @@ -105,7 +106,9 @@ jobs: # Required by make generate-bicep-types-contrib to parse defaults.yaml. run: | mkdir -p "${RUNNER_TEMP}/bin" - GOBIN="${RUNNER_TEMP}/bin" go install github.com/mikefarah/yq/v4@${{ env.YQ_VERSION }} + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" # Generate Bicep docs diff --git a/.github/workflows/verify-resource-types.yaml b/.github/workflows/verify-resource-types.yaml index c945959991..209ee8af89 100644 --- a/.github/workflows/verify-resource-types.yaml +++ b/.github/workflows/verify-resource-types.yaml @@ -54,6 +54,10 @@ on: permissions: {} +env: + YQ_VERSION: v4.44.3 + YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7 + concurrency: group: verify-resource-types-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true @@ -77,7 +81,9 @@ jobs: - name: Install yq run: | mkdir -p "${RUNNER_TEMP}/bin" - GOBIN="${RUNNER_TEMP}/bin" go install github.com/mikefarah/yq/v4@v4.44.3 + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq" + echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c - + chmod +x "${RUNNER_TEMP}/bin/yq" echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}" # Re-run the copy step (without bumping the version) to regenerate the From f4729115daeb68cb4db402ebccb6e61e65972af4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 12:53:47 -0700 Subject: [PATCH 08/13] build(deps): bump github.com/go-git/go-git/v5 from 5.19.0 to 5.19.1 (#11939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.0 to 5.19.1.
Release notes

Sourced from github.com/go-git/go-git/v5's releases.

v5.19.1

What's Changed

Full Changelog: https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1

Commits
  • 3c3be60 Merge pull request #2137 from go-git/validate-v5
  • 3fba897 plumbing: format/packfile, cap delta chain depth in parser
  • a97d660 Merge pull request #2125 from hiddeco/v5/format-input-bounds
  • aeaa125 plumbing: format/objfile, require Header before Read
  • 1f38e17 plumbing: format/packfile, bound inflate size
  • f7545a0 plumbing: format/idxfile, bound nr by file size
  • 170b881 Merge pull request #2116 from pjbgf/symlink-v5
  • 7b6d994 Merge pull request #2117 from hiddeco/v5/worktree-fs-mkdirall-root-noop
  • f0709b3 git: Stop validating symlink target paths
  • 776d00f git: Allow MkdirAll on worktree-root paths
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Zach Casper --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4f11508fe8..a2e606ef2a 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/fluxcd/source-controller/api v1.8.4 github.com/getkin/kin-openapi v0.138.0 github.com/go-chi/chi/v5 v5.2.5 - github.com/go-git/go-git/v5 v5.19.0 + github.com/go-git/go-git/v5 v5.19.1 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/go-openapi/errors v0.22.7 diff --git a/go.sum b/go.sum index bb2619420b..07f5aa1dc5 100644 --- a/go.sum +++ b/go.sum @@ -303,8 +303,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= -github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= From e44e17a94159b7ca40d7bef245ac535bfd366bfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 14:00:18 -0700 Subject: [PATCH 09/13] build(deps): bump the npm-dependencies group across 3 directories with 5 updates (#11956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm-dependencies group with 5 updates in the /hack/bicep-types-radius/src/autorest.bicep directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.7.0` | `25.9.1` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.59.3` | `8.59.4` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.59.3` | `8.59.4` | | [eslint](https://github.com/eslint/eslint) | `10.3.0` | `10.4.0` | | [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.4.9` | `29.4.10` | Bumps the npm-dependencies group with 4 updates in the /hack/bicep-types-radius/src/generator directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [eslint](https://github.com/eslint/eslint). Bumps the npm-dependencies group with 1 update in the /typespec directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.7.0 to 25.9.1
Commits

Updates `@typescript-eslint/eslint-plugin` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/eslint-plugin's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/eslint-plugin's changelog.

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits
  • ca6ca14 chore(release): publish 8.59.4
  • 4302433 fix(eslint-plugin): [no-floating-promises] stack overflow when using recursiv...
  • 10b79f1 chore(deps): update dependency eslint to v10.4.0 (#12339)
  • 2a6765d chore: clenaup getAwaitedType from typescript.d.ts (#12302)
  • See full diff in compare view

Updates `@typescript-eslint/parser` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/parser's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/parser's changelog.

8.59.4 (2026-05-18)

This was a version bump only for parser to align it with other projects, there were no code changes.

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits

Updates `eslint` from 10.3.0 to 10.4.0
Release notes

Sourced from eslint's releases.

v10.4.0

Features

  • 1a45ec5 feat: check sequence expressions in for-direction (#20701) (kuldeep kumar)
  • 450040b feat: add includeIgnoreFile() to eslint/config (#20735) (Kirk Waiblinger)

Bug Fixes

  • 544c0c3 fix: escape code path DOT labels in debug output (#20866) (Pixel998)
  • 6799431 fix: update dependency @​eslint/config-helpers to ^0.6.0 (#20850) (renovate[bot])
  • f078fef fix: handle non-array deprecated rule replacements (#20825) (xbinaryx)

Documentation

  • 7e52a71 docs: add mention of @eslint-react/eslint-plugin (#20869) (Pavel)
  • db3468b docs: tweak wording around ambiguous CJS-vs-ESM config (#20865) (Kirk Waiblinger)
  • 9084664 docs: Update README (GitHub Actions Bot)
  • 9cc7387 docs: Update README (GitHub Actions Bot)
  • 3d7b548 docs: Update README (GitHub Actions Bot)
  • 191ec3c docs: Update README (GitHub Actions Bot)

Chores

  • 6616856 chore: upgrade knip to v6 (#20875) (Pixel998)
  • d13b084 ci: ensure auto-created PRs run CI (#20860) (lumir)
  • e71c7af ci: bump pnpm/action-setup from 6.0.5 to 6.0.7 (#20862) (dependabot[bot])
  • d84393d test: add unit tests for SuppressionsService.applySuppressions() (#20863) (kuldeep kumar)
  • 24db8cb test: add tests for SuppressionsService.save() (#20802) (kuldeep kumar)
  • 2ef0549 chore: update ecosystem plugins (#20857) (github-actions[bot])
  • a429791 ci: remove eslint-webpack-plugin types integration test (#20668) (Milos Djermanovic)
  • 9e37386 chore: replace recast with range approach in code-sample-minimizer (#20682) (Copilot)
  • 0dd1f9f test: disable warning for vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER (#20845) (Francesco Trotta)
  • 9da3c7b refactor: remove deprecated meta.language and migrate meta.dialects (#20716) (Pixel998)
  • 2099ed1 refactor: add meta.defaultOptions to more rules, enable linting (#20800) (xbinaryx)
  • f1dfbc9 chore: update ecosystem plugins (#20836) (github-actions[bot])
  • c759413 ci: bump pnpm/action-setup from 6.0.3 to 6.0.5 (#20843) (dependabot[bot])
  • 5b817d6 test: add unit tests for lib/shared/ast-utils (#20838) (kuldeep kumar)
  • 1c13ae3 test: add unit tests for lib/shared/severity (#20835) (kuldeep kumar)
Commits

Updates `ts-jest` from 29.4.9 to 29.4.10
Release notes

Sourced from ts-jest's releases.

v29.4.10

Please refer to CHANGELOG.md for details.

Changelog

Sourced from ts-jest's changelog.

29.4.10 (2026-05-18)

Bug Fixes

  • pass resolutionMode to ts.resolveModuleName for hybrid module support (b557a85)
  • rebuild Program when consecutive compiles need different module kinds (a82a2b3), closes #4774
  • respect tsconfig moduleResolution instead of forcing Node10 (1bffffc)
  • transformer: transpile mjs files from node_modules for CJS mode (96d025d)
  • transformer: use a consistent comparator in hoist-jest sortStatements (8a8fd2f)
Commits
  • 96b3ac0 chore(release): 29.4.10
  • e98ec64 build(deps): update github/codeql-action digest to 458d36d
  • 21ac58f build(deps): update jest packages
  • 0fdc96d build(deps): update dependency semver to ^7.8.0
  • 4b95551 build(deps): update dependency jest-environment-jsdom to ^30.4.1 (#5311)
  • 7b88447 build(deps): update eslint packages to ^8.59.3 (#5310)
  • a82a2b3 fix: rebuild Program when consecutive compiles need different module kinds
  • 6edad68 build(deps): update dependency @​types/node to v20.19.41
  • 8c443a6 build(deps): update google/osv-scanner-action action to v2.3.8
  • 3195ba6 build(deps): bump @​babel/plugin-transform-modules-systemjs in /website
  • Additional commits viewable in compare view
Attestation changes

This version has no provenance attestation, while the previous version (29.4.9) was attested. Review the package versions before updating.


Updates `@types/node` from 25.7.0 to 25.9.1
Commits

Updates `@typescript-eslint/eslint-plugin` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/eslint-plugin's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/eslint-plugin's changelog.

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits
  • ca6ca14 chore(release): publish 8.59.4
  • 4302433 fix(eslint-plugin): [no-floating-promises] stack overflow when using recursiv...
  • 10b79f1 chore(deps): update dependency eslint to v10.4.0 (#12339)
  • 2a6765d chore: clenaup getAwaitedType from typescript.d.ts (#12302)
  • See full diff in compare view

Updates `@typescript-eslint/parser` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/parser's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/parser's changelog.

8.59.4 (2026-05-18)

This was a version bump only for parser to align it with other projects, there were no code changes.

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits

Updates `eslint` from 10.3.0 to 10.4.0
Release notes

Sourced from eslint's releases.

v10.4.0

Features

  • 1a45ec5 feat: check sequence expressions in for-direction (#20701) (kuldeep kumar)
  • 450040b feat: add includeIgnoreFile() to eslint/config (#20735) (Kirk Waiblinger)

Bug Fixes

  • 544c0c3 fix: escape code path DOT labels in debug output (#20866) (Pixel998)
  • 6799431 fix: update dependency @​eslint/config-helpers to ^0.6.0 (#20850) (renovate[bot])
  • f078fef fix: handle non-array deprecated rule replacements (#20825) (xbinaryx)

Documentation

  • 7e52a71 docs: add mention of @eslint-react/eslint-plugin (#20869) (Pavel)
  • db3468b docs: tweak wording around ambiguous CJS-vs-ESM config (#20865) (Kirk Waiblinger)
  • 9084664 docs: Update README (GitHub Actions Bot)
  • 9cc7387 docs: Update README (GitHub Actions Bot)
  • 3d7b548 docs: Update README (GitHub Actions Bot)
  • 191ec3c docs: Update README (GitHub Actions Bot)

Chores

  • 6616856 chore: upgrade knip to v6 (#20875) (Pixel998)
  • d13b084 ci: ensure auto-created PRs run CI (#20860) (lumir)
  • e71c7af ci: bump pnpm/action-setup from 6.0.5 to 6.0.7 (#20862) (dependabot[bot])
  • d84393d test: add unit tests for SuppressionsService.applySuppressions() (#20863) (kuldeep kumar)
  • 24db8cb test: add tests for SuppressionsService.save() (#20802) (kuldeep kumar)
  • 2ef0549 chore: update ecosystem plugins (#20857) (github-actions[bot])
  • a429791 ci: remove eslint-webpack-plugin types integration test (#20668) (Milos Djermanovic)
  • 9e37386 chore: replace recast with range approach in code-sample-minimizer (#20682) (Copilot)
  • 0dd1f9f test: disable warning for vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER (#20845) (Francesco Trotta)
  • 9da3c7b refactor: remove deprecated meta.language and migrate meta.dialects (#20716) (Pixel998)
  • 2099ed1 refactor: add meta.defaultOptions to more rules, enable linting (#20800) (xbinaryx)
  • f1dfbc9 chore: update ecosystem plugins (#20836) (github-actions[bot])
  • c759413 ci: bump pnpm/action-setup from 6.0.3 to 6.0.5 (#20843) (dependabot[bot])
  • 5b817d6 test: add unit tests for lib/shared/ast-utils (#20838) (kuldeep kumar)
  • 1c13ae3 test: add unit tests for lib/shared/severity (#20835) (kuldeep kumar)
Commits

Updates `@types/node` from 25.7.0 to 25.9.1
Commits

Updates `@typescript-eslint/eslint-plugin` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/eslint-plugin's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/eslint-plugin's changelog.

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits
  • ca6ca14 chore(release): publish 8.59.4
  • 4302433 fix(eslint-plugin): [no-floating-promises] stack overflow when using recursiv...
  • 10b79f1 chore(deps): update dependency eslint to v10.4.0 (#12339)
  • 2a6765d chore: clenaup getAwaitedType from typescript.d.ts (#12302)
  • See full diff in compare view

Updates `@typescript-eslint/parser` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/parser's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/parser's changelog.

8.59.4 (2026-05-18)

This was a version bump only for parser to align it with other projects, there were no code changes.

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits

Updates `eslint` from 10.3.0 to 10.4.0
Release notes

Sourced from eslint's releases.

v10.4.0

Features

  • 1a45ec5 feat: check sequence expressions in for-direction (#20701) (kuldeep kumar)
  • 450040b feat: add includeIgnoreFile() to eslint/config (#20735) (Kirk Waiblinger)

Bug Fixes

  • 544c0c3 fix: escape code path DOT labels in debug output (#20866) (Pixel998)
  • 6799431 fix: update dependency @​eslint/config-helpers to ^0.6.0 (#20850) (renovate[bot])
  • f078fef fix: handle non-array deprecated rule replacements (#20825) (xbinaryx)

Documentation

  • 7e52a71 docs: add mention of @eslint-react/eslint-plugin (#20869) (Pavel)
  • db3468b docs: tweak wording around ambiguous CJS-vs-ESM config (#20865) (Kirk Waiblinger)
  • 9084664 docs: Update README (GitHub Actions Bot)
  • 9cc7387 docs: Update README (GitHub Actions Bot)
  • 3d7b548 docs: Update README (GitHub Actions Bot)
  • 191ec3c docs: Update README (GitHub Actions Bot)

Chores

  • 6616856 chore: upgrade knip to v6 (#20875) (Pixel998)
  • d13b084 ci: ensure auto-created PRs run CI (#20860) (lumir)
  • e71c7af ci: bump pnpm/action-setup from 6.0.5 to 6.0.7 (#20862) (dependabot[bot])
  • d84393d test: add unit tests for SuppressionsService.applySuppressions() (#20863) (kuldeep kumar)
  • 24db8cb test: add tests for SuppressionsService.save() (#20802) (kuldeep kumar)
  • 2ef0549 chore: update ecosystem plugins (#20857) (github-actions[bot])
  • a429791 ci: remove eslint-webpack-plugin types integration test (#20668) (Milos Djermanovic)
  • 9e37386 chore: replace recast with range approach in code-sample-minimizer (#20682) (Copilot)
  • 0dd1f9f test: disable warning for vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER (#20845) (Francesco Trotta)
  • 9da3c7b refactor: remove deprecated meta.language and migrate meta.dialects (#20716) (Pixel998)
  • 2099ed1 refactor: add meta.defaultOptions to more rules, enable linting (#20800) (xbinaryx)
  • f1dfbc9 chore: update ecosystem plugins (#20836) (github-actions[bot])
  • c759413 ci: bump pnpm/action-setup from 6.0.3 to 6.0.5 (#20843) (dependabot[bot])
  • 5b817d6 test: add unit tests for lib/shared/ast-utils (#20838) (kuldeep kumar)
  • 1c13ae3 test: add unit tests for lib/shared/severity (#20835) (kuldeep kumar)
Commits

Updates `@types/node` from 25.7.0 to 25.9.1
Commits

Updates `@typescript-eslint/eslint-plugin` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/eslint-plugin's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/eslint-plugin's changelog.

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits
  • ca6ca14 chore(release): publish 8.59.4
  • 4302433 fix(eslint-plugin): [no-floating-promises] stack overflow when using recursiv...
  • 10b79f1 chore(deps): update dependency eslint to v10.4.0 (#12339)
  • 2a6765d chore: clenaup getAwaitedType from typescript.d.ts (#12302)
  • See full diff in compare view

Updates `@typescript-eslint/parser` from 8.59.3 to 8.59.4
Release notes

Sourced from @​typescript-eslint/parser's releases.

v8.59.4

8.59.4 (2026-05-18)

🩹 Fixes

  • eslint-plugin: [no-floating-promises] stack overflow when using recursive types (#12294)
  • project-service: throw error cause in getParsedConfigFileFromTSServer (#12321)
  • typescript-eslint: export Compatible* types from typescript-eslint to resolve pnpm TS error (#12340)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/parser's changelog.

8.59.4 (2026-05-18)

This was a version bump only for parser to align it with other projects, there were no code changes.

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits

Updates `eslint` from 10.3.0 to 10.4.0
Release notes

Sourced from eslint's releases.

v10.4.0

Features

  • 1a45ec5 feat: check sequence expressions in for-direction (#20701) (kuldeep kumar)
  • 450040b feat: add includeIgnoreFile() to eslint/config (#20735) (Kirk Waiblinger)

Bug Fixes

  • 544c0c3 fix: escape code path DOT labels in debug output (#20866) (Pixel998)
  • 6799431 fix: update dependency @​eslint/config-helpers to ^0.6.0 (#20850) (renovate[bot])
  • f078fef fix: handle non-array deprecated rule replacements (#20825) (xbinaryx)

Documentation

  • 7e52a71 docs: add mention of @eslint-react/eslint-plugin (#20869) (Pavel)
  • db3468b docs: tweak wording around ambiguous CJS-vs-ESM config (#20865) (Kirk Waiblinger)
  • 9084664 docs: Update README (GitHub Actions Bot)
  • 9cc7387 docs: Update README (GitHub Actions Bot)
  • 3d7b548 docs: Update README (GitHub Actions Bot)
  • 191ec3c docs: Update README (GitHub Actions Bot)

Chores