Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions pkg/cli/clients/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
)
Expand Down Expand Up @@ -69,3 +70,52 @@ func Is404Error(err error) bool {

return false
}

// ConvertAzureErrorResponse converts Azure SDK's ErrorResponse to Radius ErrorDetails.
// This function handles nested error details recursively.
func ConvertAzureErrorResponse(azErr *armresources.ErrorResponse) *v1.ErrorDetails {
if azErr == nil {
return nil
}

errDetails := &v1.ErrorDetails{}

// Convert basic fields
if azErr.Code != nil {
errDetails.Code = *azErr.Code
}
if azErr.Message != nil {
errDetails.Message = *azErr.Message
}
if azErr.Target != nil {
errDetails.Target = *azErr.Target
}

// Convert additional info if present
if len(azErr.AdditionalInfo) > 0 {
errDetails.AdditionalInfo = make([]*v1.ErrorAdditionalInfo, len(azErr.AdditionalInfo))
for i, info := range azErr.AdditionalInfo {
additionalInfo := &v1.ErrorAdditionalInfo{}
if info.Type != nil {
additionalInfo.Type = *info.Type
}
if info.Info != nil {
// Info is an 'any' type from Azure SDK, we need to handle it carefully
if infoMap, ok := info.Info.(map[string]any); ok {
additionalInfo.Info = infoMap
}
}
errDetails.AdditionalInfo[i] = additionalInfo
}
}

// Recursively convert nested error details
if len(azErr.Details) > 0 {
errDetails.Details = make([]*v1.ErrorDetails, len(azErr.Details))
for i, detail := range azErr.Details {
errDetails.Details[i] = ConvertAzureErrorResponse(detail)
}
}

return errDetails
}
138 changes: 138 additions & 0 deletions pkg/cli/clients/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import (
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/to"
"github.com/stretchr/testify/require"
)

func TestIs404Error(t *testing.T) {
Expand Down Expand Up @@ -69,3 +72,138 @@ func TestIs404Error(t *testing.T) {
t.Errorf("Expected Is404Error to return true for fake server not found response, but it returned false")
}
}

func TestConvertAzureErrorResponse(t *testing.T) {
t.Run("nil error response", func(t *testing.T) {
result := ConvertAzureErrorResponse(nil)
require.Nil(t, result)
})

t.Run("simple error without nested details", func(t *testing.T) {
azErr := &armresources.ErrorResponse{
Code: to.Ptr("DeploymentFailed"),
Message: to.Ptr("The deployment failed"),
Target: to.Ptr("resource"),
}

result := ConvertAzureErrorResponse(azErr)
require.NotNil(t, result)
require.Equal(t, "DeploymentFailed", result.Code)
require.Equal(t, "The deployment failed", result.Message)
require.Equal(t, "resource", result.Target)
require.Empty(t, result.Details)
require.Empty(t, result.AdditionalInfo)
})

t.Run("error with nested details", func(t *testing.T) {
azErr := &armresources.ErrorResponse{
Code: to.Ptr("DeploymentFailed"),
Message: to.Ptr("The deployment failed"),
Details: []*armresources.ErrorResponse{
{
Code: to.Ptr("InvalidTemplate"),
Message: to.Ptr("Template validation failed"),
},
{
Code: to.Ptr("ResourceNotFound"),
Message: to.Ptr("Resource does not exist"),
},
},
}

result := ConvertAzureErrorResponse(azErr)
require.NotNil(t, result)
require.Equal(t, "DeploymentFailed", result.Code)
require.Equal(t, "The deployment failed", result.Message)
require.Len(t, result.Details, 2)
require.Equal(t, "InvalidTemplate", result.Details[0].Code)
require.Equal(t, "Template validation failed", result.Details[0].Message)
require.Equal(t, "ResourceNotFound", result.Details[1].Code)
require.Equal(t, "Resource does not exist", result.Details[1].Message)
})

t.Run("error with additional info", func(t *testing.T) {
azErr := &armresources.ErrorResponse{
Code: to.Ptr("DeploymentFailed"),
Message: to.Ptr("The deployment failed"),
AdditionalInfo: []*armresources.ErrorAdditionalInfo{
{
Type: to.Ptr("PolicyViolation"),
Info: map[string]any{
"policyName": "RequiredTags",
"severity": "High",
},
},
},
}

result := ConvertAzureErrorResponse(azErr)
require.NotNil(t, result)
require.Equal(t, "DeploymentFailed", result.Code)
require.Len(t, result.AdditionalInfo, 1)
require.Equal(t, "PolicyViolation", result.AdditionalInfo[0].Type)
require.Equal(t, "RequiredTags", result.AdditionalInfo[0].Info["policyName"])
require.Equal(t, "High", result.AdditionalInfo[0].Info["severity"])
})

t.Run("deeply nested error details", func(t *testing.T) {
azErr := &armresources.ErrorResponse{
Code: to.Ptr("DeploymentFailed"),
Message: to.Ptr("The deployment failed"),
Details: []*armresources.ErrorResponse{
{
Code: to.Ptr("InvalidTemplate"),
Message: to.Ptr("Template validation failed"),
Details: []*armresources.ErrorResponse{
{
Code: to.Ptr("MissingParameter"),
Message: to.Ptr("Required parameter not provided"),
},
},
},
},
}

result := ConvertAzureErrorResponse(azErr)
require.NotNil(t, result)
require.Equal(t, "DeploymentFailed", result.Code)
require.Len(t, result.Details, 1)
require.Equal(t, "InvalidTemplate", result.Details[0].Code)
require.Len(t, result.Details[0].Details, 1)
require.Equal(t, "MissingParameter", result.Details[0].Details[0].Code)
require.Equal(t, "Required parameter not provided", result.Details[0].Details[0].Message)
})

t.Run("error with all fields populated", func(t *testing.T) {
azErr := &armresources.ErrorResponse{
Code: to.Ptr("DeploymentFailed"),
Message: to.Ptr("The deployment failed"),
Target: to.Ptr("Microsoft.Resources/deployments"),
AdditionalInfo: []*armresources.ErrorAdditionalInfo{
{
Type: to.Ptr("PolicyViolation"),
Info: map[string]any{
"policyName": "RequiredTags",
},
},
},
Details: []*armresources.ErrorResponse{
{
Code: to.Ptr("InvalidTemplate"),
Message: to.Ptr("Template validation failed"),
Target: to.Ptr("template"),
},
},
}

result := ConvertAzureErrorResponse(azErr)
require.NotNil(t, result)
require.Equal(t, "DeploymentFailed", result.Code)
require.Equal(t, "The deployment failed", result.Message)
require.Equal(t, "Microsoft.Resources/deployments", result.Target)
require.Len(t, result.AdditionalInfo, 1)
require.Len(t, result.Details, 1)
require.Equal(t, "InvalidTemplate", result.Details[0].Code)
require.Equal(t, "template", result.Details[0].Target)
})
}
58 changes: 56 additions & 2 deletions pkg/cli/cmd/app/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/radius-project/radius/pkg/cli/objectformats"
"github.com/radius-project/radius/pkg/cli/output"
"github.com/radius-project/radius/pkg/cli/workspaces"
corerp "github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -110,8 +111,9 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
// Run runs the `rad app list` command.
//

