Skip to content

Commit 2b66ff8

Browse files
officialasishkumarclaudewilldavsmithnicolejmsnithyatsu
authored
fix(cli): improve error message when deploying Radius types with wrong API version (#11557)
# Description When a Bicep template uses a Radius resource type with an unsupported API version (e.g., `Radius.Core/applications@2023-10-01-preview`), the Bicep compiler cannot resolve the type and treats it as a passthrough ARM resource. The deployment engine then routes it through Azure, producing a misleading error: ``` "Azure deployment failed, please ensure you have configured an Azure provider with your Radius environment" ``` This is confusing because the user intended to deploy a Radius resource, not an Azure resource. The actual problem is that the API version is wrong for the given resource type namespace. This change detects the mismatch between the error (Azure-related) and the template contents (Radius-only resource types) and wraps the error with a clear, actionable message that suggests checking the API version for each resource type. After this change, users will see: ``` Deployment failed with an Azure provider error, but the template only contains Radius resource types. This commonly happens when a resource type is used with an incorrect or unsupported API version, causing it to be misrouted. Please verify that each resource uses a supported API version. For example, Radius.Core types use API version '2025-08-01-preview' and Applications.Core types use '2023-10-01-preview'. ``` ### Changes - **`pkg/cli/bicep/resources.go`**: Add `ExtractResourceTypes`, `IsRadiusResourceType`, and `HasOnlyRadiusResourceTypes` helpers for template resource type analysis - **`pkg/cli/cmd/deploy/deploy.go`**: Add `addDeploymentErrorContext` to detect and contextualize misleading Azure errors for Radius-only templates - **`pkg/cli/bicep/resources_test.go`**: Add comprehensive unit tests for resource type extraction and classification - **`pkg/cli/cmd/deploy/deploy_test.go`**: Add unit tests for the error context wrapping logic ## Type of change - This pull request fixes a bug in Radius and has an approved issue (issue link required). Fixes: #11462 ## 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 <!-- TaskRadio schema --> - [x] Not applicable <!-- TaskRadio schema --> - 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 <!-- TaskRadio design-pr --> - [x] Not applicable <!-- TaskRadio design-pr --> - The design document has been reviewed and approved by Radius maintainers/approvers. - [ ] Yes <!-- TaskRadio design-review --> - [x] Not applicable <!-- TaskRadio design-review --> - 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 <!-- TaskRadio samples-pr --> - [x] Not applicable <!-- TaskRadio samples-pr --> - 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 <!-- TaskRadio docs-pr --> - [x] Not applicable <!-- TaskRadio docs-pr --> - 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 <!-- TaskRadio recipes-pr --> - [x] Not applicable <!-- TaskRadio recipes-pr --> --------- Signed-off-by: Asish Kumar <officialasishkumar@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Will Smith <willdavsmith@gmail.com> Co-authored-by: Nicole James <101607760+nicolejms@users.noreply.github.com> Co-authored-by: Nithya Subramanian <98416062+nithyatsu@users.noreply.github.com>
1 parent ec789e8 commit 2b66ff8

4 files changed

Lines changed: 593 additions & 1 deletion

File tree

‎pkg/cli/bicep/resources.go‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,105 @@ const (
2828
legacyEnvironmentResourceType = "applications.core/environments"
2929
)
3030

31+
// radiusNamespacePatterns lists namespace prefixes for resource type namespaces that belong to Radius.
32+
// Resource types matching these prefixes are routed through the Radius control plane, not Azure ARM.
33+
var radiusNamespacePatterns = []string{
34+
"Applications.*",
35+
"Radius.*",
36+
}
37+
3138
// TemplateInspectionResult contains the results of inspecting a Bicep template's resources.
3239
type TemplateInspectionResult struct {
3340
// ContainsEnvironmentResource indicates whether the template contains an environment resource.
3441
ContainsEnvironmentResource bool
3542
}
3643

44+
// ResourceTypeEntry represents a parsed resource type from a compiled Bicep/ARM template.
45+
type ResourceTypeEntry struct {
46+
// FullType is the full type string including API version, e.g. "Radius.Core/applications@2025-08-01-preview".
47+
FullType string
48+
49+
// Type is the resource type without the API version, e.g. "Radius.Core/applications".
50+
Type string
51+
52+
// APIVersion is the API version, e.g. "2025-08-01-preview".
53+
APIVersion string
54+
}
55+
56+
// ExtractResourceTypes extracts all resource types and their API versions from a compiled Bicep/ARM template.
57+
func ExtractResourceTypes(template map[string]any) []ResourceTypeEntry {
58+
if template == nil {
59+
return nil
60+
}
61+
62+
resourcesValue, ok := template["resources"]
63+
if !ok {
64+
return nil
65+
}
66+
67+
resources, ok := resourcesValue.(map[string]any)
68+
if !ok {
69+
return nil
70+
}
71+
72+
var entries []ResourceTypeEntry
73+
for _, resourceValue := range resources {
74+
resource, ok := resourceValue.(map[string]any)
75+
if !ok {
76+
continue
77+
}
78+
79+
resourceType, ok := resource["type"].(string)
80+
if !ok || resourceType == "" {
81+
continue
82+
}
83+
84+
entry := ResourceTypeEntry{FullType: resourceType}
85+
if idx := strings.Index(resourceType, "@"); idx >= 0 {
86+
entry.Type = resourceType[:idx]
87+
entry.APIVersion = resourceType[idx+1:]
88+
} else {
89+
entry.Type = resourceType
90+
}
91+
92+
entries = append(entries, entry)
93+
}
94+
95+
return entries
96+
}
97+
98+
// IsRadiusResourceType returns true if the given resource type belongs to a known Radius namespace.
99+
// It matches against the patterns in radiusNamespacePatterns (e.g. "Applications.*", "Radius.*")
100+
// using case-insensitive prefix matching on the namespace portion before the dot and slash.
101+
func IsRadiusResourceType(resourceType string) bool {
102+
lower := strings.ToLower(resourceType)
103+
for _, pattern := range radiusNamespacePatterns {
104+
// Convert pattern like "Applications.*" to a lowercase prefix like "applications."
105+
prefix := strings.ToLower(strings.TrimSuffix(pattern, "*"))
106+
if strings.HasPrefix(lower, prefix) {
107+
return true
108+
}
109+
}
110+
return false
111+
}
112+
113+
// HasOnlyRadiusResourceTypes returns true if the template contains at least one resource and
114+
// all resources belong to known Radius namespaces (none are Azure ARM or other non-Radius types).
115+
func HasOnlyRadiusResourceTypes(template map[string]any) bool {
116+
entries := ExtractResourceTypes(template)
117+
if len(entries) == 0 {
118+
return false
119+
}
120+
121+
for _, entry := range entries {
122+
if !IsRadiusResourceType(entry.Type) {
123+
return false
124+
}
125+
}
126+
127+
return true
128+
}
129+
37130
// InspectTemplateResources inspects the compiled Radius Bicep template's resources to find
38131
// environment resources and determine if any environment resource is present.
39132
//

‎pkg/cli/bicep/resources_test.go‎

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,235 @@ func Test_ContainsEnvironmentResource(t *testing.T) {
302302
}
303303
}
304304

