diff --git a/cli/azd/extensions/extension.schema.json b/cli/azd/extensions/extension.schema.json index 97784f836f5..2a8340e59ff 100644 --- a/cli/azd/extensions/extension.schema.json +++ b/cli/azd/extensions/extension.schema.json @@ -112,7 +112,7 @@ "capabilities": { "type": "array", "title": "Capabilities", - "description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, provisioning-provider, metadata. Select one or more from the allowed list. Each value must be unique. Not required for extension packs, which declare dependencies instead and have no executable.", + "description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, provisioning-provider, validation-provider, metadata. Select one or more from the allowed list. Each value must be unique. Not required for extension packs, which declare dependencies instead and have no executable.", "minItems": 1, "uniqueItems": true, "items": { @@ -153,6 +153,12 @@ "title": "Provisioning Provider", "description": "Provisioning provider enables extensions to provide a custom infrastructure provisioning experience." }, + { + "type": "string", + "const": "validation-provider", + "title": "Validation Provider", + "description": "Validation provider enables extensions to contribute checks to azd validation pipelines." + }, { "type": "string", "const": "metadata", diff --git a/cli/azd/extensions/microsoft.azd.demo/ci-test.ps1 b/cli/azd/extensions/microsoft.azd.demo/ci-test.ps1 index db87a8a2ceb..5042111638d 100644 --- a/cli/azd/extensions/microsoft.azd.demo/ci-test.ps1 +++ b/cli/azd/extensions/microsoft.azd.demo/ci-test.ps1 @@ -1,4 +1,8 @@ -Write-Host "Run test command(s) here..." - -# Exit 0 for success, nonzero exit for failure -exit 0 +Write-Host "Running unit tests..." +go test ./... -count=1 + +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +exit 0 diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go index 8b476eee273..dad328203e1 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go @@ -28,77 +28,8 @@ func newListenCommand() *cobra.Command { } defer azdClient.Close() - host := azdext.NewExtensionHost(azdClient). - WithServiceTarget("demo", func() azdext.ServiceTargetProvider { - return project.NewDemoServiceTargetProvider(azdClient) - }). - WithFrameworkService("rust", func() azdext.FrameworkServiceProvider { - return project.NewDemoFrameworkServiceProvider(azdClient) - }). - WithProvisioningProvider("demo", func() azdext.ProvisioningProvider { - return project.NewDemoProvisioningProvider(azdClient) - }). - WithValidationCheck(azdext.ValidationCheckRegistration{ - // Bicep-only check: runs during BicepProvider preflight and - // receives the Bicep snapshot / ARM template context. It is - // skipped gracefully when no snapshot is available (e.g. a - // non-Bicep provider), but is not dead code for Bicep. - CheckType: azdext.ValidationCheckTypeLocalPreflight, - RuleID: "demo_warning", - Factory: func() azdext.ValidationCheckProvider { - return project.NewDemoValidationCheck() - }, - }). - WithValidationCheck(azdext.ValidationCheckRegistration{ - // Provider-agnostic check: runs before provisioning for every - // provider (Bicep, Terraform, and extension providers such as - // this demo provider). Receives the lean provision context. - CheckType: azdext.ValidationCheckTypeProvision, - RuleID: "demo_provision_warning", - Factory: func() azdext.ValidationCheckProvider { - return project.NewDemoProvisionValidationCheck() - }, - }). - WithProjectEventHandler("preprovision", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important work in extension...\n", i) - time.Sleep(250 * time.Millisecond) - } - - return nil - }). - WithProjectEventHandler("predeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important predeploy project work in extension...\n", i) - time.Sleep(250 * time.Millisecond) - } - - return nil - }). - WithProjectEventHandler("postdeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important postdeploy project work in extension...\n", i) - time.Sleep(250 * time.Millisecond) - } - - return nil - }). - WithServiceEventHandler("prepackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("Service: %s, Artifacts: %d\n", args.Service.Name, len(args.ServiceContext.Package)) - time.Sleep(250 * time.Millisecond) - } - - return nil - }, nil). - WithServiceEventHandler("postpackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { - for i := 1; i <= 20; i++ { - fmt.Printf("Service: %s, Artifacts: %d\n", args.Service.Name, len(args.ServiceContext.Package)) - time.Sleep(250 * time.Millisecond) - } - - return nil - }, nil) + host := azdext.NewExtensionHost(azdClient) + configureExtensionHost(host) // Start listening for events // This is a blocking call and will not return until the server connection is closed. @@ -112,3 +43,81 @@ func newListenCommand() *cobra.Command { return cmd } + +// configureExtensionHost wires the demo extension's providers and event handlers onto +// the supplied host, so tests can verify the registrations against extension.yaml. +func configureExtensionHost(host *azdext.ExtensionHost) { + azdClient := host.Client() + + host. + WithServiceTarget("demo", func() azdext.ServiceTargetProvider { + return project.NewDemoServiceTargetProvider(azdClient) + }). + WithFrameworkService("rust", func() azdext.FrameworkServiceProvider { + return project.NewDemoFrameworkServiceProvider(azdClient) + }). + WithProvisioningProvider("demo", func() azdext.ProvisioningProvider { + return project.NewDemoProvisioningProvider(azdClient) + }). + WithValidationCheck(azdext.ValidationCheckRegistration{ + // Bicep-only check: runs during BicepProvider preflight and + // receives the Bicep snapshot / ARM template context. It is + // skipped gracefully when no snapshot is available (e.g. a + // non-Bicep provider), but is not dead code for Bicep. + CheckType: azdext.ValidationCheckTypeLocalPreflight, + RuleID: "demo_warning", + Factory: func() azdext.ValidationCheckProvider { + return project.NewDemoValidationCheck() + }, + }). + WithValidationCheck(azdext.ValidationCheckRegistration{ + // Provider-agnostic check: runs before provisioning for every + // provider (Bicep, Terraform, and extension providers such as + // this demo provider). Receives the lean provision context. + CheckType: azdext.ValidationCheckTypeProvision, + RuleID: "demo_provision_warning", + Factory: func() azdext.ValidationCheckProvider { + return project.NewDemoProvisionValidationCheck() + }, + }). + WithProjectEventHandler("preprovision", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). + WithProjectEventHandler("predeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important predeploy project work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). + WithProjectEventHandler("postdeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important postdeploy project work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). + WithServiceEventHandler("prepackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("Service: %s, Artifacts: %d\n", args.Service.Name, len(args.ServiceContext.Package)) + time.Sleep(250 * time.Millisecond) + } + + return nil + }, nil). + WithServiceEventHandler("postpackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("Service: %s, Artifacts: %d\n", args.Service.Name, len(args.ServiceContext.Package)) + time.Sleep(250 * time.Millisecond) + } + + return nil + }, nil) +} diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/providers_manifest_test.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/providers_manifest_test.go new file mode 100644 index 00000000000..2588ad49b9c --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/providers_manifest_test.go @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/stretchr/testify/require" +) + +// TestConfigureExtensionHostMatchesManifest verifies that the providers this +// extension registers match those declared in its extension.yaml. +func TestConfigureExtensionHostMatchesManifest(t *testing.T) { + manifestPath := filepath.Join("..", "..", "extension.yaml") + require.NoError(t, azdext.VerifyProvidersMatchManifest(configureExtensionHost, manifestPath)) +} diff --git a/cli/azd/extensions/registry.schema.json b/cli/azd/extensions/registry.schema.json index 464834349e6..5ab7d22be42 100644 --- a/cli/azd/extensions/registry.schema.json +++ b/cli/azd/extensions/registry.schema.json @@ -81,6 +81,7 @@ "service-target-provider", "framework-service-provider", "provisioning-provider", + "validation-provider", "metadata" ] } diff --git a/cli/azd/pkg/azdext/extension_host.go b/cli/azd/pkg/azdext/extension_host.go index 760469bdb63..8c234abdedf 100644 --- a/cli/azd/pkg/azdext/extension_host.go +++ b/cli/azd/pkg/azdext/extension_host.go @@ -9,6 +9,7 @@ import ( "fmt" "log" "os" + "slices" "strconv" "sync" @@ -127,6 +128,18 @@ func (er *ExtensionHost) Client() *AzdClient { return er.client } +// ServiceTargets returns a copy of the service target providers registered so far, +// letting tests and tooling introspect registrations without invoking Run. +func (er *ExtensionHost) ServiceTargets() []ServiceTargetRegistration { + return slices.Clone(er.serviceTargets) +} + +// ProvisioningProviders returns a copy of the provisioning providers registered so +// far. See [ExtensionHost.ServiceTargets]. +func (er *ExtensionHost) ProvisioningProviders() []ProvisioningProviderRegistration { + return slices.Clone(er.provisioningProviders) +} + func (er *ExtensionHost) initManagers(extensionId string, brokerLogger *log.Logger) { if er.serviceTargetManager == nil { er.serviceTargetManager = NewServiceTargetManager(extensionId, er.client, brokerLogger) diff --git a/cli/azd/pkg/azdext/provider_manifest.go b/cli/azd/pkg/azdext/provider_manifest.go new file mode 100644 index 00000000000..d55a488dc0d --- /dev/null +++ b/cli/azd/pkg/azdext/provider_manifest.go @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "fmt" + "os" + "slices" + "strings" + + "github.com/azure/azure-dev/cli/azd/pkg/extensions" + "gopkg.in/yaml.v3" +) + +// manifestProviderDoc is the minimal subset of an extension.yaml manifest needed to +// compare declared providers against what an extension registers at runtime. +type manifestProviderDoc struct { + Providers []extensions.Provider `yaml:"providers"` +} + +// manifestComparedProviderTypes is the set of provider types representable in a +// manifest's `providers:` list. Framework service and validation providers are +// registered in code only, so they are excluded. +var manifestComparedProviderTypes = []extensions.ProviderType{ + extensions.ServiceTargetProviderType, + extensions.ProvisioningProviderType, +} + +// VerifyProvidersMatchManifest asserts that the providers an extension registers via +// the supplied configure callback exactly match the providers declared in its +// extension.yaml manifest at manifestPath. +// +// It runs configure against a bare [ExtensionHost] (no azd connection; provider +// factories are never invoked) and compares the registered names against the +// manifest's `providers:` list. Only service-target and provisioning-provider types +// are compared; framework-service and validation registrations have no manifest +// representation. +// +// It returns a descriptive error when a provider is declared but not registered, +// registered but not declared, or listed more than once on either side. +func VerifyProvidersMatchManifest(configure func(host *ExtensionHost), manifestPath string) error { + if configure == nil { + return fmt.Errorf("configure callback must not be nil") + } + + declared, err := loadManifestProviders(manifestPath) + if err != nil { + return err + } + + host := NewExtensionHost(nil) + // Verification depends on provider registration remaining lazy so configure + // can safely record names without invoking factories against a nil azd client. + configure(host) + + registered := map[extensions.ProviderType][]string{} + for _, reg := range host.ServiceTargets() { + registered[extensions.ServiceTargetProviderType] = append( + registered[extensions.ServiceTargetProviderType], reg.Host) + } + for _, reg := range host.ProvisioningProviders() { + registered[extensions.ProvisioningProviderType] = append( + registered[extensions.ProvisioningProviderType], reg.Name) + } + + var mismatches []string + for _, providerType := range manifestComparedProviderTypes { + declaredNames := declared[providerType] + registeredNames := registered[providerType] + declaredDuplicateKeys := duplicateNameKeys(declaredNames) + registeredDuplicateKeys := duplicateNameKeys(registeredNames) + + for _, name := range duplicatedNames(declaredNames) { + mismatches = append(mismatches, fmt.Sprintf( + "provider %q of type %q is declared more than once in %s", + name, providerType, manifestPath)) + } + for _, name := range duplicatedNames(registeredNames) { + mismatches = append(mismatches, fmt.Sprintf( + "provider %q of type %q is registered more than once by the extension", + name, providerType)) + } + + for _, name := range declaredNames { + key := strings.ToLower(name) + if declaredDuplicateKeys[key] || registeredDuplicateKeys[key] || + slices.Contains(registeredNames, name) { + continue + } + + if registeredName, ok := equalFoldName(registeredNames, name); ok { + mismatches = append(mismatches, fmt.Sprintf( + "provider %q of type %q is declared in %s but registered as %q; provider names are case-sensitive", + name, providerType, manifestPath, registeredName)) + continue + } + + mismatches = append(mismatches, fmt.Sprintf( + "provider %q of type %q is declared in %s but not registered by the extension", + name, providerType, manifestPath)) + } + for _, name := range registeredNames { + key := strings.ToLower(name) + if declaredDuplicateKeys[key] || registeredDuplicateKeys[key] || + slices.Contains(declaredNames, name) { + continue + } + if _, ok := equalFoldName(declaredNames, name); ok { + continue + } + + mismatches = append(mismatches, fmt.Sprintf( + "provider %q of type %q is registered by the extension but not declared in %s", + name, providerType, manifestPath)) + } + } + + if len(mismatches) > 0 { + slices.Sort(mismatches) + return fmt.Errorf("extension providers do not match manifest:\n - %s", + strings.Join(mismatches, "\n - ")) + } + + return nil +} + +func duplicateNameKeys(names []string) map[string]bool { + counts := make(map[string]int, len(names)) + for _, name := range names { + counts[strings.ToLower(name)]++ + } + + duplicates := map[string]bool{} + for key, count := range counts { + if count > 1 { + duplicates[key] = true + } + } + return duplicates +} + +// duplicatedNames returns, once each and sorted, the names that appear more than +// once in names. Matching is case-insensitive because differently cased provider +// names collide during registry discovery, but the first-seen spelling is reported. +func duplicatedNames(names []string) []string { + duplicateKeys := duplicateNameKeys(names) + original := make(map[string]string, len(names)) + for _, name := range names { + key := strings.ToLower(name) + if _, ok := original[key]; !ok { + original[key] = name + } + } + + duplicated := make([]string, 0, len(duplicateKeys)) + for key := range duplicateKeys { + if name, ok := original[key]; ok { + duplicated = append(duplicated, name) + } + } + slices.Sort(duplicated) + return duplicated +} + +func equalFoldName(names []string, target string) (string, bool) { + for _, name := range names { + if strings.EqualFold(name, target) { + return name, true + } + } + return "", false +} + +// loadManifestProviders reads and groups a manifest's declared providers by type, +// limited to the types that are comparable against runtime registrations. +func loadManifestProviders(manifestPath string) (map[extensions.ProviderType][]string, error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + return nil, fmt.Errorf("reading manifest %s: %w", manifestPath, err) + } + + var doc manifestProviderDoc + if err := yaml.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("parsing manifest %s: %w", manifestPath, err) + } + + declared := map[extensions.ProviderType][]string{} + for _, provider := range doc.Providers { + if slices.Contains(manifestComparedProviderTypes, provider.Type) { + declared[provider.Type] = append(declared[provider.Type], provider.Name) + } + } + return declared, nil +} diff --git a/cli/azd/pkg/azdext/provider_manifest_test.go b/cli/azd/pkg/azdext/provider_manifest_test.go new file mode 100644 index 00000000000..4a3c81d91bf --- /dev/null +++ b/cli/azd/pkg/azdext/provider_manifest_test.go @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func writeManifest(t *testing.T, contents string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "extension.yaml") + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + return path +} + +func TestVerifyProvidersMatchManifest_Match(t *testing.T) { + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: custom.host + type: service-target + description: d + - name: custom.provider + type: provisioning-provider + description: d +`) + + factoryInvoked := false + configure := func(host *ExtensionHost) { + host. + WithServiceTarget("custom.host", func() ServiceTargetProvider { + factoryInvoked = true + return nil + }). + WithProvisioningProvider("custom.provider", func() ProvisioningProvider { + factoryInvoked = true + return nil + }) + } + + require.NoError(t, VerifyProvidersMatchManifest(configure, manifest)) + require.False(t, factoryInvoked, "provider factories must remain lazy during verification") +} + +func TestVerifyProvidersMatchManifest_DeadClaim(t *testing.T) { + // Manifest declares a service target the code never registers. + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: custom.host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) {} + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), "declared in") + require.Contains(t, err.Error(), "not registered") +} + +func TestVerifyProvidersMatchManifest_UndeclaredRegistration(t *testing.T) { + // Code registers a provider the manifest does not declare. + manifest := writeManifest(t, ` +id: publisher.extension +providers: [] +`) + + configure := func(host *ExtensionHost) { + host.WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }) + } + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), "registered by the extension but not declared") +} + +func TestVerifyProvidersMatchManifest_IgnoresFrameworkAndValidation(t *testing.T) { + // Framework and validation registrations are not represented in providers: and + // must not cause a mismatch. + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: custom.host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) { + host. + WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }). + WithFrameworkService("rust", func() FrameworkServiceProvider { return nil }) + } + + require.NoError(t, VerifyProvidersMatchManifest(configure, manifest)) +} + +func TestVerifyProvidersMatchManifest_DuplicateRegistration(t *testing.T) { + // A duplicate registration is rejected at runtime, so the check must flag it. + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: custom.host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) { + host. + WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }). + WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }) + } + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), "registered more than once") +} + +func TestVerifyProvidersMatchManifest_DuplicateManifestEntry(t *testing.T) { + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: custom.host + type: service-target + description: d + - name: custom.host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) { + host.WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }) + } + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), "declared more than once") +} + +func TestVerifyProvidersMatchManifest_NilConfigure(t *testing.T) { + manifest := writeManifest(t, "id: publisher.extension\n") + require.Error(t, VerifyProvidersMatchManifest(nil, manifest)) +} + +func TestVerifyProvidersMatchManifest_CaseMismatch(t *testing.T) { + // Runtime provider registration uses exact string keys, so a casing-only + // difference between the manifest and code must be reported. + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: Custom.Host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) { + host.WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }) + } + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), `provider "Custom.Host"`) + require.Contains(t, err.Error(), `registered as "custom.host"`) + require.Contains(t, err.Error(), "provider names are case-sensitive") + require.NotContains(t, err.Error(), "not registered") + require.NotContains(t, err.Error(), "not declared") +} + +func TestVerifyProvidersMatchManifest_CaseInsensitiveDuplicate(t *testing.T) { + // Duplicate detection is case-insensitive, matching core. + manifest := writeManifest(t, ` +id: publisher.extension +providers: + - name: Custom.Host + type: service-target + description: d + - name: custom.host + type: service-target + description: d +`) + + configure := func(host *ExtensionHost) { + host.WithServiceTarget("custom.host", func() ServiceTargetProvider { return nil }) + } + + err := VerifyProvidersMatchManifest(configure, manifest) + require.Error(t, err) + require.Contains(t, err.Error(), "declared more than once") + require.NotContains(t, err.Error(), "not registered") +} diff --git a/cli/azd/pkg/extensions/capability_schema_test.go b/cli/azd/pkg/extensions/capability_schema_test.go new file mode 100644 index 00000000000..4597d1f35de --- /dev/null +++ b/cli/azd/pkg/extensions/capability_schema_test.go @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package extensions + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestCapabilitySchemaTypesInSyncWithGo keeps both JSON schema capability enums +// aligned with the capabilities accepted by azd. +func TestCapabilitySchemaTypesInSyncWithGo(t *testing.T) { + expected := capabilityStrings() + + t.Run("extension.schema.json", func(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "extensions", "extension.schema.json")) + require.NoError(t, err) + + var schema struct { + Properties struct { + Capabilities struct { + Items struct { + OneOf []struct { + Const string `json:"const"` + } `json:"oneOf"` + } `json:"items"` + } `json:"capabilities"` + } `json:"properties"` + } + require.NoError(t, json.Unmarshal(data, &schema)) + + actual := make([]string, 0, len(schema.Properties.Capabilities.Items.OneOf)) + for _, capability := range schema.Properties.Capabilities.Items.OneOf { + actual = append(actual, capability.Const) + } + require.ElementsMatch(t, expected, actual) + }) + + t.Run("registry.schema.json", func(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "extensions", "registry.schema.json")) + require.NoError(t, err) + + var schema struct { + Definitions struct { + Version struct { + Properties struct { + Capabilities struct { + Items struct { + Enum []string `json:"enum"` + } `json:"items"` + } `json:"capabilities"` + } `json:"properties"` + } `json:"Version"` + } `json:"definitions"` + } + require.NoError(t, json.Unmarshal(data, &schema)) + + require.ElementsMatch(t, expected, schema.Definitions.Version.Properties.Capabilities.Items.Enum) + }) +} diff --git a/cli/azd/pkg/extensions/registry.go b/cli/azd/pkg/extensions/registry.go index 6748994b77f..dd46b919a27 100644 --- a/cli/azd/pkg/extensions/registry.go +++ b/cli/azd/pkg/extensions/registry.go @@ -64,6 +64,8 @@ type ProviderType string const ( // Service target provider type for custom deployment targets ServiceTargetProviderType ProviderType = "service-target" + // Provisioning provider type for custom infrastructure provisioning experiences + ProvisioningProviderType ProviderType = "provisioning-provider" ) // Extension represents an extension in the registry diff --git a/cli/azd/pkg/extensions/validate_registry_test.go b/cli/azd/pkg/extensions/validate_registry_test.go index f19ff61f908..f264b298b4b 100644 --- a/cli/azd/pkg/extensions/validate_registry_test.go +++ b/cli/azd/pkg/extensions/validate_registry_test.go @@ -549,6 +549,7 @@ func TestValidateExtension_AllValidCapabilities(t *testing.T) { FrameworkServiceProviderCapability, MetadataCapability, ProvisioningProviderCapability, + ValidationProviderCapability, }, Artifacts: validArtifacts(), },