// Run() creates an ApplicationsManagementClient using the provided ConnectionFactory, then lists the applications and
// writes the output in the specified format, returning an error if any of these steps fail.
// Run() creates an ApplicationsManagementClient using the provided ConnectionFactory, then lists the applications,
// filters them by the default environment if one is set, and writes the output in the specified format,
// returning an error if any of these steps fail.
func (r *Runner) Run(ctx context.Context) error {
client, err := r.ConnectionFactory.CreateApplicationsManagementClient(ctx, *r.Workspace)
if err != nil {
Expand All @@ -123,5 +125,57 @@ func (r *Runner) Run(ctx context.Context) error {
return err
}

// Filter applications by the default environment if one is configured in the workspace
if r.Workspace.Environment != "" {
apps = filterApplicationsByEnvironment(apps, r.Workspace.Environment)
}

return r.Output.WriteFormatted(r.Format, apps, objectformats.GetResourceTableFormat())
}

// filterApplicationsByEnvironment filters the list of applications to only include those
// associated with the specified environment. The environment can be either a resource ID
// or just an environment name.
func filterApplicationsByEnvironment(apps []corerp.ApplicationResource, environment string) []corerp.ApplicationResource {
var filtered []corerp.ApplicationResource

for _, app := range apps {
if app.Properties != nil && app.Properties.Environment != nil {
// Match either by full resource ID or by environment name at the end of the ID
if matchesEnvironment(*app.Properties.Environment, environment) {
filtered = append(filtered, app)
}
}
}

return filtered
}

// matchesEnvironment checks if the application's environment matches the filter environment.
// It handles both full resource IDs and simple environment names.
func matchesEnvironment(appEnvironment, filterEnvironment string) bool {
// Direct match (full resource ID)
if appEnvironment == filterEnvironment {
return true
}

// Check if the filter environment appears at the end of the app's environment resource ID
// This handles cases where the workspace has just the name but the app has the full ID
if len(appEnvironment) > len(filterEnvironment) {
// Check if it ends with /environments/{name}
suffix := "/" + filterEnvironment
if len(appEnvironment) >= len(suffix) && appEnvironment[len(appEnvironment)-len(suffix):] == suffix {
return true
}
}

// Check if the app environment ends with the filter (for cases where filter is a full ID but app is shorter)
if len(filterEnvironment) > len(appEnvironment) {
suffix := "/" + appEnvironment
if len(filterEnvironment) >= len(suffix) && filterEnvironment[len(filterEnvironment)-len(suffix):] == suffix {
return true
}
}

return false
}
Loading