305+
func Test_ExtractResourceTypes(t *testing.T) {
306+
tests := []struct {
307+
name string
308+
template map[string]any
309+
expected []ResourceTypeEntry
310+
}{
311+
{
312+
name: "Nil template",
313+
template: nil,
314+
expected: nil,
315+
},
316+
{
317+
name: "Empty template",
318+
template: map[string]any{},
319+
expected: nil,
320+
},
321+
{
322+
name: "Template with no resources field",
323+
template: map[string]any{
324+
"parameters": map[string]any{},
325+
},
326+
expected: nil,
327+
},
328+
{
329+
name: "Template with single resource",
330+
template: map[string]any{
331+
"resources": map[string]any{
332+
"app": map[string]any{
333+
"type": "Radius.Core/applications@2025-08-01-preview",
334+
"name": "my-app",
335+
},
336+
},
337+
},
338+
expected: []ResourceTypeEntry{
339+
{
340+
FullType: "Radius.Core/applications@2025-08-01-preview",
341+
Type: "Radius.Core/applications",
342+
APIVersion: "2025-08-01-preview",
343+
},
344+
},
345+
},
346+
{
347+
name: "Template with resource missing API version",
348+
template: map[string]any{
349+
"resources": map[string]any{
350+
"app": map[string]any{
351+
"type": "Radius.Core/applications",
352+
"name": "my-app",
353+
},
354+
},
355+
},
356+
expected: []ResourceTypeEntry{
357+
{
358+
FullType: "Radius.Core/applications",
359+
Type: "Radius.Core/applications",
360+
APIVersion: "",
361+
},
362+
},
363+
},
364+
{
365+
name: "Template with invalid resource format",
366+
template: map[string]any{
367+
"resources": map[string]any{
368+
"app": "not a map",
369+
},
370+
},
371+
expected: nil,
372+
},
373+
{
374+
name: "Template with resource missing type",
375+
template: map[string]any{
376+
"resources": map[string]any{
377+
"app": map[string]any{
378+
"name": "my-app",
379+
},
380+
},
381+
},
382+
expected: nil,
383+
},
384+
}
385+
386+
for _, tt := range tests {
387+
t.Run(tt.name, func(t *testing.T) {
388+
result := ExtractResourceTypes(tt.template)
389+
require.Equal(t, tt.expected, result)
390+
})
391+
}
392+
}
393+
394+
func Test_IsRadiusResourceType(t *testing.T) {
395+
tests := []struct {
396+
name string
397+
resourceType string
398+
expected bool
399+
}{
400+
{
401+
name: "Applications.Core type",
402+
resourceType: "Applications.Core/applications",
403+
expected: true,
404+
},
405+
{
406+
name: "Radius.Core type",
407+
resourceType: "Radius.Core/environments",
408+
expected: true,
409+
},
410+
{
411+
name: "Applications.Dapr type",
412+
resourceType: "Applications.Dapr/pubSubBrokers",
413+
expected: true,
414+
},
415+
{
416+
name: "Applications wildcard namespace",
417+
resourceType: "Applications.Networking/gateways",
418+
expected: true,
419+
},
420+
{
421+
name: "Applications.Datastores type",
422+
resourceType: "Applications.Datastores/redisCaches",
423+
expected: true,
424+
},
425+
{
426+
name: "Applications.Messaging type",
427+
resourceType: "Applications.Messaging/rabbitMQQueues",
428+
expected: true,
429+
},
430+
{
431+
name: "Case insensitive match",
432+
resourceType: "applications.core/containers",
433+
expected: true,
434+
},
435+
{
436+
name: "Azure type",
437+
resourceType: "Microsoft.Storage/storageAccounts",
438+
expected: false,
439+
},
440+
{
441+
name: "AWS type",
442+
resourceType: "AWS.S3/Bucket",
443+
expected: false,
444+
},
445+
{
446+
name: "Empty string",
447+
resourceType: "",
448+
expected: false,
449+
},
450+
}
451+
452+
for _, tt := range tests {
453+
t.Run(tt.name, func(t *testing.T) {
454+
result := IsRadiusResourceType(tt.resourceType)
455+
require.Equal(t, tt.expected, result)
456+
})
457+
}
458+
}
459+
460+
func Test_HasOnlyRadiusResourceTypes(t *testing.T) {
461+
tests := []struct {
462+
name string
463+
template map[string]any
464+
expected bool
465+
}{
466+
{
467+
name: "Nil template",
468+
template: nil,
469+
expected: false,
470+
},
471+
{
472+
name: "Empty resources",
473+
template: map[string]any{
474+
"resources": map[string]any{},
475+
},
476+
expected: false,
477+
},
478+
{
479+
name: "Only Radius types",
480+
template: map[string]any{
481+
"resources": map[string]any{
482+
"app": map[string]any{
483+
"type": "Radius.Core/applications@2025-08-01-preview",
484+
},
485+
"env": map[string]any{
486+
"type": "Applications.Core/environments@2023-10-01-preview",
487+
},
488+
},
489+
},
490+
expected: true,
491+
},
492+
{
493+
name: "Mixed Radius and Azure types",
494+
template: map[string]any{
495+
"resources": map[string]any{
496+
"app": map[string]any{
497+
"type": "Applications.Core/applications@2023-10-01-preview",
498+
},
499+
"storage": map[string]any{
500+
"type": "Microsoft.Storage/storageAccounts@2021-01-01",
501+
},
502+
},
503+
},
504+
expected: false,
505+
},
506+
{
507+
name: "Only Azure types",
508+
template: map[string]any{
509+
"resources": map[string]any{
510+
"storage": map[string]any{
511+
"type": "Microsoft.Storage/storageAccounts@2021-01-01",
512+
},
513+
},
514+
},
515+
expected: false,
516+
},
517+
{
518+
name: "Radius type with wrong API version",
519+
template: map[string]any{
520+
"resources": map[string]any{
521+
"app": map[string]any{
522+
"type": "Radius.Core/applications@2023-10-01-preview",
523+
},
524+
},
525+
},
526+
expected: true,
527+
},
528+
}
529+
530+
for _, tt := range tests {
531+
t.Run(tt.name, func(t *testing.T) {
532+
result := HasOnlyRadiusResourceTypes(tt.template)
533+
require.Equal(t, tt.expected, result)
534+
})
535+
}
536+
}

0 commit comments

Comments
 (0)