diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index f75a2d299d3..3e086c0dfff 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -119,6 +119,7 @@ gosec goterm gotest gotestsum +grpcserver hotspot ignorefile iidfile diff --git a/cli/azd/Makefile b/cli/azd/Makefile index 859933955cc..d4ee96c5f3f 100644 --- a/cli/azd/Makefile +++ b/cli/azd/Makefile @@ -1,6 +1,5 @@ PROTO_DIR=grpc/proto GEN_DIR=pkg/azdext -INCLUDE_DIR=grpc/include # Local directory containing the protobuf include files .PHONY: all proto clean @@ -10,8 +9,7 @@ proto: # Generate server-side code mkdir -p $(GEN_DIR) # Generate code for all .proto files into the same package - protoc --proto_path=$(INCLUDE_DIR) \ - --proto_path=$(PROTO_DIR) \ + protoc --proto_path=$(PROTO_DIR) \ --go_out=$(GEN_DIR) --go_opt=paths=source_relative \ --go-grpc_out=$(GEN_DIR) --go-grpc_opt=paths=source_relative \ $(PROTO_DIR)/*.proto diff --git a/cli/azd/cmd/monitor.go b/cli/azd/cmd/monitor.go index 1ed7c6dd5a8..f59dfa49a27 100644 --- a/cli/azd/cmd/monitor.go +++ b/cli/azd/cmd/monitor.go @@ -118,8 +118,8 @@ func (m *monitorAction) Run(ctx context.Context) (*actions.ActionResult, error) return nil, fmt.Errorf("discovering resource groups from deployment: %w", err) } - var insightsResources []*azapi.Resource - var portalResources []*azapi.Resource + var insightsResources []*azapi.ResourceExtended + var portalResources []*azapi.ResourceExtended for _, resourceGroup := range resourceGroups { resources, err := m.resourceService.ListResourceGroupResources( diff --git a/cli/azd/grpc/proto/prompt.proto b/cli/azd/grpc/proto/prompt.proto index 37263e7ed13..6fba5784e05 100644 --- a/cli/azd/grpc/proto/prompt.proto +++ b/cli/azd/grpc/proto/prompt.proto @@ -15,34 +15,6 @@ service PromptService { rpc Select(SelectRequest) returns (SelectResponse); rpc PromptSubscriptionResource(PromptSubscriptionResourceRequest) returns (PromptSubscriptionResourceResponse); rpc PromptResourceGroupResource(PromptResourceGroupResourceRequest) returns (PromptResourceGroupResourceResponse); - rpc PromptStream(stream StreamWorkflowRequestMessage) returns (stream StreamWorkflowResponseMessage); -} - -message StreamWorkflowRequestMessage{ - oneof payload { - PromptOptions options = 1; - PromptValidation validation = 2; - } -} - -message StreamWorkflowResponseMessage{ - oneof payload { - PromptValue value = 1; - PromptFinalValue complete = 2; - } -} - -message PromptValue { - string value = 1; -} - -message PromptValidation { - bool valid = 1; - string message = 2; -} - -message PromptFinalValue { - string value = 1; } message PromptSubscriptionRequest {} diff --git a/cli/azd/internal/grpcserver/deployment_service.go b/cli/azd/internal/grpcserver/deployment_service.go new file mode 100644 index 00000000000..795de3e5f2e --- /dev/null +++ b/cli/azd/internal/grpcserver/deployment_service.go @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning/bicep" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" + "github.com/azure/azure-dev/cli/azd/pkg/project" +) + +type deploymentService struct { + azdext.UnimplementedDeploymentServiceServer + + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] + lazyEnvManager *lazy.Lazy[environment.Manager] + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig] + lazyBicepProvider *lazy.Lazy[*bicep.BicepProvider] + deploymentService azapi.DeploymentService +} + +func NewDeploymentService( + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + lazyEnvManager *lazy.Lazy[environment.Manager], + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], + lazyBicepProvider *lazy.Lazy[*bicep.BicepProvider], + azureDeploymentService azapi.DeploymentService, +) azdext.DeploymentServiceServer { + return &deploymentService{ + lazyAzdContext: lazyAzdContext, + lazyEnvManager: lazyEnvManager, + lazyProjectConfig: lazyProjectConfig, + lazyBicepProvider: lazyBicepProvider, + deploymentService: azureDeploymentService, + } +} + +func (s *deploymentService) GetDeployment( + ctx context.Context, + req *azdext.EmptyRequest, +) (*azdext.GetDeploymentResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + bicepProvider, err := s.lazyBicepProvider.GetValue() + if err != nil { + return nil, err + } + + if err := bicepProvider.Initialize(ctx, azdContext.ProjectDirectory(), projectConfig.Infra); err != nil { + return nil, err + } + + latestDeployment, err := bicepProvider.LastDeployment(ctx) + if err != nil { + return nil, err + } + + deployment := &azdext.Deployment{ + Id: latestDeployment.Id, + Name: latestDeployment.Name, + Location: latestDeployment.Location, + DeploymentId: latestDeployment.DeploymentId, + Type: latestDeployment.Type, + Resources: []string{}, + Tags: map[string]string{}, + } + + for key, value := range latestDeployment.Tags { + deployment.Tags[key] = *value + } + + for _, resource := range latestDeployment.Resources { + deployment.Resources = append(deployment.Resources, *resource.ID) + } + + return &azdext.GetDeploymentResponse{ + Deployment: deployment, + }, nil +} + +func (s *deploymentService) GetDeploymentContext( + ctx context.Context, + req *azdext.EmptyRequest, +) (*azdext.GetDeploymentContextResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + defaultEnvironment, err := azdContext.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + if defaultEnvironment == "" { + return nil, environment.ErrDefaultEnvironmentNotFound + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + env, err := envManager.Get(ctx, defaultEnvironment) + if err != nil { + return nil, err + } + + tenantId := env.Getenv(environment.TenantIdEnvVarName) + subscriptionId := env.GetSubscriptionId() + resourceGroup := env.Getenv(environment.ResourceGroupEnvVarName) + location := env.Getenv(environment.LocationEnvVarName) + + azureScope := &azdext.AzureScope{ + TenantId: tenantId, + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + Location: location, + } + + latestDeployment, err := s.GetDeployment(ctx, req) + if err != nil { + return nil, err + } + + azureContext := &azdext.AzureContext{ + Scope: azureScope, + Resources: latestDeployment.Deployment.Resources, + } + + return &azdext.GetDeploymentContextResponse{ + AzureContext: azureContext, + }, nil +} diff --git a/cli/azd/internal/grpcserver/environment_service.go b/cli/azd/internal/grpcserver/environment_service.go new file mode 100644 index 00000000000..8b215ca392d --- /dev/null +++ b/cli/azd/internal/grpcserver/environment_service.go @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" +) + +type environmentService struct { + azdext.UnimplementedEnvironmentServiceServer + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] + lazyEnvManager *lazy.Lazy[environment.Manager] + + azdContext *azdcontext.AzdContext + envManager environment.Manager + initialized bool +} + +func NewEnvironmentService( + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + lazyEnvManager *lazy.Lazy[environment.Manager], +) azdext.EnvironmentServiceServer { + return &environmentService{ + lazyAzdContext: lazyAzdContext, + lazyEnvManager: lazyEnvManager, + } +} + +func (s *environmentService) List(ctx context.Context, req *azdext.EmptyRequest) (*azdext.EnvironmentListResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + envList, err := s.envManager.List(ctx) + if err != nil { + return nil, err + } + + environments := make([]*azdext.EnvironmentDescription, len(envList)) + for i, env := range envList { + environments[i] = &azdext.EnvironmentDescription{ + Name: env.Name, + Local: env.HasLocal, + Remote: env.HasRemote, + Default: env.IsDefault, + } + } + + return &azdext.EnvironmentListResponse{ + Environments: environments, + }, nil +} + +func (s *environmentService) GetCurrent( + ctx context.Context, + req *azdext.EmptyRequest, +) (*azdext.EnvironmentResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + return &azdext.EnvironmentResponse{ + Environment: &azdext.Environment{ + Name: env.Name(), + }, + }, nil +} + +func (s *environmentService) Get( + ctx context.Context, + req *azdext.GetEnvironmentRequest, +) (*azdext.EnvironmentResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.envManager.Get(ctx, req.Name) + if err != nil { + return nil, err + } + + return &azdext.EnvironmentResponse{ + Environment: &azdext.Environment{ + Name: env.Name(), + }, + }, nil +} + +func (s *environmentService) Select( + ctx context.Context, + req *azdext.SelectEnvironmentRequest, +) (*azdext.EmptyResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.envManager.Get(ctx, req.Name) + if err != nil { + return nil, err + } + + projectState := azdcontext.ProjectState{ + DefaultEnvironment: env.Name(), + } + + if err := s.azdContext.SetProjectState(projectState); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// GetValues retrieves all key-value pairs in the specified environment. +func (s *environmentService) GetValues( + ctx context.Context, + req *azdext.GetEnvironmentRequest, +) (*azdext.KeyValueListResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.envManager.Get(ctx, req.Name) + if err != nil { + return nil, err + } + + value := env.Dotenv() + keyValues := make([]*azdext.KeyValue, len(value)) + + i := 0 + for key, value := range value { + keyValues[i] = &azdext.KeyValue{ + Key: key, + Value: value, + } + i++ + } + + return &azdext.KeyValueListResponse{ + KeyValues: keyValues, + }, nil +} + +// GetValue retrieves the value of a specific key in the specified environment. +func (s *environmentService) GetValue(ctx context.Context, req *azdext.GetEnvRequest) (*azdext.KeyValueResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.envManager.Get(ctx, req.EnvName) + if err != nil { + return nil, err + } + + value := env.Getenv(req.Key) + + return &azdext.KeyValueResponse{ + Key: req.Key, + Value: value, + }, nil +} + +// SetValue sets the value of a key in the specified environment. +func (s *environmentService) SetValue(ctx context.Context, req *azdext.SetEnvRequest) (*azdext.EmptyResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.envManager.Get(ctx, req.EnvName) + if err != nil { + return nil, err + } + + env.DotenvSet(req.Key, req.Value) + + return &azdext.EmptyResponse{}, nil +} + +func (s *environmentService) currentEnvironment(ctx context.Context) (*environment.Environment, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + defaultEnvironment, err := s.azdContext.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + if defaultEnvironment == "" { + return nil, environment.ErrDefaultEnvironmentNotFound + } + + env, err := s.envManager.Get(ctx, defaultEnvironment) + if err != nil { + return nil, err + } + + return env, nil +} + +// GetConfig retrieves a config value by path. +func (s *environmentService) GetConfig( + ctx context.Context, + req *azdext.GetConfigRequest, +) (*azdext.GetConfigResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + value, exists := env.Config.Get(req.Path) + + var valueBytes []byte + if exists { + bytes, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("failed to marshal value: %w", err) + } + + valueBytes = bytes + } + + return &azdext.GetConfigResponse{ + Value: valueBytes, + Found: exists, + }, nil +} + +// GetConfigString retrieves a config value as a string by path. +func (s *environmentService) GetConfigString( + ctx context.Context, + req *azdext.GetConfigStringRequest, +) (*azdext.GetConfigStringResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + value, exists := env.Config.GetString(req.Path) + + return &azdext.GetConfigStringResponse{ + Value: value, + Found: exists, + }, nil +} + +// GetConfigSection retrieves a config section by path. +func (s *environmentService) GetConfigSection( + ctx context.Context, + req *azdext.GetConfigSectionRequest, +) (*azdext.GetConfigSectionResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + var section map[string]any + + exists, err := env.Config.GetSection(req.Path, §ion) + if err != nil { + return nil, fmt.Errorf("failed to get section: %w", err) + } + + var valueBytes []byte + if exists { + bytes, err := json.Marshal(section) + if err != nil { + return nil, fmt.Errorf("failed to marshal value: %w", err) + } + + valueBytes = bytes + } + + return &azdext.GetConfigSectionResponse{ + Section: valueBytes, + Found: exists, + }, nil +} + +// SetConfig sets a config value at a given path. +func (s *environmentService) SetConfig(ctx context.Context, req *azdext.SetConfigRequest) (*azdext.EmptyResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + var value any + if err := json.Unmarshal(req.Value, &value); err != nil { + return nil, fmt.Errorf("failed to unmarshal value: %w", err) + } + + if err := env.Config.Set(req.Path, value); err != nil { + return nil, fmt.Errorf("failed to set value: %w", err) + } + + if err := s.envManager.Save(ctx, env); err != nil { + return nil, fmt.Errorf("failed to save config: %w", err) + } + + return &azdext.EmptyResponse{}, nil +} + +// UnsetConfig unsets a config value at a given path. +func (s *environmentService) UnsetConfig( + ctx context.Context, + req *azdext.UnsetConfigRequest, +) (*azdext.EmptyResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + env, err := s.currentEnvironment(ctx) + if err != nil { + return nil, err + } + + if err := env.Config.Unset(req.Path); err != nil { + return nil, fmt.Errorf("failed to unset value: %w", err) + } + + if err := s.envManager.Save(ctx, env); err != nil { + return nil, fmt.Errorf("failed to save config: %w", err) + } + + return &azdext.EmptyResponse{}, nil +} + +func (s *environmentService) initialize() error { + if s.initialized { + return nil + } + + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return err + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return err + } + + s.azdContext = azdContext + s.envManager = envManager + s.initialized = true + + return nil +} diff --git a/cli/azd/internal/grpcserver/project_service.go b/cli/azd/internal/grpcserver/project_service.go new file mode 100644 index 00000000000..f0f98d0a107 --- /dev/null +++ b/cli/azd/internal/grpcserver/project_service.go @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" + "github.com/azure/azure-dev/cli/azd/pkg/project" +) + +type projectService struct { + azdext.UnimplementedProjectServiceServer + + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] + lazyEnvManager *lazy.Lazy[environment.Manager] + + azdContext *azdcontext.AzdContext + envManager environment.Manager + + initialized bool +} + +func NewProjectService( + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + lazyEnvManager *lazy.Lazy[environment.Manager], +) azdext.ProjectServiceServer { + return &projectService{ + lazyAzdContext: lazyAzdContext, + lazyEnvManager: lazyEnvManager, + } +} + +func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*azdext.GetProjectResponse, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, s.azdContext.ProjectPath()) + if err != nil { + return nil, err + } + + envKeyMapper := func(env string) string { + return "" + } + + defaultEnvironment, err := s.azdContext.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + if defaultEnvironment != "" { + env, err := s.envManager.Get(ctx, defaultEnvironment) + if err == nil && env != nil { + envKeyMapper = env.Getenv + } + } + + project := &azdext.ProjectConfig{ + Name: projectConfig.Name, + ResourceGroupName: projectConfig.ResourceGroupName.MustEnvsubst(envKeyMapper), + Path: projectConfig.Path, + Infra: &azdext.InfraOptions{ + Provider: string(projectConfig.Infra.Provider), + Path: projectConfig.Infra.Path, + Module: projectConfig.Infra.Module, + }, + Services: map[string]*azdext.ServiceConfig{}, + } + + if projectConfig.Metadata != nil { + project.Metadata = &azdext.ProjectMetadata{ + Template: projectConfig.Metadata.Template, + } + } + + for name, service := range projectConfig.Services { + project.Services[name] = &azdext.ServiceConfig{ + Name: service.Name, + ResourceGroupName: service.ResourceGroupName.MustEnvsubst(envKeyMapper), + ResourceName: service.ResourceName.MustEnvsubst(envKeyMapper), + ApiVersion: service.ApiVersion, + RelativePath: service.RelativePath, + Host: string(service.Host), + Language: string(service.Language), + OutputPath: service.OutputPath, + Image: service.Image.MustEnvsubst(envKeyMapper), + } + } + + return &azdext.GetProjectResponse{ + Project: project, + }, nil +} + +func (s *projectService) initialize() error { + if s.initialized { + return nil + } + + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return err + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return err + } + + s.azdContext = azdContext + s.envManager = envManager + s.initialized = true + + return nil +} diff --git a/cli/azd/internal/grpcserver/prompt_service.go b/cli/azd/internal/grpcserver/prompt_service.go new file mode 100644 index 00000000000..b97951886e9 --- /dev/null +++ b/cli/azd/internal/grpcserver/prompt_service.go @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/prompt" + "github.com/azure/azure-dev/cli/azd/pkg/ux" +) + +type promptService struct { + azdext.UnimplementedPromptServiceServer + prompter prompt.PromptService + resourceService *azapi.ResourceService +} + +func NewPromptService(prompter prompt.PromptService, resourceService *azapi.ResourceService) azdext.PromptServiceServer { + return &promptService{ + prompter: prompter, + resourceService: resourceService, + } +} + +func (s *promptService) Confirm(ctx context.Context, req *azdext.ConfirmRequest) (*azdext.ConfirmResponse, error) { + options := &ux.ConfirmOptions{ + DefaultValue: req.Options.DefaultValue, + Message: req.Options.Message, + HelpMessage: req.Options.HelpMessage, + Hint: req.Options.Hint, + PlaceHolder: req.Options.Placeholder, + } + + confirm := ux.NewConfirm(options) + value, err := confirm.Ask() + + return &azdext.ConfirmResponse{ + Value: value, + }, err +} + +func (s *promptService) Select(ctx context.Context, req *azdext.SelectRequest) (*azdext.SelectResponse, error) { + options := &ux.SelectOptions{ + SelectedIndex: convertToInt(req.Options.SelectedIndex), + Message: req.Options.Message, + Allowed: req.Options.Allowed, + HelpMessage: req.Options.HelpMessage, + Hint: req.Options.Hint, + DisplayCount: int(req.Options.DisplayCount), + DisplayNumbers: req.Options.DisplayNumbers, + EnableFiltering: req.Options.EnableFiltering, + } + + selectPrompt := ux.NewSelect(options) + value, err := selectPrompt.Ask() + + return &azdext.SelectResponse{ + Value: convertToInt32(value), + }, err +} + +func (s *promptService) Prompt(ctx context.Context, req *azdext.PromptRequest) (*azdext.PromptResponse, error) { + options := &ux.PromptOptions{ + DefaultValue: req.Options.DefaultValue, + Message: req.Options.Message, + HelpMessage: req.Options.HelpMessage, + Hint: req.Options.Hint, + PlaceHolder: req.Options.Placeholder, + ValidationMessage: req.Options.ValidationMessage, + RequiredMessage: req.Options.RequiredMessage, + Required: req.Options.Required, + ClearOnCompletion: req.Options.ClearOnCompletion, + IgnoreHintKeys: req.Options.IgnoreHintKeys, + } + + prompt := ux.NewPrompt(options) + value, err := prompt.Ask() + + return &azdext.PromptResponse{ + Value: value, + }, err +} + +func (s *promptService) PromptSubscription( + ctx context.Context, + req *azdext.PromptSubscriptionRequest, +) (*azdext.PromptSubscriptionResponse, error) { + selectedSubscription, err := s.prompter.PromptSubscription(ctx, nil) + if err != nil { + return nil, err + } + + subscription := &azdext.Subscription{ + Id: selectedSubscription.Id, + Name: selectedSubscription.Name, + TenantId: selectedSubscription.TenantId, + UserAccessTenantId: selectedSubscription.UserAccessTenantId, + IsDefault: selectedSubscription.IsDefault, + } + + return &azdext.PromptSubscriptionResponse{ + Subscription: subscription, + }, nil +} + +func (s *promptService) PromptLocation( + ctx context.Context, + req *azdext.PromptLocationRequest, +) (*azdext.PromptLocationResponse, error) { + azureContext, err := s.createAzureContext(req.AzureContext) + if err != nil { + return nil, err + } + + selectedLocation, err := s.prompter.PromptLocation(ctx, azureContext, nil) + if err != nil { + return nil, err + } + + location := &azdext.Location{ + Name: selectedLocation.Name, + DisplayName: selectedLocation.DisplayName, + RegionalDisplayName: selectedLocation.RegionalDisplayName, + } + + return &azdext.PromptLocationResponse{ + Location: location, + }, nil +} + +func (s *promptService) PromptResourceGroup( + ctx context.Context, + req *azdext.PromptResourceGroupRequest, +) (*azdext.PromptResourceGroupResponse, error) { + azureContext, err := s.createAzureContext(req.AzureContext) + if err != nil { + return nil, err + } + + selectedResourceGroup, err := s.prompter.PromptResourceGroup(ctx, azureContext, nil) + if err != nil { + return nil, err + } + + resourceGroup := &azdext.ResourceGroup{ + Id: selectedResourceGroup.Id, + Name: selectedResourceGroup.Name, + Location: selectedResourceGroup.Location, + } + + return &azdext.PromptResourceGroupResponse{ + ResourceGroup: resourceGroup, + }, nil +} + +func (s *promptService) PromptSubscriptionResource( + ctx context.Context, + req *azdext.PromptSubscriptionResourceRequest, +) (*azdext.PromptSubscriptionResourceResponse, error) { + azureContext, err := s.createAzureContext(req.AzureContext) + if err != nil { + return nil, err + } + + options := createResourceOptions(req.Options) + + resource, err := s.prompter.PromptSubscriptionResource(ctx, azureContext, options) + if err != nil { + return nil, err + } + + return &azdext.PromptSubscriptionResourceResponse{ + Resource: &azdext.ResourceExtended{ + Id: resource.Id, + Name: resource.Name, + Type: resource.Type, + Location: resource.Location, + Kind: resource.Kind, + }, + }, nil +} + +func (s *promptService) PromptResourceGroupResource( + ctx context.Context, + req *azdext.PromptResourceGroupResourceRequest, +) (*azdext.PromptResourceGroupResourceResponse, error) { + azureContext, err := s.createAzureContext(req.AzureContext) + if err != nil { + return nil, err + } + + options := createResourceOptions(req.Options) + + resource, err := s.prompter.PromptSubscriptionResource(ctx, azureContext, options) + if err != nil { + return nil, err + } + + return &azdext.PromptResourceGroupResourceResponse{ + Resource: &azdext.ResourceExtended{ + Id: resource.Id, + Name: resource.Name, + Type: resource.Type, + Location: resource.Location, + Kind: resource.Kind, + }, + }, nil +} + +func (s *promptService) createAzureContext(wire *azdext.AzureContext) (*prompt.AzureContext, error) { + scope := prompt.AzureScope{ + TenantId: wire.Scope.TenantId, + SubscriptionId: wire.Scope.SubscriptionId, + Location: wire.Scope.Location, + ResourceGroup: wire.Scope.ResourceGroup, + } + + resources := []*arm.ResourceID{} + for _, resourceId := range wire.Resources { + parsedResource, err := arm.ParseResourceID(resourceId) + if err != nil { + return nil, err + } + + resources = append(resources, parsedResource) + } + + resourceList := prompt.NewAzureResourceList(s.resourceService, resources) + + return prompt.NewAzureContext(s.prompter, scope, resourceList), nil +} + +func createResourceOptions(options *azdext.PromptResourceOptions) prompt.ResourceOptions { + if options == nil { + return prompt.ResourceOptions{} + } + + var resourceType *azapi.AzureResourceType + if options.ResourceType != "" { + resourceType = to.Ptr(azapi.AzureResourceType(options.ResourceType)) + } + + var selectOptions *prompt.SelectOptions + + if options.SelectOptions != nil { + selectOptions = &prompt.SelectOptions{ + ForceNewResource: options.SelectOptions.ForceNewResource, + NewResourceMessage: options.SelectOptions.NewResourceMessage, + CreatingMessage: options.SelectOptions.CreatingMessage, + Message: options.SelectOptions.Message, + HelpMessage: options.SelectOptions.HelpMessage, + LoadingMessage: options.SelectOptions.LoadingMessage, + DisplayCount: int(options.SelectOptions.DisplayCount), + DisplayNumbers: options.SelectOptions.DisplayNumbers, + AllowNewResource: options.SelectOptions.AllowNewResource, + } + } + + resourceOptions := prompt.ResourceOptions{ + ResourceType: resourceType, + Kinds: options.Kinds, + ResourceTypeDisplayName: options.ResourceTypeDisplayName, + SelectorOptions: selectOptions, + } + + return resourceOptions +} + +func convertToInt32(input *int) *int32 { + if input == nil { + return nil // Handle the nil case + } + + // nolint:gosec // G115 + value := int32(*input) // Convert the dereferenced value to int32 + return &value // Return the address of the new int32 value +} + +func convertToInt(input *int32) *int { + if input == nil { + return nil // Handle the nil case + } + value := int(*input) // Convert the dereferenced value to int + return &value // Return the address of the new int value +} diff --git a/cli/azd/internal/grpcserver/server.go b/cli/azd/internal/grpcserver/server.go new file mode 100644 index 00000000000..1671b5d8350 --- /dev/null +++ b/cli/azd/internal/grpcserver/server.go @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "net" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type ServerInfo struct { + Address string + Port int + AccessToken string +} + +type Server struct { + grpcServer *grpc.Server + projectService azdext.ProjectServiceServer + environmentService azdext.EnvironmentServiceServer + promptService azdext.PromptServiceServer + userConfigService azdext.UserConfigServiceServer + deploymentService azdext.DeploymentServiceServer +} + +func NewServer( + projectService azdext.ProjectServiceServer, + environmentService azdext.EnvironmentServiceServer, + promptService azdext.PromptServiceServer, + userConfigService azdext.UserConfigServiceServer, + deploymentService azdext.DeploymentServiceServer, +) *Server { + return &Server{ + projectService: projectService, + environmentService: environmentService, + promptService: promptService, + userConfigService: userConfigService, + deploymentService: deploymentService, + } +} + +func (s *Server) Start() (*ServerInfo, error) { + accessToken, err := generateToken() + if err != nil { + return nil, fmt.Errorf("failed to generate access token: %w", err) + } + + s.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(tokenAuthInterceptor(accessToken)), + ) + + // Use ":0" to let the system assign an available random port + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to listen: %w", err) + } + + // Get the assigned random port + randomPort := listener.Addr().(*net.TCPAddr).Port + + // Register the Greeter service with the gRPC server + azdext.RegisterProjectServiceServer(s.grpcServer, s.projectService) + azdext.RegisterEnvironmentServiceServer(s.grpcServer, s.environmentService) + azdext.RegisterPromptServiceServer(s.grpcServer, s.promptService) + azdext.RegisterUserConfigServiceServer(s.grpcServer, s.userConfigService) + azdext.RegisterDeploymentServiceServer(s.grpcServer, s.deploymentService) + + go func() { + // Start the gRPC server + if err := s.grpcServer.Serve(listener); err != nil { + log.Fatalf("failed to serve: %v", err) + } + }() + + log.Printf("AZD Server listening on port %d", randomPort) + + return &ServerInfo{ + Address: fmt.Sprintf("localhost:%d", randomPort), + Port: randomPort, + AccessToken: accessToken, + }, nil +} + +func (s *Server) Stop() error { + if s.grpcServer == nil { + return fmt.Errorf("server is not running") + } + + s.grpcServer.Stop() + log.Println("AZD Server stopped") + + return nil +} + +func tokenAuthInterceptor(expectedToken string) grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "metadata missing") + } + + // Extract the authorization token from metadata + token := md["authorization"] + if len(token) == 0 || token[0] != expectedToken { + return nil, status.Error(codes.Unauthenticated, "invalid token") + } + + // Proceed to the handler + return handler(ctx, req) + } +} + +func generateToken() (string, error) { + bytes := make([]byte, 16) // 128-bit token + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} diff --git a/cli/azd/internal/grpcserver/user_config_service.go b/cli/azd/internal/grpcserver/user_config_service.go new file mode 100644 index 00000000000..3b7e3620db7 --- /dev/null +++ b/cli/azd/internal/grpcserver/user_config_service.go @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/config" +) + +// configService is the implementation of ConfigServiceServer. +type userConfigService struct { + azdext.UnimplementedUserConfigServiceServer + + configManager config.UserConfigManager + config config.Config +} + +// NewConfigService creates a new instance of configService. +func NewUserConfigService(userConfigManager config.UserConfigManager) (azdext.UserConfigServiceServer, error) { + config, err := userConfigManager.Load() + if err != nil { + return nil, fmt.Errorf("failed to load user config: %w", err) + } + + return &userConfigService{ + configManager: userConfigManager, + config: config, + }, nil +} + +func (s *userConfigService) Get(ctx context.Context, req *azdext.GetRequest) (*azdext.GetResponse, error) { + value, exists := s.config.Get(req.Path) + + var valueBytes []byte + if exists { + bytes, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("failed to marshal value: %w", err) + } + + valueBytes = bytes + } + + return &azdext.GetResponse{ + Value: valueBytes, + Found: exists, + }, nil +} + +func (s *userConfigService) GetString(ctx context.Context, req *azdext.GetStringRequest) (*azdext.GetStringResponse, error) { + value, exists := s.config.GetString(req.Path) + + return &azdext.GetStringResponse{ + Value: value, + Found: exists, + }, nil +} + +func (s *userConfigService) GetSection( + ctx context.Context, + req *azdext.GetSectionRequest, +) (*azdext.GetSectionResponse, error) { + var section map[string]any + + exists, err := s.config.GetSection(req.Path, §ion) + if err != nil { + return nil, fmt.Errorf("failed to get section: %w", err) + } + + var valueBytes []byte + if exists { + bytes, err := json.Marshal(section) + if err != nil { + return nil, fmt.Errorf("failed to marshal value: %w", err) + } + + valueBytes = bytes + } + + return &azdext.GetSectionResponse{ + Section: valueBytes, + Found: exists, + }, nil +} + +func (s *userConfigService) Set(ctx context.Context, req *azdext.SetRequest) (*azdext.SetResponse, error) { + var value any + if err := json.Unmarshal(req.Value, &value); err != nil { + return nil, fmt.Errorf("failed to unmarshal value: %w", err) + } + + if err := s.config.Set(req.Path, value); err != nil { + return nil, fmt.Errorf("failed to set value: %w", err) + } + + if err := s.configManager.Save(s.config); err != nil { + return nil, fmt.Errorf("failed to save config: %w", err) + } + + return &azdext.SetResponse{}, nil +} + +func (s *userConfigService) Unset(ctx context.Context, req *azdext.UnsetRequest) (*azdext.UnsetResponse, error) { + if err := s.config.Unset(req.Path); err != nil { + return nil, fmt.Errorf("failed to unset value: %w", err) + } + + if err := s.configManager.Save(s.config); err != nil { + return nil, fmt.Errorf("failed to save config: %w", err) + } + + return &azdext.UnsetResponse{}, nil +} diff --git a/cli/azd/pkg/azapi/resource_service.go b/cli/azd/pkg/azapi/resource_service.go index 2aef51c46a0..ec003d83677 100644 --- a/cli/azd/pkg/azapi/resource_service.go +++ b/cli/azd/pkg/azapi/resource_service.go @@ -12,6 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/convert" ) type Resource struct { @@ -21,6 +22,12 @@ type Resource struct { Location string `json:"location"` } +type ResourceGroup struct { + Id string `json:"id"` + Name string `json:"name"` + Location string `json:"location"` +} + type ResourceExtended struct { Resource Kind string `json:"kind"` @@ -90,7 +97,7 @@ func (rs *ResourceService) ListResourceGroupResources( subscriptionId string, resourceGroupName string, listOptions *ListResourceGroupResourcesOptions, -) ([]*Resource, error) { +) ([]*ResourceExtended, error) { client, err := rs.createResourcesClient(ctx, subscriptionId) if err != nil { return nil, err @@ -103,7 +110,7 @@ func (rs *ResourceService) ListResourceGroupResources( options.Filter = listOptions.Filter } - resources := []*Resource{} + resources := []*ResourceExtended{} pager := client.NewListByResourceGroupPager(resourceGroupName, &options) for pager.More() { page, err := pager.NextPage(ctx) @@ -112,11 +119,14 @@ func (rs *ResourceService) ListResourceGroupResources( } for _, resource := range page.ResourceListResult.Value { - resources = append(resources, &Resource{ - Id: *resource.ID, - Name: *resource.Name, - Type: *resource.Type, - Location: *resource.Location, + resources = append(resources, &ResourceExtended{ + Resource: Resource{ + Id: *resource.ID, + Name: *resource.Name, + Type: *resource.Type, + Location: *resource.Location, + }, + Kind: convert.ToValueWithDefault(resource.Kind, ""), }) } } @@ -172,24 +182,73 @@ func (rs *ResourceService) ListResourceGroup( return groups, nil } +func (rs *ResourceService) ListSubscriptionResources( + ctx context.Context, + subscriptionId string, + listOptions *armresources.ClientListOptions, +) ([]*ResourceExtended, error) { + client, err := rs.createResourcesClient(ctx, subscriptionId) + if err != nil { + return nil, err + } + + // Filter expression on the underlying REST API are different from --query param in az cli. + // https://learn.microsoft.com/en-us/rest/api/resources/resources/list-by-resource-group#uri-parameters + options := armresources.ClientListOptions{} + if listOptions != nil && *listOptions.Filter != "" { + options.Filter = listOptions.Filter + } + + resources := []*ResourceExtended{} + pager := client.NewListPager(&options) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, resource := range page.ResourceListResult.Value { + resources = append(resources, &ResourceExtended{ + Resource: Resource{ + Id: *resource.ID, + Name: *resource.Name, + Type: *resource.Type, + Location: *resource.Location, + }, + Kind: convert.ToValueWithDefault(resource.Kind, ""), + }) + } + } + + return resources, nil +} + func (rs *ResourceService) CreateOrUpdateResourceGroup( ctx context.Context, subscriptionId string, resourceGroupName string, location string, tags map[string]*string, -) error { +) (*ResourceGroup, error) { client, err := rs.createResourceGroupClient(ctx, subscriptionId) if err != nil { - return err + return nil, err } - _, err = client.CreateOrUpdate(ctx, resourceGroupName, armresources.ResourceGroup{ + response, err := client.CreateOrUpdate(ctx, resourceGroupName, armresources.ResourceGroup{ Location: &location, Tags: tags, }, nil) - return err + if err != nil { + return nil, fmt.Errorf("creating or updating resource group: %w", err) + } + + return &ResourceGroup{ + Id: *response.ID, + Name: *response.Name, + Location: *response.Location, + }, nil } func (rs *ResourceService) DeleteResourceGroup(ctx context.Context, subscriptionId string, resourceGroupName string) error { diff --git a/cli/azd/pkg/azdext/prompt.pb.go b/cli/azd/pkg/azdext/prompt.pb.go index 2448f7383a0..a40b0f9231e 100644 --- a/cli/azd/pkg/azdext/prompt.pb.go +++ b/cli/azd/pkg/azdext/prompt.pb.go @@ -23,307 +23,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type StreamWorkflowRequestMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Payload: - // - // *StreamWorkflowRequestMessage_Options - // *StreamWorkflowRequestMessage_Validation - Payload isStreamWorkflowRequestMessage_Payload `protobuf_oneof:"payload"` -} - -func (x *StreamWorkflowRequestMessage) Reset() { - *x = StreamWorkflowRequestMessage{} - mi := &file_prompt_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamWorkflowRequestMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamWorkflowRequestMessage) ProtoMessage() {} - -func (x *StreamWorkflowRequestMessage) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamWorkflowRequestMessage.ProtoReflect.Descriptor instead. -func (*StreamWorkflowRequestMessage) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{0} -} - -func (m *StreamWorkflowRequestMessage) GetPayload() isStreamWorkflowRequestMessage_Payload { - if m != nil { - return m.Payload - } - return nil -} - -func (x *StreamWorkflowRequestMessage) GetOptions() *PromptOptions { - if x, ok := x.GetPayload().(*StreamWorkflowRequestMessage_Options); ok { - return x.Options - } - return nil -} - -func (x *StreamWorkflowRequestMessage) GetValidation() *PromptValidation { - if x, ok := x.GetPayload().(*StreamWorkflowRequestMessage_Validation); ok { - return x.Validation - } - return nil -} - -type isStreamWorkflowRequestMessage_Payload interface { - isStreamWorkflowRequestMessage_Payload() -} - -type StreamWorkflowRequestMessage_Options struct { - Options *PromptOptions `protobuf:"bytes,1,opt,name=options,proto3,oneof"` -} - -type StreamWorkflowRequestMessage_Validation struct { - Validation *PromptValidation `protobuf:"bytes,2,opt,name=validation,proto3,oneof"` -} - -func (*StreamWorkflowRequestMessage_Options) isStreamWorkflowRequestMessage_Payload() {} - -func (*StreamWorkflowRequestMessage_Validation) isStreamWorkflowRequestMessage_Payload() {} - -type StreamWorkflowResponseMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Payload: - // - // *StreamWorkflowResponseMessage_Value - // *StreamWorkflowResponseMessage_Complete - Payload isStreamWorkflowResponseMessage_Payload `protobuf_oneof:"payload"` -} - -func (x *StreamWorkflowResponseMessage) Reset() { - *x = StreamWorkflowResponseMessage{} - mi := &file_prompt_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StreamWorkflowResponseMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StreamWorkflowResponseMessage) ProtoMessage() {} - -func (x *StreamWorkflowResponseMessage) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StreamWorkflowResponseMessage.ProtoReflect.Descriptor instead. -func (*StreamWorkflowResponseMessage) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{1} -} - -func (m *StreamWorkflowResponseMessage) GetPayload() isStreamWorkflowResponseMessage_Payload { - if m != nil { - return m.Payload - } - return nil -} - -func (x *StreamWorkflowResponseMessage) GetValue() *PromptValue { - if x, ok := x.GetPayload().(*StreamWorkflowResponseMessage_Value); ok { - return x.Value - } - return nil -} - -func (x *StreamWorkflowResponseMessage) GetComplete() *PromptFinalValue { - if x, ok := x.GetPayload().(*StreamWorkflowResponseMessage_Complete); ok { - return x.Complete - } - return nil -} - -type isStreamWorkflowResponseMessage_Payload interface { - isStreamWorkflowResponseMessage_Payload() -} - -type StreamWorkflowResponseMessage_Value struct { - Value *PromptValue `protobuf:"bytes,1,opt,name=value,proto3,oneof"` -} - -type StreamWorkflowResponseMessage_Complete struct { - Complete *PromptFinalValue `protobuf:"bytes,2,opt,name=complete,proto3,oneof"` -} - -func (*StreamWorkflowResponseMessage_Value) isStreamWorkflowResponseMessage_Payload() {} - -func (*StreamWorkflowResponseMessage_Complete) isStreamWorkflowResponseMessage_Payload() {} - -type PromptValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` -} - -func (x *PromptValue) Reset() { - *x = PromptValue{} - mi := &file_prompt_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PromptValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PromptValue) ProtoMessage() {} - -func (x *PromptValue) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PromptValue.ProtoReflect.Descriptor instead. -func (*PromptValue) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{2} -} - -func (x *PromptValue) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type PromptValidation struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *PromptValidation) Reset() { - *x = PromptValidation{} - mi := &file_prompt_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PromptValidation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PromptValidation) ProtoMessage() {} - -func (x *PromptValidation) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PromptValidation.ProtoReflect.Descriptor instead. -func (*PromptValidation) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{3} -} - -func (x *PromptValidation) GetValid() bool { - if x != nil { - return x.Valid - } - return false -} - -func (x *PromptValidation) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type PromptFinalValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` -} - -func (x *PromptFinalValue) Reset() { - *x = PromptFinalValue{} - mi := &file_prompt_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PromptFinalValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PromptFinalValue) ProtoMessage() {} - -func (x *PromptFinalValue) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PromptFinalValue.ProtoReflect.Descriptor instead. -func (*PromptFinalValue) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{4} -} - -func (x *PromptFinalValue) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - type PromptSubscriptionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -332,7 +31,7 @@ type PromptSubscriptionRequest struct { func (x *PromptSubscriptionRequest) Reset() { *x = PromptSubscriptionRequest{} - mi := &file_prompt_proto_msgTypes[5] + mi := &file_prompt_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -344,7 +43,7 @@ func (x *PromptSubscriptionRequest) String() string { func (*PromptSubscriptionRequest) ProtoMessage() {} func (x *PromptSubscriptionRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[5] + mi := &file_prompt_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -357,7 +56,7 @@ func (x *PromptSubscriptionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptSubscriptionRequest.ProtoReflect.Descriptor instead. func (*PromptSubscriptionRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{5} + return file_prompt_proto_rawDescGZIP(), []int{0} } type PromptSubscriptionResponse struct { @@ -370,7 +69,7 @@ type PromptSubscriptionResponse struct { func (x *PromptSubscriptionResponse) Reset() { *x = PromptSubscriptionResponse{} - mi := &file_prompt_proto_msgTypes[6] + mi := &file_prompt_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -382,7 +81,7 @@ func (x *PromptSubscriptionResponse) String() string { func (*PromptSubscriptionResponse) ProtoMessage() {} func (x *PromptSubscriptionResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[6] + mi := &file_prompt_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -395,7 +94,7 @@ func (x *PromptSubscriptionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptSubscriptionResponse.ProtoReflect.Descriptor instead. func (*PromptSubscriptionResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{6} + return file_prompt_proto_rawDescGZIP(), []int{1} } func (x *PromptSubscriptionResponse) GetSubscription() *Subscription { @@ -415,7 +114,7 @@ type PromptLocationRequest struct { func (x *PromptLocationRequest) Reset() { *x = PromptLocationRequest{} - mi := &file_prompt_proto_msgTypes[7] + mi := &file_prompt_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -427,7 +126,7 @@ func (x *PromptLocationRequest) String() string { func (*PromptLocationRequest) ProtoMessage() {} func (x *PromptLocationRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[7] + mi := &file_prompt_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -440,7 +139,7 @@ func (x *PromptLocationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptLocationRequest.ProtoReflect.Descriptor instead. func (*PromptLocationRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{7} + return file_prompt_proto_rawDescGZIP(), []int{2} } func (x *PromptLocationRequest) GetAzureContext() *AzureContext { @@ -460,7 +159,7 @@ type PromptLocationResponse struct { func (x *PromptLocationResponse) Reset() { *x = PromptLocationResponse{} - mi := &file_prompt_proto_msgTypes[8] + mi := &file_prompt_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -472,7 +171,7 @@ func (x *PromptLocationResponse) String() string { func (*PromptLocationResponse) ProtoMessage() {} func (x *PromptLocationResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[8] + mi := &file_prompt_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -485,7 +184,7 @@ func (x *PromptLocationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptLocationResponse.ProtoReflect.Descriptor instead. func (*PromptLocationResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{8} + return file_prompt_proto_rawDescGZIP(), []int{3} } func (x *PromptLocationResponse) GetLocation() *Location { @@ -505,7 +204,7 @@ type PromptResourceGroupRequest struct { func (x *PromptResourceGroupRequest) Reset() { *x = PromptResourceGroupRequest{} - mi := &file_prompt_proto_msgTypes[9] + mi := &file_prompt_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -517,7 +216,7 @@ func (x *PromptResourceGroupRequest) String() string { func (*PromptResourceGroupRequest) ProtoMessage() {} func (x *PromptResourceGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[9] + mi := &file_prompt_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -530,7 +229,7 @@ func (x *PromptResourceGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptResourceGroupRequest.ProtoReflect.Descriptor instead. func (*PromptResourceGroupRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{9} + return file_prompt_proto_rawDescGZIP(), []int{4} } func (x *PromptResourceGroupRequest) GetAzureContext() *AzureContext { @@ -550,7 +249,7 @@ type PromptResourceGroupResponse struct { func (x *PromptResourceGroupResponse) Reset() { *x = PromptResourceGroupResponse{} - mi := &file_prompt_proto_msgTypes[10] + mi := &file_prompt_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -562,7 +261,7 @@ func (x *PromptResourceGroupResponse) String() string { func (*PromptResourceGroupResponse) ProtoMessage() {} func (x *PromptResourceGroupResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[10] + mi := &file_prompt_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -575,7 +274,7 @@ func (x *PromptResourceGroupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptResourceGroupResponse.ProtoReflect.Descriptor instead. func (*PromptResourceGroupResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{10} + return file_prompt_proto_rawDescGZIP(), []int{5} } func (x *PromptResourceGroupResponse) GetResourceGroup() *ResourceGroup { @@ -595,7 +294,7 @@ type ConfirmRequest struct { func (x *ConfirmRequest) Reset() { *x = ConfirmRequest{} - mi := &file_prompt_proto_msgTypes[11] + mi := &file_prompt_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -607,7 +306,7 @@ func (x *ConfirmRequest) String() string { func (*ConfirmRequest) ProtoMessage() {} func (x *ConfirmRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[11] + mi := &file_prompt_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -620,7 +319,7 @@ func (x *ConfirmRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmRequest.ProtoReflect.Descriptor instead. func (*ConfirmRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{11} + return file_prompt_proto_rawDescGZIP(), []int{6} } func (x *ConfirmRequest) GetOptions() *ConfirmOptions { @@ -640,7 +339,7 @@ type ConfirmResponse struct { func (x *ConfirmResponse) Reset() { *x = ConfirmResponse{} - mi := &file_prompt_proto_msgTypes[12] + mi := &file_prompt_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -652,7 +351,7 @@ func (x *ConfirmResponse) String() string { func (*ConfirmResponse) ProtoMessage() {} func (x *ConfirmResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[12] + mi := &file_prompt_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -665,7 +364,7 @@ func (x *ConfirmResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmResponse.ProtoReflect.Descriptor instead. func (*ConfirmResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{12} + return file_prompt_proto_rawDescGZIP(), []int{7} } func (x *ConfirmResponse) GetValue() bool { @@ -685,7 +384,7 @@ type PromptRequest struct { func (x *PromptRequest) Reset() { *x = PromptRequest{} - mi := &file_prompt_proto_msgTypes[13] + mi := &file_prompt_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -697,7 +396,7 @@ func (x *PromptRequest) String() string { func (*PromptRequest) ProtoMessage() {} func (x *PromptRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[13] + mi := &file_prompt_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -710,7 +409,7 @@ func (x *PromptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptRequest.ProtoReflect.Descriptor instead. func (*PromptRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{13} + return file_prompt_proto_rawDescGZIP(), []int{8} } func (x *PromptRequest) GetOptions() *PromptOptions { @@ -730,7 +429,7 @@ type PromptResponse struct { func (x *PromptResponse) Reset() { *x = PromptResponse{} - mi := &file_prompt_proto_msgTypes[14] + mi := &file_prompt_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -742,7 +441,7 @@ func (x *PromptResponse) String() string { func (*PromptResponse) ProtoMessage() {} func (x *PromptResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[14] + mi := &file_prompt_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -755,7 +454,7 @@ func (x *PromptResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptResponse.ProtoReflect.Descriptor instead. func (*PromptResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{14} + return file_prompt_proto_rawDescGZIP(), []int{9} } func (x *PromptResponse) GetValue() string { @@ -775,7 +474,7 @@ type SelectRequest struct { func (x *SelectRequest) Reset() { *x = SelectRequest{} - mi := &file_prompt_proto_msgTypes[15] + mi := &file_prompt_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -787,7 +486,7 @@ func (x *SelectRequest) String() string { func (*SelectRequest) ProtoMessage() {} func (x *SelectRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[15] + mi := &file_prompt_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -800,7 +499,7 @@ func (x *SelectRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectRequest.ProtoReflect.Descriptor instead. func (*SelectRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{15} + return file_prompt_proto_rawDescGZIP(), []int{10} } func (x *SelectRequest) GetOptions() *SelectOptions { @@ -820,7 +519,7 @@ type SelectResponse struct { func (x *SelectResponse) Reset() { *x = SelectResponse{} - mi := &file_prompt_proto_msgTypes[16] + mi := &file_prompt_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -832,7 +531,7 @@ func (x *SelectResponse) String() string { func (*SelectResponse) ProtoMessage() {} func (x *SelectResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[16] + mi := &file_prompt_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -845,7 +544,7 @@ func (x *SelectResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectResponse.ProtoReflect.Descriptor instead. func (*SelectResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{16} + return file_prompt_proto_rawDescGZIP(), []int{11} } func (x *SelectResponse) GetValue() int32 { @@ -866,7 +565,7 @@ type PromptSubscriptionResourceRequest struct { func (x *PromptSubscriptionResourceRequest) Reset() { *x = PromptSubscriptionResourceRequest{} - mi := &file_prompt_proto_msgTypes[17] + mi := &file_prompt_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -878,7 +577,7 @@ func (x *PromptSubscriptionResourceRequest) String() string { func (*PromptSubscriptionResourceRequest) ProtoMessage() {} func (x *PromptSubscriptionResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[17] + mi := &file_prompt_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -891,7 +590,7 @@ func (x *PromptSubscriptionResourceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use PromptSubscriptionResourceRequest.ProtoReflect.Descriptor instead. func (*PromptSubscriptionResourceRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{17} + return file_prompt_proto_rawDescGZIP(), []int{12} } func (x *PromptSubscriptionResourceRequest) GetAzureContext() *AzureContext { @@ -918,7 +617,7 @@ type PromptSubscriptionResourceResponse struct { func (x *PromptSubscriptionResourceResponse) Reset() { *x = PromptSubscriptionResourceResponse{} - mi := &file_prompt_proto_msgTypes[18] + mi := &file_prompt_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +629,7 @@ func (x *PromptSubscriptionResourceResponse) String() string { func (*PromptSubscriptionResourceResponse) ProtoMessage() {} func (x *PromptSubscriptionResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[18] + mi := &file_prompt_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +642,7 @@ func (x *PromptSubscriptionResourceResponse) ProtoReflect() protoreflect.Message // Deprecated: Use PromptSubscriptionResourceResponse.ProtoReflect.Descriptor instead. func (*PromptSubscriptionResourceResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{18} + return file_prompt_proto_rawDescGZIP(), []int{13} } func (x *PromptSubscriptionResourceResponse) GetResource() *ResourceExtended { @@ -964,7 +663,7 @@ type PromptResourceGroupResourceRequest struct { func (x *PromptResourceGroupResourceRequest) Reset() { *x = PromptResourceGroupResourceRequest{} - mi := &file_prompt_proto_msgTypes[19] + mi := &file_prompt_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -976,7 +675,7 @@ func (x *PromptResourceGroupResourceRequest) String() string { func (*PromptResourceGroupResourceRequest) ProtoMessage() {} func (x *PromptResourceGroupResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[19] + mi := &file_prompt_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -989,7 +688,7 @@ func (x *PromptResourceGroupResourceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use PromptResourceGroupResourceRequest.ProtoReflect.Descriptor instead. func (*PromptResourceGroupResourceRequest) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{19} + return file_prompt_proto_rawDescGZIP(), []int{14} } func (x *PromptResourceGroupResourceRequest) GetAzureContext() *AzureContext { @@ -1016,7 +715,7 @@ type PromptResourceGroupResourceResponse struct { func (x *PromptResourceGroupResourceResponse) Reset() { *x = PromptResourceGroupResourceResponse{} - mi := &file_prompt_proto_msgTypes[20] + mi := &file_prompt_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1028,7 +727,7 @@ func (x *PromptResourceGroupResourceResponse) String() string { func (*PromptResourceGroupResourceResponse) ProtoMessage() {} func (x *PromptResourceGroupResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[20] + mi := &file_prompt_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1041,7 +740,7 @@ func (x *PromptResourceGroupResourceResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use PromptResourceGroupResourceResponse.ProtoReflect.Descriptor instead. func (*PromptResourceGroupResourceResponse) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{20} + return file_prompt_proto_rawDescGZIP(), []int{15} } func (x *PromptResourceGroupResourceResponse) GetResource() *ResourceExtended { @@ -1065,7 +764,7 @@ type ConfirmOptions struct { func (x *ConfirmOptions) Reset() { *x = ConfirmOptions{} - mi := &file_prompt_proto_msgTypes[21] + mi := &file_prompt_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1077,7 +776,7 @@ func (x *ConfirmOptions) String() string { func (*ConfirmOptions) ProtoMessage() {} func (x *ConfirmOptions) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[21] + mi := &file_prompt_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1090,7 +789,7 @@ func (x *ConfirmOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmOptions.ProtoReflect.Descriptor instead. func (*ConfirmOptions) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{21} + return file_prompt_proto_rawDescGZIP(), []int{16} } func (x *ConfirmOptions) GetDefaultValue() bool { @@ -1147,7 +846,7 @@ type PromptOptions struct { func (x *PromptOptions) Reset() { *x = PromptOptions{} - mi := &file_prompt_proto_msgTypes[22] + mi := &file_prompt_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1159,7 +858,7 @@ func (x *PromptOptions) String() string { func (*PromptOptions) ProtoMessage() {} func (x *PromptOptions) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[22] + mi := &file_prompt_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1172,7 +871,7 @@ func (x *PromptOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptOptions.ProtoReflect.Descriptor instead. func (*PromptOptions) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{22} + return file_prompt_proto_rawDescGZIP(), []int{17} } func (x *PromptOptions) GetMessage() string { @@ -1262,7 +961,7 @@ type SelectOptions struct { func (x *SelectOptions) Reset() { *x = SelectOptions{} - mi := &file_prompt_proto_msgTypes[23] + mi := &file_prompt_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1274,7 +973,7 @@ func (x *SelectOptions) String() string { func (*SelectOptions) ProtoMessage() {} func (x *SelectOptions) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[23] + mi := &file_prompt_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1287,7 +986,7 @@ func (x *SelectOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use SelectOptions.ProtoReflect.Descriptor instead. func (*SelectOptions) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{23} + return file_prompt_proto_rawDescGZIP(), []int{18} } func (x *SelectOptions) GetSelectedIndex() int32 { @@ -1359,7 +1058,7 @@ type PromptResourceOptions struct { func (x *PromptResourceOptions) Reset() { *x = PromptResourceOptions{} - mi := &file_prompt_proto_msgTypes[24] + mi := &file_prompt_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1371,7 +1070,7 @@ func (x *PromptResourceOptions) String() string { func (*PromptResourceOptions) ProtoMessage() {} func (x *PromptResourceOptions) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[24] + mi := &file_prompt_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1384,7 +1083,7 @@ func (x *PromptResourceOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptResourceOptions.ProtoReflect.Descriptor instead. func (*PromptResourceOptions) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{24} + return file_prompt_proto_rawDescGZIP(), []int{19} } func (x *PromptResourceOptions) GetResourceType() string { @@ -1433,7 +1132,7 @@ type PromptResourceSelectOptions struct { func (x *PromptResourceSelectOptions) Reset() { *x = PromptResourceSelectOptions{} - mi := &file_prompt_proto_msgTypes[25] + mi := &file_prompt_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1445,7 +1144,7 @@ func (x *PromptResourceSelectOptions) String() string { func (*PromptResourceSelectOptions) ProtoMessage() {} func (x *PromptResourceSelectOptions) ProtoReflect() protoreflect.Message { - mi := &file_prompt_proto_msgTypes[25] + mi := &file_prompt_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1458,7 +1157,7 @@ func (x *PromptResourceSelectOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use PromptResourceSelectOptions.ProtoReflect.Descriptor instead. func (*PromptResourceSelectOptions) Descriptor() ([]byte, []int) { - return file_prompt_proto_rawDescGZIP(), []int{25} + return file_prompt_proto_rawDescGZIP(), []int{20} } func (x *PromptResourceSelectOptions) GetForceNewResource() bool { @@ -1529,35 +1228,7 @@ var File_prompt_proto protoreflect.FileDescriptor var file_prompt_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x1a, 0x0c, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x98, 0x01, 0x0a, 0x1c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x57, - 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, - 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, - 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x56, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, - 0x8f, 0x01, 0x0a, 0x1d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x36, - 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x22, 0x23, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x28, 0x0a, 0x10, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1b, 0x0a, 0x19, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x75, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1b, 0x0a, 0x19, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x56, 0x0a, 0x1a, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, @@ -1740,7 +1411,7 @@ var file_prompt_proto_rawDesc = []byte{ 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x75, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x32, 0x99, 0x06, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, + 0x62, 0x65, 0x72, 0x73, 0x32, 0xb8, 0x05, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x12, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x75, 0x62, 0x73, @@ -1783,18 +1454,12 @@ var file_prompt_proto_rawDesc = []byte{ 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x5f, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, - 0x24, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x57, - 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x25, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, - 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, - 0x7a, 0x75, 0x72, 0x65, 0x2f, 0x61, 0x7a, 0x75, 0x72, 0x65, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, - 0x6c, 0x69, 0x2f, 0x61, 0x7a, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x7a, 0x64, 0x65, 0x78, - 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x7a, + 0x75, 0x72, 0x65, 0x2f, 0x61, 0x7a, 0x75, 0x72, 0x65, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x6c, + 0x69, 0x2f, 0x61, 0x7a, 0x64, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, + 0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1809,83 +1474,72 @@ func file_prompt_proto_rawDescGZIP() []byte { return file_prompt_proto_rawDescData } -var file_prompt_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_prompt_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_prompt_proto_goTypes = []any{ - (*StreamWorkflowRequestMessage)(nil), // 0: azdext.StreamWorkflowRequestMessage - (*StreamWorkflowResponseMessage)(nil), // 1: azdext.StreamWorkflowResponseMessage - (*PromptValue)(nil), // 2: azdext.PromptValue - (*PromptValidation)(nil), // 3: azdext.PromptValidation - (*PromptFinalValue)(nil), // 4: azdext.PromptFinalValue - (*PromptSubscriptionRequest)(nil), // 5: azdext.PromptSubscriptionRequest - (*PromptSubscriptionResponse)(nil), // 6: azdext.PromptSubscriptionResponse - (*PromptLocationRequest)(nil), // 7: azdext.PromptLocationRequest - (*PromptLocationResponse)(nil), // 8: azdext.PromptLocationResponse - (*PromptResourceGroupRequest)(nil), // 9: azdext.PromptResourceGroupRequest - (*PromptResourceGroupResponse)(nil), // 10: azdext.PromptResourceGroupResponse - (*ConfirmRequest)(nil), // 11: azdext.ConfirmRequest - (*ConfirmResponse)(nil), // 12: azdext.ConfirmResponse - (*PromptRequest)(nil), // 13: azdext.PromptRequest - (*PromptResponse)(nil), // 14: azdext.PromptResponse - (*SelectRequest)(nil), // 15: azdext.SelectRequest - (*SelectResponse)(nil), // 16: azdext.SelectResponse - (*PromptSubscriptionResourceRequest)(nil), // 17: azdext.PromptSubscriptionResourceRequest - (*PromptSubscriptionResourceResponse)(nil), // 18: azdext.PromptSubscriptionResourceResponse - (*PromptResourceGroupResourceRequest)(nil), // 19: azdext.PromptResourceGroupResourceRequest - (*PromptResourceGroupResourceResponse)(nil), // 20: azdext.PromptResourceGroupResourceResponse - (*ConfirmOptions)(nil), // 21: azdext.ConfirmOptions - (*PromptOptions)(nil), // 22: azdext.PromptOptions - (*SelectOptions)(nil), // 23: azdext.SelectOptions - (*PromptResourceOptions)(nil), // 24: azdext.PromptResourceOptions - (*PromptResourceSelectOptions)(nil), // 25: azdext.PromptResourceSelectOptions - (*Subscription)(nil), // 26: azdext.Subscription - (*AzureContext)(nil), // 27: azdext.AzureContext - (*Location)(nil), // 28: azdext.Location - (*ResourceGroup)(nil), // 29: azdext.ResourceGroup - (*ResourceExtended)(nil), // 30: azdext.ResourceExtended + (*PromptSubscriptionRequest)(nil), // 0: azdext.PromptSubscriptionRequest + (*PromptSubscriptionResponse)(nil), // 1: azdext.PromptSubscriptionResponse + (*PromptLocationRequest)(nil), // 2: azdext.PromptLocationRequest + (*PromptLocationResponse)(nil), // 3: azdext.PromptLocationResponse + (*PromptResourceGroupRequest)(nil), // 4: azdext.PromptResourceGroupRequest + (*PromptResourceGroupResponse)(nil), // 5: azdext.PromptResourceGroupResponse + (*ConfirmRequest)(nil), // 6: azdext.ConfirmRequest + (*ConfirmResponse)(nil), // 7: azdext.ConfirmResponse + (*PromptRequest)(nil), // 8: azdext.PromptRequest + (*PromptResponse)(nil), // 9: azdext.PromptResponse + (*SelectRequest)(nil), // 10: azdext.SelectRequest + (*SelectResponse)(nil), // 11: azdext.SelectResponse + (*PromptSubscriptionResourceRequest)(nil), // 12: azdext.PromptSubscriptionResourceRequest + (*PromptSubscriptionResourceResponse)(nil), // 13: azdext.PromptSubscriptionResourceResponse + (*PromptResourceGroupResourceRequest)(nil), // 14: azdext.PromptResourceGroupResourceRequest + (*PromptResourceGroupResourceResponse)(nil), // 15: azdext.PromptResourceGroupResourceResponse + (*ConfirmOptions)(nil), // 16: azdext.ConfirmOptions + (*PromptOptions)(nil), // 17: azdext.PromptOptions + (*SelectOptions)(nil), // 18: azdext.SelectOptions + (*PromptResourceOptions)(nil), // 19: azdext.PromptResourceOptions + (*PromptResourceSelectOptions)(nil), // 20: azdext.PromptResourceSelectOptions + (*Subscription)(nil), // 21: azdext.Subscription + (*AzureContext)(nil), // 22: azdext.AzureContext + (*Location)(nil), // 23: azdext.Location + (*ResourceGroup)(nil), // 24: azdext.ResourceGroup + (*ResourceExtended)(nil), // 25: azdext.ResourceExtended } var file_prompt_proto_depIdxs = []int32{ - 22, // 0: azdext.StreamWorkflowRequestMessage.options:type_name -> azdext.PromptOptions - 3, // 1: azdext.StreamWorkflowRequestMessage.validation:type_name -> azdext.PromptValidation - 2, // 2: azdext.StreamWorkflowResponseMessage.value:type_name -> azdext.PromptValue - 4, // 3: azdext.StreamWorkflowResponseMessage.complete:type_name -> azdext.PromptFinalValue - 26, // 4: azdext.PromptSubscriptionResponse.subscription:type_name -> azdext.Subscription - 27, // 5: azdext.PromptLocationRequest.azure_context:type_name -> azdext.AzureContext - 28, // 6: azdext.PromptLocationResponse.location:type_name -> azdext.Location - 27, // 7: azdext.PromptResourceGroupRequest.azure_context:type_name -> azdext.AzureContext - 29, // 8: azdext.PromptResourceGroupResponse.resource_group:type_name -> azdext.ResourceGroup - 21, // 9: azdext.ConfirmRequest.options:type_name -> azdext.ConfirmOptions - 22, // 10: azdext.PromptRequest.options:type_name -> azdext.PromptOptions - 23, // 11: azdext.SelectRequest.options:type_name -> azdext.SelectOptions - 27, // 12: azdext.PromptSubscriptionResourceRequest.azure_context:type_name -> azdext.AzureContext - 24, // 13: azdext.PromptSubscriptionResourceRequest.options:type_name -> azdext.PromptResourceOptions - 30, // 14: azdext.PromptSubscriptionResourceResponse.resource:type_name -> azdext.ResourceExtended - 27, // 15: azdext.PromptResourceGroupResourceRequest.azure_context:type_name -> azdext.AzureContext - 24, // 16: azdext.PromptResourceGroupResourceRequest.options:type_name -> azdext.PromptResourceOptions - 30, // 17: azdext.PromptResourceGroupResourceResponse.resource:type_name -> azdext.ResourceExtended - 25, // 18: azdext.PromptResourceOptions.select_options:type_name -> azdext.PromptResourceSelectOptions - 5, // 19: azdext.PromptService.PromptSubscription:input_type -> azdext.PromptSubscriptionRequest - 7, // 20: azdext.PromptService.PromptLocation:input_type -> azdext.PromptLocationRequest - 9, // 21: azdext.PromptService.PromptResourceGroup:input_type -> azdext.PromptResourceGroupRequest - 11, // 22: azdext.PromptService.Confirm:input_type -> azdext.ConfirmRequest - 13, // 23: azdext.PromptService.Prompt:input_type -> azdext.PromptRequest - 15, // 24: azdext.PromptService.Select:input_type -> azdext.SelectRequest - 17, // 25: azdext.PromptService.PromptSubscriptionResource:input_type -> azdext.PromptSubscriptionResourceRequest - 19, // 26: azdext.PromptService.PromptResourceGroupResource:input_type -> azdext.PromptResourceGroupResourceRequest - 0, // 27: azdext.PromptService.PromptStream:input_type -> azdext.StreamWorkflowRequestMessage - 6, // 28: azdext.PromptService.PromptSubscription:output_type -> azdext.PromptSubscriptionResponse - 8, // 29: azdext.PromptService.PromptLocation:output_type -> azdext.PromptLocationResponse - 10, // 30: azdext.PromptService.PromptResourceGroup:output_type -> azdext.PromptResourceGroupResponse - 12, // 31: azdext.PromptService.Confirm:output_type -> azdext.ConfirmResponse - 14, // 32: azdext.PromptService.Prompt:output_type -> azdext.PromptResponse - 16, // 33: azdext.PromptService.Select:output_type -> azdext.SelectResponse - 18, // 34: azdext.PromptService.PromptSubscriptionResource:output_type -> azdext.PromptSubscriptionResourceResponse - 20, // 35: azdext.PromptService.PromptResourceGroupResource:output_type -> azdext.PromptResourceGroupResourceResponse - 1, // 36: azdext.PromptService.PromptStream:output_type -> azdext.StreamWorkflowResponseMessage - 28, // [28:37] is the sub-list for method output_type - 19, // [19:28] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 21, // 0: azdext.PromptSubscriptionResponse.subscription:type_name -> azdext.Subscription + 22, // 1: azdext.PromptLocationRequest.azure_context:type_name -> azdext.AzureContext + 23, // 2: azdext.PromptLocationResponse.location:type_name -> azdext.Location + 22, // 3: azdext.PromptResourceGroupRequest.azure_context:type_name -> azdext.AzureContext + 24, // 4: azdext.PromptResourceGroupResponse.resource_group:type_name -> azdext.ResourceGroup + 16, // 5: azdext.ConfirmRequest.options:type_name -> azdext.ConfirmOptions + 17, // 6: azdext.PromptRequest.options:type_name -> azdext.PromptOptions + 18, // 7: azdext.SelectRequest.options:type_name -> azdext.SelectOptions + 22, // 8: azdext.PromptSubscriptionResourceRequest.azure_context:type_name -> azdext.AzureContext + 19, // 9: azdext.PromptSubscriptionResourceRequest.options:type_name -> azdext.PromptResourceOptions + 25, // 10: azdext.PromptSubscriptionResourceResponse.resource:type_name -> azdext.ResourceExtended + 22, // 11: azdext.PromptResourceGroupResourceRequest.azure_context:type_name -> azdext.AzureContext + 19, // 12: azdext.PromptResourceGroupResourceRequest.options:type_name -> azdext.PromptResourceOptions + 25, // 13: azdext.PromptResourceGroupResourceResponse.resource:type_name -> azdext.ResourceExtended + 20, // 14: azdext.PromptResourceOptions.select_options:type_name -> azdext.PromptResourceSelectOptions + 0, // 15: azdext.PromptService.PromptSubscription:input_type -> azdext.PromptSubscriptionRequest + 2, // 16: azdext.PromptService.PromptLocation:input_type -> azdext.PromptLocationRequest + 4, // 17: azdext.PromptService.PromptResourceGroup:input_type -> azdext.PromptResourceGroupRequest + 6, // 18: azdext.PromptService.Confirm:input_type -> azdext.ConfirmRequest + 8, // 19: azdext.PromptService.Prompt:input_type -> azdext.PromptRequest + 10, // 20: azdext.PromptService.Select:input_type -> azdext.SelectRequest + 12, // 21: azdext.PromptService.PromptSubscriptionResource:input_type -> azdext.PromptSubscriptionResourceRequest + 14, // 22: azdext.PromptService.PromptResourceGroupResource:input_type -> azdext.PromptResourceGroupResourceRequest + 1, // 23: azdext.PromptService.PromptSubscription:output_type -> azdext.PromptSubscriptionResponse + 3, // 24: azdext.PromptService.PromptLocation:output_type -> azdext.PromptLocationResponse + 5, // 25: azdext.PromptService.PromptResourceGroup:output_type -> azdext.PromptResourceGroupResponse + 7, // 26: azdext.PromptService.Confirm:output_type -> azdext.ConfirmResponse + 9, // 27: azdext.PromptService.Prompt:output_type -> azdext.PromptResponse + 11, // 28: azdext.PromptService.Select:output_type -> azdext.SelectResponse + 13, // 29: azdext.PromptService.PromptSubscriptionResource:output_type -> azdext.PromptSubscriptionResourceResponse + 15, // 30: azdext.PromptService.PromptResourceGroupResource:output_type -> azdext.PromptResourceGroupResourceResponse + 23, // [23:31] is the sub-list for method output_type + 15, // [15:23] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_prompt_proto_init() } @@ -1894,26 +1548,18 @@ func file_prompt_proto_init() { return } file_models_proto_init() - file_prompt_proto_msgTypes[0].OneofWrappers = []any{ - (*StreamWorkflowRequestMessage_Options)(nil), - (*StreamWorkflowRequestMessage_Validation)(nil), - } - file_prompt_proto_msgTypes[1].OneofWrappers = []any{ - (*StreamWorkflowResponseMessage_Value)(nil), - (*StreamWorkflowResponseMessage_Complete)(nil), - } - file_prompt_proto_msgTypes[12].OneofWrappers = []any{} + file_prompt_proto_msgTypes[7].OneofWrappers = []any{} + file_prompt_proto_msgTypes[11].OneofWrappers = []any{} file_prompt_proto_msgTypes[16].OneofWrappers = []any{} - file_prompt_proto_msgTypes[21].OneofWrappers = []any{} - file_prompt_proto_msgTypes[23].OneofWrappers = []any{} - file_prompt_proto_msgTypes[25].OneofWrappers = []any{} + file_prompt_proto_msgTypes[18].OneofWrappers = []any{} + file_prompt_proto_msgTypes[20].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_prompt_proto_rawDesc, NumEnums: 0, - NumMessages: 26, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/prompt_grpc.pb.go b/cli/azd/pkg/azdext/prompt_grpc.pb.go index 9b5e780977e..f1baf551db2 100644 --- a/cli/azd/pkg/azdext/prompt_grpc.pb.go +++ b/cli/azd/pkg/azdext/prompt_grpc.pb.go @@ -30,7 +30,6 @@ const ( PromptService_Select_FullMethodName = "/azdext.PromptService/Select" PromptService_PromptSubscriptionResource_FullMethodName = "/azdext.PromptService/PromptSubscriptionResource" PromptService_PromptResourceGroupResource_FullMethodName = "/azdext.PromptService/PromptResourceGroupResource" - PromptService_PromptStream_FullMethodName = "/azdext.PromptService/PromptStream" ) // PromptServiceClient is the client API for PromptService service. @@ -45,7 +44,6 @@ type PromptServiceClient interface { Select(ctx context.Context, in *SelectRequest, opts ...grpc.CallOption) (*SelectResponse, error) PromptSubscriptionResource(ctx context.Context, in *PromptSubscriptionResourceRequest, opts ...grpc.CallOption) (*PromptSubscriptionResourceResponse, error) PromptResourceGroupResource(ctx context.Context, in *PromptResourceGroupResourceRequest, opts ...grpc.CallOption) (*PromptResourceGroupResourceResponse, error) - PromptStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage], error) } type promptServiceClient struct { @@ -136,19 +134,6 @@ func (c *promptServiceClient) PromptResourceGroupResource(ctx context.Context, i return out, nil } -func (c *promptServiceClient) PromptStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &PromptService_ServiceDesc.Streams[0], PromptService_PromptStream_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type PromptService_PromptStreamClient = grpc.BidiStreamingClient[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage] - // PromptServiceServer is the server API for PromptService service. // All implementations must embed UnimplementedPromptServiceServer // for forward compatibility. @@ -161,7 +146,6 @@ type PromptServiceServer interface { Select(context.Context, *SelectRequest) (*SelectResponse, error) PromptSubscriptionResource(context.Context, *PromptSubscriptionResourceRequest) (*PromptSubscriptionResourceResponse, error) PromptResourceGroupResource(context.Context, *PromptResourceGroupResourceRequest) (*PromptResourceGroupResourceResponse, error) - PromptStream(grpc.BidiStreamingServer[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage]) error mustEmbedUnimplementedPromptServiceServer() } @@ -196,9 +180,6 @@ func (UnimplementedPromptServiceServer) PromptSubscriptionResource(context.Conte func (UnimplementedPromptServiceServer) PromptResourceGroupResource(context.Context, *PromptResourceGroupResourceRequest) (*PromptResourceGroupResourceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method PromptResourceGroupResource not implemented") } -func (UnimplementedPromptServiceServer) PromptStream(grpc.BidiStreamingServer[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage]) error { - return status.Errorf(codes.Unimplemented, "method PromptStream not implemented") -} func (UnimplementedPromptServiceServer) mustEmbedUnimplementedPromptServiceServer() {} func (UnimplementedPromptServiceServer) testEmbeddedByValue() {} @@ -364,13 +345,6 @@ func _PromptService_PromptResourceGroupResource_Handler(srv interface{}, ctx con return interceptor(ctx, in, info, handler) } -func _PromptService_PromptStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(PromptServiceServer).PromptStream(&grpc.GenericServerStream[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type PromptService_PromptStreamServer = grpc.BidiStreamingServer[StreamWorkflowRequestMessage, StreamWorkflowResponseMessage] - // PromptService_ServiceDesc is the grpc.ServiceDesc for PromptService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -411,13 +385,6 @@ var PromptService_ServiceDesc = grpc.ServiceDesc{ Handler: _PromptService_PromptResourceGroupResource_Handler, }, }, - Streams: []grpc.StreamDesc{ - { - StreamName: "PromptStream", - Handler: _PromptService_PromptStream_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, + Streams: []grpc.StreamDesc{}, Metadata: "prompt.proto", } diff --git a/cli/azd/pkg/environment/manager.go b/cli/azd/pkg/environment/manager.go index 05ce359642c..1fd93ded735 100644 --- a/cli/azd/pkg/environment/manager.go +++ b/cli/azd/pkg/environment/manager.go @@ -53,6 +53,9 @@ var ( // Error returned when an environment name is not specified ErrNameNotSpecified = errors.New("environment not specified") + + // Error returned when the default environment cannot be found + ErrDefaultEnvironmentNotFound = errors.New("default environment not found") ) // Manager is the interface used for managing instances of environments diff --git a/cli/azd/pkg/project/framework_service.go b/cli/azd/pkg/project/framework_service.go index ef9518be5c8..0950898576d 100644 --- a/cli/azd/pkg/project/framework_service.go +++ b/cli/azd/pkg/project/framework_service.go @@ -41,7 +41,8 @@ func parseServiceLanguage(kind ServiceLanguageKind) (ServiceLanguageKind, error) ServiceLanguageJavaScript, ServiceLanguageTypeScript, ServiceLanguagePython, - ServiceLanguageJava: + ServiceLanguageJava, + ServiceLanguageDocker: // Excluding ServiceLanguageDocker and ServiceLanguageSwa since it is implicitly derived currently, // and not an actual language return kind, nil diff --git a/cli/azd/pkg/project/resource_manager.go b/cli/azd/pkg/project/resource_manager.go index 9f411e7a5a1..8ca8a2cdbf7 100644 --- a/cli/azd/pkg/project/resource_manager.go +++ b/cli/azd/pkg/project/resource_manager.go @@ -31,14 +31,14 @@ type ResourceManager interface { subscriptionId string, resourceGroupName string, serviceConfig *ServiceConfig, - ) ([]*azapi.Resource, error) + ) ([]*azapi.ResourceExtended, error) GetServiceResource( ctx context.Context, subscriptionId string, resourceGroupName string, serviceConfig *ServiceConfig, rerunCommand string, - ) (*azapi.Resource, error) + ) (*azapi.ResourceExtended, error) GetTargetResource( ctx context.Context, subscriptionId string, @@ -113,7 +113,7 @@ func (rm *resourceManager) GetServiceResources( subscriptionId string, resourceGroupName string, serviceConfig *ServiceConfig, -) ([]*azapi.Resource, error) { +) ([]*azapi.ResourceExtended, error) { filter := fmt.Sprintf("tagName eq '%s' and tagValue eq '%s'", azure.TagKeyAzdServiceName, serviceConfig.Name) subst, err := serviceConfig.ResourceName.Envsubst(rm.env.Getenv) @@ -145,7 +145,7 @@ func (rm *resourceManager) GetServiceResource( resourceGroupName string, serviceConfig *ServiceConfig, rerunCommand string, -) (*azapi.Resource, error) { +) (*azapi.ResourceExtended, error) { expandedResourceName, err := serviceConfig.ResourceName.Envsubst(rm.env.Getenv) if err != nil { return nil, fmt.Errorf("expanding name: %w", err) @@ -237,7 +237,7 @@ func (rm *resourceManager) resolveServiceResource( resourceGroupName string, serviceConfig *ServiceConfig, rerunCommand string, -) (*azapi.Resource, error) { +) (*azapi.ResourceExtended, error) { azureResource, err := rm.GetServiceResource(ctx, subscriptionId, resourceGroupName, serviceConfig, rerunCommand) // If the service target supports delayed provisioning, the resource isn't expected to be found yet. @@ -246,7 +246,7 @@ func (rm *resourceManager) resolveServiceResource( if err != nil && errors.As(err, &resourceNotFoundError) && ServiceTargetKind(serviceConfig.Host).SupportsDelayedProvisioning() { - return &azapi.Resource{}, nil + return &azapi.ResourceExtended{}, nil } if err != nil { diff --git a/cli/azd/pkg/project/service_target_aks_test.go b/cli/azd/pkg/project/service_target_aks_test.go index 6b4ba4a8ccc..cfea8a74b43 100644 --- a/cli/azd/pkg/project/service_target_aks_test.go +++ b/cli/azd/pkg/project/service_target_aks_test.go @@ -955,9 +955,9 @@ func (m *MockResourceManager) GetServiceResources( subscriptionId string, resourceGroupName string, serviceConfig *ServiceConfig, -) ([]*azapi.Resource, error) { +) ([]*azapi.ResourceExtended, error) { args := m.Called(ctx, subscriptionId, resourceGroupName, serviceConfig) - return args.Get(0).([]*azapi.Resource), args.Error(1) + return args.Get(0).([]*azapi.ResourceExtended), args.Error(1) } func (m *MockResourceManager) GetServiceResource( @@ -966,9 +966,9 @@ func (m *MockResourceManager) GetServiceResource( resourceGroupName string, serviceConfig *ServiceConfig, rerunCommand string, -) (*azapi.Resource, error) { +) (*azapi.ResourceExtended, error) { args := m.Called(ctx, subscriptionId, resourceGroupName, serviceConfig, rerunCommand) - return args.Get(0).(*azapi.Resource), args.Error(1) + return args.Get(0).(*azapi.ResourceExtended), args.Error(1) } func (m *MockResourceManager) GetTargetResource( diff --git a/cli/azd/pkg/prompt/azure_context_test.go b/cli/azd/pkg/prompt/azure_context_test.go new file mode 100644 index 00000000000..741480b53b9 --- /dev/null +++ b/cli/azd/pkg/prompt/azure_context_test.go @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package prompt + +import ( + "context" + "fmt" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var ( + mockContextType = mock.Anything + mockSelectOptionsType = mock.AnythingOfType("*prompt.SelectOptions") + mockAzureContextType = mock.AnythingOfType("*prompt.AzureContext") + mockResourceGroupOptions = mock.AnythingOfType("*prompt.ResourceGroupOptions") +) + +func TestAzureContext_EnsureSubscription(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{}, nil) + + mockPromptService.On("PromptSubscription", mockContextType, mockSelectOptionsType). + Return(&account.Subscription{ + Id: "test-subscription-id", + TenantId: "test-tenant-id", + }, nil) + + err := azureContext.EnsureSubscription(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-subscription-id", azureContext.Scope.SubscriptionId) + require.Equal(t, "test-tenant-id", azureContext.Scope.TenantId) + + mockPromptService.AssertCalled(t, "PromptSubscription", mockContextType, mockSelectOptionsType) +} + +func TestAzureContext_EnsureSubscription_NoPrompt(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + TenantId: "test-tenant-id", + }, nil) + + err := azureContext.EnsureSubscription(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-subscription-id", azureContext.Scope.SubscriptionId) + require.Equal(t, "test-tenant-id", azureContext.Scope.TenantId) + + mockPromptService.AssertNotCalled(t, "PromptSubscription", mock.Anything, mock.Anything) +} + +func TestAzureContext_EnsureSubscription_Error(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{}, nil) + + mockPromptService.On("PromptSubscription", mockContextType, mockSelectOptionsType). + Return(nil, fmt.Errorf("subscription error")) + + err := azureContext.EnsureSubscription(context.Background()) + require.Error(t, err) + require.Equal(t, "", azureContext.Scope.SubscriptionId) + require.Equal(t, "", azureContext.Scope.TenantId) +} + +func TestAzureContext_EnsureResourceGroup(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + }, nil) + + mockPromptService.On("PromptResourceGroup", mockContextType, mockAzureContextType, mockResourceGroupOptions). + Return(&azapi.ResourceGroup{ + Name: "test-resource-group", + }, nil) + + err := azureContext.EnsureResourceGroup(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-resource-group", azureContext.Scope.ResourceGroup) + + mockPromptService.AssertCalled(t, "PromptResourceGroup", mockContextType, mockAzureContextType, mockResourceGroupOptions) +} + +func TestAzureContext_EnsureResourceGroup_NoPrompt(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + ResourceGroup: "test-resource-group", + }, nil) + + err := azureContext.EnsureResourceGroup(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-resource-group", azureContext.Scope.ResourceGroup) + + mockPromptService.AssertNotCalled(t, "PromptResourceGroup", mock.Anything, mock.Anything, mock.Anything) +} + +func TestAzureContext_EnsureResourceGroup_Error(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + }, nil) + + mockPromptService.On("PromptResourceGroup", mockContextType, mockAzureContextType, mockResourceGroupOptions). + Return(nil, fmt.Errorf("resource group error")) + + err := azureContext.EnsureResourceGroup(context.Background()) + require.Error(t, err) + require.Equal(t, "", azureContext.Scope.ResourceGroup) +} + +func TestAzureContext_EnsureLocation(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + }, nil) + + mockPromptService.On("PromptLocation", mockContextType, mockAzureContextType, mockSelectOptionsType). + Return(&account.Location{ + Name: "test-location", + }, nil) + + err := azureContext.EnsureLocation(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-location", azureContext.Scope.Location) + + mockPromptService.AssertCalled(t, "PromptLocation", mockContextType, mockAzureContextType, mockSelectOptionsType) +} + +func TestAzureContext_EnsureLocation_NoPrompt(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + Location: "test-location", + }, nil) + + err := azureContext.EnsureLocation(context.Background()) + require.NoError(t, err) + require.Equal(t, "test-location", azureContext.Scope.Location) + + mockPromptService.AssertNotCalled(t, "PromptLocation", mock.Anything, mock.Anything, mock.Anything) +} + +func TestAzureContext_EnsureLocation_Error(t *testing.T) { + mockPromptService := &MockPromptService{} + azureContext := NewAzureContext(mockPromptService, AzureScope{ + SubscriptionId: "test-subscription-id", + }, nil) + + mockPromptService.On("PromptLocation", mockContextType, mockAzureContextType, mockSelectOptionsType). + Return(nil, fmt.Errorf("location error")) + + err := azureContext.EnsureLocation(context.Background()) + require.Error(t, err) + require.Equal(t, "", azureContext.Scope.Location) +} + +type MockPromptService struct { + mock.Mock +} + +func (m *MockPromptService) PromptSubscription( + ctx context.Context, + selectorOptions *SelectOptions, +) (*account.Subscription, error) { + args := m.Called(ctx, selectorOptions) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*account.Subscription), args.Error(1) +} + +func (m *MockPromptService) PromptLocation( + ctx context.Context, + azureContext *AzureContext, + selectorOptions *SelectOptions, +) (*account.Location, error) { + args := m.Called(ctx, azureContext, selectorOptions) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*account.Location), args.Error(1) +} + +func (m *MockPromptService) PromptResourceGroup( + ctx context.Context, + azureContext *AzureContext, + options *ResourceGroupOptions, +) (*azapi.ResourceGroup, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceGroup), args.Error(1) +} + +func (m *MockPromptService) PromptSubscriptionResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, +) (*azapi.ResourceExtended, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceExtended), args.Error(1) +} + +func (m *MockPromptService) PromptResourceGroupResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, +) (*azapi.ResourceExtended, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceExtended), args.Error(1) +} diff --git a/cli/azd/pkg/prompt/azure_resource_list_test.go b/cli/azd/pkg/prompt/azure_resource_list_test.go new file mode 100644 index 00000000000..7096c4d20b5 --- /dev/null +++ b/cli/azd/pkg/prompt/azure_resource_list_test.go @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package prompt + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockazapi" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const ( + // nolint:lll + testVMResourceID = "/subscriptions/123/resourceGroups/testGroup/providers/Microsoft.Compute/virtualMachines/testVM" + // nolint:lll + testVM2ResourceID = "/subscriptions/123/resourceGroups/testGroup/providers/Microsoft.Compute/virtualMachines/testVM2" + // nolint:lll + nonExistentVMResourceID = "/subscriptions/123/resourceGroups/testGroup/providers/Microsoft.Compute/virtualMachines/nonExistentVM" +) + +func TestAzureResourceList_Add(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + require.Equal(t, 1, len(resourceList.resources)) +} + +func TestAzureResourceList_FindById(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resource, found := resourceList.FindById(testVMResourceID) + require.True(t, found) + require.NotNil(t, resource) +} + +func TestAzureResourceList_FindById_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resource, found := resourceList.FindById(nonExistentVMResourceID) + require.False(t, found) + require.Nil(t, resource) +} + +func TestAzureResourceList_FindByType(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resource, found := resourceList.FindByType("Microsoft.Compute/virtualMachines") + require.True(t, found) + require.NotNil(t, resource) +} + +func TestAzureResourceList_FindByType_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resource, found := resourceList.FindByType("Microsoft.Compute/nonExistentType") + require.False(t, found) + require.Nil(t, resource) +} + +func TestAzureResourceList_FindByTypeAndKind(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resourceService.On( + "GetResource", + mock.Anything, + "123", + testVMResourceID, + "", + ).Return(azapi.ResourceExtended{Kind: "testKind"}, nil) + + resource, found := resourceList.FindByTypeAndKind( + context.Background(), + "Microsoft.Compute/virtualMachines", + []string{"testKind"}, + ) + require.True(t, found) + require.NotNil(t, resource) +} + +func TestAzureResourceList_FindByTypeAndKind_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resourceService.On( + "GetResource", + mock.Anything, + "123", + testVMResourceID, + "", + ).Return(azapi.ResourceExtended{Kind: "testKind"}, nil) + + resource, found := resourceList.FindByTypeAndKind( + context.Background(), + "Microsoft.Compute/virtualMachines", + []string{"nonExistentKind"}, + ) + require.False(t, found) + require.Nil(t, resource) +} + +func TestAzureResourceList_Find(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resource, found := resourceList.Find(func(resourceId *arm.ResourceID) bool { + return resourceId.Name == "testVM" + }) + require.True(t, found) + require.NotNil(t, resource) +} + +func TestAzureResourceList_Find_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resource, found := resourceList.Find(func(resourceId *arm.ResourceID) bool { + return resourceId.Name == "nonExistentVM" + }) + require.False(t, found) + require.Nil(t, resource) +} + +func TestAzureResourceList_FindAll(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + err = resourceList.Add(testVM2ResourceID) + require.NoError(t, err) + + resources, found := resourceList.FindAll(func(resourceId *arm.ResourceID) bool { + return resourceId.ResourceType.String() == "Microsoft.Compute/virtualMachines" + }) + require.True(t, found) + require.Equal(t, 2, len(resources)) +} + +func TestAzureResourceList_FindAll_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resources, found := resourceList.FindAll(func(resourceId *arm.ResourceID) bool { + return resourceId.ResourceType.String() == "Microsoft.Compute/nonExistentType" + }) + require.False(t, found) + require.Equal(t, 0, len(resources)) +} + +func TestAzureResourceList_FindAllByType(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + err = resourceList.Add(testVM2ResourceID) + require.NoError(t, err) + + resources, found := resourceList.FindAllByType("Microsoft.Compute/virtualMachines") + require.True(t, found) + require.Equal(t, 2, len(resources)) +} + +func TestAzureResourceList_FindAllByType_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resources, found := resourceList.FindAllByType("Microsoft.Compute/nonExistentType") + require.False(t, found) + require.Equal(t, 0, len(resources)) +} + +func TestAzureResourceList_FindByTypeAndName(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + err := resourceList.Add(testVMResourceID) + require.NoError(t, err) + + resource, found := resourceList.FindByTypeAndName("Microsoft.Compute/virtualMachines", "testVM") + require.True(t, found) + require.NotNil(t, resource) +} + +func TestAzureResourceList_FindByTypeAndName_NotFound(t *testing.T) { + resourceService := &mockazapi.MockResourceService{} + resourceList := NewAzureResourceList(resourceService, nil) + + resource, found := resourceList.FindByTypeAndName("Microsoft.Compute/virtualMachines", "nonExistentVM") + require.False(t, found) + require.Nil(t, resource) +} diff --git a/cli/azd/pkg/prompt/prompt_models.go b/cli/azd/pkg/prompt/prompt_models.go new file mode 100644 index 00000000000..0676ffb2f14 --- /dev/null +++ b/cli/azd/pkg/prompt/prompt_models.go @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package prompt + +import ( + "context" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" +) + +// Azure Scope contains the high level metadata about an Azure deployment. +type AzureScope struct { + TenantId string + SubscriptionId string + Location string + ResourceGroup string +} + +// AzureResourceList contains a list of Azure resources. +type AzureResourceList struct { + resourceService ResourceService + resources []*arm.ResourceID +} + +// NewAzureResourceList creates a new Azure resource list. +func NewAzureResourceList(resourceService ResourceService, resources []*arm.ResourceID) *AzureResourceList { + if resources == nil { + resources = []*arm.ResourceID{} + } + + return &AzureResourceList{ + resourceService: resourceService, + resources: resources, + } +} + +// Add adds an Azure resource to the list. +func (arl *AzureResourceList) Add(resourceId string) error { + if _, has := arl.FindById(resourceId); has { + return nil + } + + parsedResource, err := arm.ParseResourceID(resourceId) + if err != nil { + return err + } + + arl.resources = append(arl.resources, parsedResource) + + return nil +} + +// Find finds the first Azure resource matched by the predicate function. +func (arl *AzureResourceList) Find(predicate func(resourceId *arm.ResourceID) bool) (*arm.ResourceID, bool) { + for _, resource := range arl.resources { + if predicate(resource) { + return resource, true + } + } + + return nil, false +} + +// FindAll finds all Azure resources matched by the predicate function. +func (arl *AzureResourceList) FindAll(predicate func(resourceId *arm.ResourceID) bool) ([]*arm.ResourceID, bool) { + matches := []*arm.ResourceID{} + + for _, resource := range arl.resources { + if predicate(resource) { + matches = append(matches, resource) + } + } + + return matches, len(matches) > 0 +} + +// FindByType finds the first Azure resource by the specified type. +func (arl *AzureResourceList) FindByType(resourceType azapi.AzureResourceType) (*arm.ResourceID, bool) { + return arl.Find(func(resourceId *arm.ResourceID) bool { + return strings.EqualFold(resourceId.ResourceType.String(), string(resourceType)) + }) +} + +// FindAllByType finds all Azure resources by the specified type. +func (arl *AzureResourceList) FindAllByType(resourceType azapi.AzureResourceType) ([]*arm.ResourceID, bool) { + return arl.FindAll(func(resourceId *arm.ResourceID) bool { + return strings.EqualFold(resourceId.ResourceType.String(), string(resourceType)) + }) +} + +// FindByTypeAndKind finds the first Azure resource by the specified type and kind. +func (arl *AzureResourceList) FindByTypeAndKind( + ctx context.Context, + resourceType azapi.AzureResourceType, + kinds []string, +) (*arm.ResourceID, bool) { + typeMatches, has := arl.FindAllByType(resourceType) + if !has { + return nil, false + } + + // When no kinds are specified, return the first resource found + if len(kinds) == 0 { + return typeMatches[0], true + } + + // When kinds are specified, check if the resource kind matches + for _, typeMatch := range typeMatches { + resource, err := arl.resourceService.GetResource(ctx, typeMatch.SubscriptionID, typeMatch.String(), "") + if err != nil { + return nil, false + } + + for _, kind := range kinds { + if strings.EqualFold(kind, resource.Kind) { + return typeMatch, true + } + } + } + + return nil, false +} + +// FindById finds the first Azure resource by the specified ID. +func (arl *AzureResourceList) FindById(resourceId string) (*arm.ResourceID, bool) { + return arl.Find(func(resource *arm.ResourceID) bool { + return strings.EqualFold(resource.String(), resourceId) + }) +} + +// FindByTypeAndName finds the first Azure resource by the specified type and name. +func (arl *AzureResourceList) FindByTypeAndName( + resourceType azapi.AzureResourceType, + resourceName string, +) (*arm.ResourceID, bool) { + return arl.Find(func(resource *arm.ResourceID) bool { + return strings.EqualFold(resource.ResourceType.String(), string(resourceType)) && + strings.EqualFold(resource.Name, resourceName) + }) +} + +// AzureContext contains the Scope and list of Resources from an Azure deployment. +type AzureContext struct { + Scope AzureScope + Resources *AzureResourceList + promptService PromptService +} + +// NewEmptyAzureContext creates a new empty Azure context. +func NewEmptyAzureContext() *AzureContext { + return &AzureContext{ + Scope: AzureScope{}, + Resources: &AzureResourceList{}, + } +} + +// NewAzureContext creates a new Azure context. +func NewAzureContext( + promptService PromptService, + scope AzureScope, + resourceList *AzureResourceList, +) *AzureContext { + return &AzureContext{ + Scope: scope, + Resources: resourceList, + promptService: promptService, + } +} + +// EnsureSubscription ensures that the Azure context has a subscription. +// If the subscription is not set, the user is prompted to select a subscription. +func (pc *AzureContext) EnsureSubscription(ctx context.Context) error { + if pc.Scope.SubscriptionId == "" { + subscription, err := pc.promptService.PromptSubscription(ctx, nil) + if err != nil { + return err + } + + pc.Scope.TenantId = subscription.TenantId + pc.Scope.SubscriptionId = subscription.Id + } + + return nil +} + +// EnsureResourceGroup ensures that the Azure context has a resource group. +// If the resource group is not set, the user is prompted to select a resource group. +func (pc *AzureContext) EnsureResourceGroup(ctx context.Context) error { + if pc.Scope.ResourceGroup == "" { + resourceGroup, err := pc.promptService.PromptResourceGroup(ctx, pc, nil) + if err != nil { + return err + } + + pc.Scope.ResourceGroup = resourceGroup.Name + } + + return nil +} + +// EnsureLocation ensures that the Azure context has a location. +// If the location is not set, the user is prompted to select a location. +func (pc *AzureContext) EnsureLocation(ctx context.Context) error { + if pc.Scope.Location == "" { + location, err := pc.promptService.PromptLocation(ctx, pc, nil) + if err != nil { + return err + } + + pc.Scope.Location = location.Name + } + + return nil +} diff --git a/cli/azd/pkg/prompt/prompt_service.go b/cli/azd/pkg/prompt/prompt_service.go new file mode 100644 index 00000000000..4a63cbd35e5 --- /dev/null +++ b/cli/azd/pkg/prompt/prompt_service.go @@ -0,0 +1,869 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package prompt + +import ( + "context" + "fmt" + "io" + "slices" + "strings" + + "dario.cat/mergo" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/pkg/ux" + "github.com/fatih/color" +) + +var ( + ErrNoResourcesFound = fmt.Errorf("no resources found") + ErrNoResourceSelected = fmt.Errorf("no resource selected") +) + +// ResourceOptions contains options for prompting the user to select a resource. +type ResourceOptions struct { + // ResourceType is the type of resource to select. + ResourceType *azapi.AzureResourceType + // Kinds is a list of resource kinds to filter by. + Kinds []string + // ResourceTypeDisplayName is the display name of the resource type. + ResourceTypeDisplayName string + // SelectorOptions contains options for the resource selector. + SelectorOptions *SelectOptions + // CreateResource is a function that creates a new resource. + CreateResource func(ctx context.Context) (*azapi.ResourceExtended, error) + // Selected is a function that determines if a resource is selected + Selected func(resource *azapi.ResourceExtended) bool +} + +// CustomResourceOptions contains options for prompting the user to select a custom resource. +type CustomResourceOptions[T any] struct { + // SelectorOptions contains options for the resource selector. + SelectorOptions *SelectOptions + // LoadData is a function that loads the resource data. + LoadData func(ctx context.Context) ([]*T, error) + // DisplayResource is a function that displays the resource. + DisplayResource func(resource *T) (string, error) + // SortResource is a function that sorts the resources. + SortResource func(a *T, b *T) int + // Selected is a function that determines if a resource is selected + Selected func(resource *T) bool + // CreateResource is a function that creates a new resource. + CreateResource func(ctx context.Context) (*T, error) +} + +// ResourceGroupOptions contains options for prompting the user to select a resource group. +type ResourceGroupOptions struct { + // SelectorOptions contains options for the resource group selector. + SelectorOptions *SelectOptions +} + +// SelectOptions contains options for prompting the user to select a resource. +type SelectOptions struct { + // ForceNewResource specifies whether to force the user to create a new resource. + ForceNewResource *bool + // AllowNewResource specifies whether to allow the user to create a new resource. + AllowNewResource *bool + // NewResourceMessage is the message to display to the user when creating a new resource. + NewResourceMessage string + // CreatingMessage is the message to display to the user when creating a new resource. + CreatingMessage string + // Message is the message to display to the user. + Message string + // HelpMessage is the help message to display to the user. + HelpMessage string + // LoadingMessage is the loading message to display to the user. + LoadingMessage string + // DisplayNumbers specifies whether to display numbers next to the choices. + DisplayNumbers *bool + // DisplayCount is the number of choices to display at a time. + DisplayCount int + // Hint is the hint to display to the user. + Hint string + // EnableFiltering specifies whether to enable filtering of choices. + EnableFiltering *bool + // Writer is the writer to use for output. + Writer io.Writer +} + +type AuthManager interface { + ClaimsForCurrentUser(ctx context.Context, options *auth.ClaimsForCurrentUserOptions) (auth.TokenClaims, error) +} + +// ResourceService defines the methods that the ResourceService must implement. +type ResourceService interface { + ListResourceGroup( + ctx context.Context, + subscriptionId string, + listOptions *azapi.ListResourceGroupOptions, + ) ([]*azapi.Resource, error) + ListResourceGroupResources( + ctx context.Context, + subscriptionId string, + resourceGroupName string, + listOptions *azapi.ListResourceGroupResourcesOptions, + ) ([]*azapi.ResourceExtended, error) + ListSubscriptionResources( + ctx context.Context, + subscriptionId string, + listOptions *armresources.ClientListOptions, + ) ([]*azapi.ResourceExtended, error) + CreateOrUpdateResourceGroup( + ctx context.Context, + subscriptionId string, + resourceGroupName string, + location string, + tags map[string]*string, + ) (*azapi.ResourceGroup, error) + GetResource( + ctx context.Context, + subscriptionId string, + resourceId string, + apiVersion string, + ) (azapi.ResourceExtended, error) +} + +// SubscriptionService defines the methods that the SubscriptionsService must implement. +type SubscriptionService interface { + ListSubscriptions(ctx context.Context, tenantId string) ([]*armsubscriptions.Subscription, error) + GetSubscription(ctx context.Context, subscriptionId string, tenantId string) (*armsubscriptions.Subscription, error) + ListSubscriptionLocations(ctx context.Context, subscriptionId string, tenantId string) ([]account.Location, error) + ListTenants(ctx context.Context) ([]armsubscriptions.TenantIDDescription, error) +} + +// PromptServiceInterface defines the methods that the PromptService must implement. +type PromptService interface { + PromptSubscription(ctx context.Context, selectorOptions *SelectOptions) (*account.Subscription, error) + PromptLocation( + ctx context.Context, + azureContext *AzureContext, + selectorOptions *SelectOptions, + ) (*account.Location, error) + PromptResourceGroup( + ctx context.Context, + azureContext *AzureContext, + options *ResourceGroupOptions, + ) (*azapi.ResourceGroup, error) + PromptSubscriptionResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, + ) (*azapi.ResourceExtended, error) + PromptResourceGroupResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, + ) (*azapi.ResourceExtended, error) +} + +// PromptService provides methods for prompting the user to select various Azure resources. +type promptService struct { + authManager AuthManager + userConfigManager config.UserConfigManager + resourceService ResourceService + subscriptionService SubscriptionService +} + +// NewPromptService creates a new prompt service. +func NewPromptService( + authManager AuthManager, + userConfigManager config.UserConfigManager, + subscriptionService SubscriptionService, + resourceService ResourceService, +) PromptService { + return &promptService{ + authManager: authManager, + userConfigManager: userConfigManager, + subscriptionService: subscriptionService, + resourceService: resourceService, + } +} + +// PromptSubscription prompts the user to select an Azure subscription. +func (ps *promptService) PromptSubscription( + ctx context.Context, + selectorOptions *SelectOptions, +) (*account.Subscription, error) { + mergedOptions := &SelectOptions{} + if selectorOptions == nil { + selectorOptions = &SelectOptions{} + } + + defaultOptions := &SelectOptions{ + Message: "Select subscription", + LoadingMessage: "Loading subscriptions...", + HelpMessage: "Choose an Azure subscription for your project.", + AllowNewResource: ux.Ptr(false), + } + + if err := mergo.Merge(mergedOptions, selectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedOptions, defaultOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + // Get default subscription from user config + var defaultSubscriptionId = "" + userConfig, err := ps.userConfigManager.Load() + if err == nil { + userSubscription, exists := userConfig.GetString("defaults.subscription") + if exists && userSubscription != "" { + defaultSubscriptionId = userSubscription + } + } + + return PromptCustomResource(ctx, CustomResourceOptions[account.Subscription]{ + SelectorOptions: mergedOptions, + LoadData: func(ctx context.Context) ([]*account.Subscription, error) { + userClaims, err := ps.authManager.ClaimsForCurrentUser(ctx, nil) + if err != nil { + return nil, err + } + + subscriptionList, err := ps.subscriptionService.ListSubscriptions(ctx, userClaims.TenantId) + if err != nil { + return nil, err + } + + subscriptions := make([]*account.Subscription, len(subscriptionList)) + for i, subscription := range subscriptionList { + subscriptions[i] = &account.Subscription{ + Id: *subscription.SubscriptionID, + Name: *subscription.DisplayName, + TenantId: *subscription.TenantID, + UserAccessTenantId: userClaims.TenantId, + } + } + + return subscriptions, nil + }, + DisplayResource: func(subscription *account.Subscription) (string, error) { + return fmt.Sprintf("%s %s", subscription.Name, color.HiBlackString("(%s)", subscription.Id)), nil + }, + Selected: func(subscription *account.Subscription) bool { + return strings.EqualFold(subscription.Id, defaultSubscriptionId) + }, + }) +} + +// PromptLocation prompts the user to select an Azure location. +func (ps *promptService) PromptLocation( + ctx context.Context, + azureContext *AzureContext, + selectorOptions *SelectOptions, +) (*account.Location, error) { + if azureContext == nil { + azureContext = NewEmptyAzureContext() + } + + if err := azureContext.EnsureSubscription(ctx); err != nil { + return nil, err + } + + mergedOptions := &SelectOptions{} + + if selectorOptions == nil { + selectorOptions = &SelectOptions{} + } + + defaultOptions := &SelectOptions{ + Message: "Select location", + LoadingMessage: "Loading locations...", + HelpMessage: "Choose an Azure location for your project.", + AllowNewResource: ux.Ptr(false), + } + + if err := mergo.Merge(mergedOptions, selectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedOptions, defaultOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + // Get default location from user config + var defaultLocation = "eastus2" + userConfig, err := ps.userConfigManager.Load() + if err == nil { + userLocation, exists := userConfig.GetString("defaults.location") + if exists && userLocation != "" { + defaultLocation = userLocation + } + } + + return PromptCustomResource(ctx, CustomResourceOptions[account.Location]{ + SelectorOptions: mergedOptions, + LoadData: func(ctx context.Context) ([]*account.Location, error) { + locationList, err := ps.subscriptionService.ListSubscriptionLocations( + ctx, + azureContext.Scope.SubscriptionId, + azureContext.Scope.TenantId, + ) + if err != nil { + return nil, err + } + + locations := make([]*account.Location, len(locationList)) + for i, location := range locationList { + locations[i] = &account.Location{ + Name: location.Name, + DisplayName: location.DisplayName, + RegionalDisplayName: location.RegionalDisplayName, + } + } + + return locations, nil + }, + DisplayResource: func(location *account.Location) (string, error) { + return fmt.Sprintf("%s %s", location.RegionalDisplayName, color.HiBlackString("(%s)", location.Name)), nil + }, + Selected: func(resource *account.Location) bool { + return resource.Name == defaultLocation + }, + }) +} + +// PromptResourceGroup prompts the user to select an Azure resource group. +func (ps *promptService) PromptResourceGroup( + ctx context.Context, + azureContext *AzureContext, + options *ResourceGroupOptions, +) (*azapi.ResourceGroup, error) { + if azureContext == nil { + azureContext = NewEmptyAzureContext() + } + + if err := azureContext.EnsureSubscription(ctx); err != nil { + return nil, err + } + + mergedSelectorOptions := &SelectOptions{} + + if options == nil { + options = &ResourceGroupOptions{} + } + + if options.SelectorOptions == nil { + options.SelectorOptions = &SelectOptions{} + } + + defaultSelectorOptions := &SelectOptions{ + Message: "Select resource group", + LoadingMessage: "Loading resource groups...", + HelpMessage: "Choose an Azure resource group for your project.", + AllowNewResource: ux.Ptr(true), + NewResourceMessage: "Create new resource group", + CreatingMessage: "Creating new resource group...", + } + + if err := mergo.Merge(mergedSelectorOptions, options.SelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedSelectorOptions, defaultSelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + return PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceGroup]{ + SelectorOptions: mergedSelectorOptions, + LoadData: func(ctx context.Context) ([]*azapi.ResourceGroup, error) { + resourceGroupList, err := ps.resourceService.ListResourceGroup(ctx, azureContext.Scope.SubscriptionId, nil) + if err != nil { + return nil, err + } + + resourceGroups := make([]*azapi.ResourceGroup, len(resourceGroupList)) + for i, resourceGroup := range resourceGroupList { + resourceGroups[i] = &azapi.ResourceGroup{ + Id: resourceGroup.Id, + Name: resourceGroup.Name, + Location: resourceGroup.Location, + } + } + + return resourceGroups, nil + }, + DisplayResource: func(resourceGroup *azapi.ResourceGroup) (string, error) { + return fmt.Sprintf( + "%s %s", + resourceGroup.Name, + color.HiBlackString("(Location: %s)", resourceGroup.Location), + ), nil + }, + Selected: func(resourceGroup *azapi.ResourceGroup) bool { + return resourceGroup.Name == azureContext.Scope.ResourceGroup + }, + CreateResource: func(ctx context.Context) (*azapi.ResourceGroup, error) { + namePrompt := ux.NewPrompt(&ux.PromptOptions{ + Message: "Enter the name for the resource group", + }) + + resourceGroupName, err := namePrompt.Ask() + if err != nil { + return nil, err + } + + if err := azureContext.EnsureLocation(ctx); err != nil { + return nil, err + } + + var resourceGroup *azapi.ResourceGroup + + taskName := fmt.Sprintf("Creating resource group %s", color.CyanString(resourceGroupName)) + + err = ux.NewTaskList(nil). + AddTask(ux.TaskOptions{ + Title: taskName, + Action: func(setProgress ux.SetProgressFunc) (ux.TaskState, error) { + newResourceGroup, err := ps.resourceService.CreateOrUpdateResourceGroup( + ctx, + azureContext.Scope.SubscriptionId, + resourceGroupName, + azureContext.Scope.Location, + nil, + ) + if err != nil { + return ux.Error, err + } + + resourceGroup = newResourceGroup + return ux.Success, nil + }, + }). + Run() + + if err != nil { + return nil, err + } + + return resourceGroup, nil + }, + }) +} + +// PromptSubscriptionResource prompts the user to select an Azure resource from the subscription specified in the context. +func (ps *promptService) PromptSubscriptionResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, +) (*azapi.ResourceExtended, error) { + if azureContext == nil { + azureContext = NewEmptyAzureContext() + } + + if err := azureContext.EnsureSubscription(ctx); err != nil { + return nil, err + } + + mergedSelectorOptions := &SelectOptions{} + + if options.SelectorOptions == nil { + options.SelectorOptions = &SelectOptions{} + } + + var existingResource *arm.ResourceID + if options.ResourceType != nil { + match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) + if has { + existingResource = match + } + } + + if options.Selected == nil { + options.Selected = func(resource *azapi.ResourceExtended) bool { + if existingResource == nil { + return false + } + + if strings.EqualFold(resource.Id, existingResource.String()) { + return true + } + + return false + } + } + + resourceName := options.ResourceTypeDisplayName + + if resourceName == "" && options.ResourceType != nil { + resourceName = string(*options.ResourceType) + } + + if resourceName == "" { + resourceName = "resource" + } + + defaultSelectorOptions := &SelectOptions{ + Message: fmt.Sprintf("Select %s", resourceName), + LoadingMessage: fmt.Sprintf("Loading %s resources...", resourceName), + HelpMessage: fmt.Sprintf("Choose an Azure %s for your project.", resourceName), + AllowNewResource: ux.Ptr(true), + NewResourceMessage: fmt.Sprintf("Create new %s", resourceName), + CreatingMessage: fmt.Sprintf("Creating new %s...", resourceName), + } + + if err := mergo.Merge(mergedSelectorOptions, options.SelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedSelectorOptions, defaultSelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ + SelectorOptions: mergedSelectorOptions, + LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { + var resourceListOptions *armresources.ClientListOptions + if options.ResourceType != nil { + resourceListOptions = &armresources.ClientListOptions{ + Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", string(*options.ResourceType))), + } + } + + resourceList, err := ps.resourceService.ListSubscriptionResources( + ctx, + azureContext.Scope.SubscriptionId, + resourceListOptions, + ) + if err != nil { + return nil, err + } + + filteredResources := []*azapi.ResourceExtended{} + hasKindFilter := len(options.Kinds) > 0 + + for _, resource := range resourceList { + if !hasKindFilter || slices.Contains(options.Kinds, resource.Kind) { + filteredResources = append(filteredResources, resource) + } + } + + if len(filteredResources) == 0 { + if options.ResourceType == nil { + return nil, ErrNoResourcesFound + } + + return nil, fmt.Errorf("no resources found with type '%v'", *options.ResourceType) + } + + return filteredResources, nil + }, + DisplayResource: func(resource *azapi.ResourceExtended) (string, error) { + parsedResource, err := arm.ParseResourceID(resource.Id) + if err != nil { + return "", fmt.Errorf("parsing resource id: %w", err) + } + + return fmt.Sprintf( + "%s %s", + parsedResource.Name, + color.HiBlackString("(%s)", parsedResource.ResourceGroupName), + ), nil + }, + Selected: options.Selected, + CreateResource: options.CreateResource, + }) + + if err != nil { + return nil, err + } + + if err := azureContext.Resources.Add(resource.Id); err != nil { + return nil, err + } + + return resource, nil +} + +// PromptResourceGroupResource prompts the user to select an Azure resource from the resource group specified in the context. +func (ps *promptService) PromptResourceGroupResource( + ctx context.Context, + azureContext *AzureContext, + options ResourceOptions, +) (*azapi.ResourceExtended, error) { + if azureContext == nil { + azureContext = NewEmptyAzureContext() + } + + if err := azureContext.EnsureResourceGroup(ctx); err != nil { + return nil, err + } + + mergedSelectorOptions := &SelectOptions{} + + if options.SelectorOptions == nil { + options.SelectorOptions = &SelectOptions{} + } + + var existingResource *arm.ResourceID + if options.ResourceType != nil { + match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) + if has { + existingResource = match + } + } + + if options.Selected == nil { + options.Selected = func(resource *azapi.ResourceExtended) bool { + if existingResource == nil { + return false + } + + return strings.EqualFold(resource.Id, existingResource.String()) + } + } + + resourceName := options.ResourceTypeDisplayName + + if resourceName == "" && options.ResourceType != nil { + resourceName = string(*options.ResourceType) + } + + if resourceName == "" { + resourceName = "resource" + } + + defaultSelectorOptions := &SelectOptions{ + Message: fmt.Sprintf("Select %s", resourceName), + LoadingMessage: fmt.Sprintf("Loading %s resources...", resourceName), + HelpMessage: fmt.Sprintf("Choose an Azure %s for your project.", resourceName), + AllowNewResource: ux.Ptr(true), + NewResourceMessage: fmt.Sprintf("Create new %s", resourceName), + CreatingMessage: fmt.Sprintf("Creating new %s...", resourceName), + } + + if err := mergo.Merge(mergedSelectorOptions, options.SelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedSelectorOptions, defaultSelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ + Selected: options.Selected, + SelectorOptions: mergedSelectorOptions, + LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { + var resourceListOptions *azapi.ListResourceGroupResourcesOptions + if options.ResourceType != nil { + resourceListOptions = &azapi.ListResourceGroupResourcesOptions{ + Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", *options.ResourceType)), + } + } + + resourceList, err := ps.resourceService.ListResourceGroupResources( + ctx, + azureContext.Scope.SubscriptionId, + azureContext.Scope.ResourceGroup, + resourceListOptions, + ) + if err != nil { + return nil, err + } + + filteredResources := []*azapi.ResourceExtended{} + hasKindFilter := len(options.Kinds) > 0 + + for _, resource := range resourceList { + if !hasKindFilter || slices.Contains(options.Kinds, resource.Kind) { + filteredResources = append(filteredResources, resource) + } + } + + if len(filteredResources) == 0 { + if options.ResourceType == nil { + return nil, ErrNoResourcesFound + } + + return nil, fmt.Errorf("no resources found with type '%v'", *options.ResourceType) + } + + return filteredResources, nil + }, + DisplayResource: func(resource *azapi.ResourceExtended) (string, error) { + return resource.Name, nil + }, + CreateResource: options.CreateResource, + }) + + if err != nil { + return nil, err + } + + if err := azureContext.Resources.Add(resource.Id); err != nil { + return nil, err + } + + return resource, nil +} + +// PromptCustomResource prompts the user to select a custom resource from a list of resources. +// This function is used internally to power selection of subscriptions, resource groups and other resources. +// This can be used directly when the list of resources require integration with other Azure SDKs for resource selection. +func PromptCustomResource[T any](ctx context.Context, options CustomResourceOptions[T]) (*T, error) { + mergedSelectorOptions := &SelectOptions{} + + if options.SelectorOptions == nil { + options.SelectorOptions = &SelectOptions{} + } + + defaultSelectorOptions := &SelectOptions{ + Message: "Select resource", + LoadingMessage: "Loading resources...", + HelpMessage: "Choose a resource for your project.", + AllowNewResource: ux.Ptr(true), + ForceNewResource: ux.Ptr(false), + NewResourceMessage: "Create new resource", + CreatingMessage: "Creating new resource...", + DisplayNumbers: ux.Ptr(true), + DisplayCount: 10, + } + + if err := mergo.Merge(mergedSelectorOptions, options.SelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + if err := mergo.Merge(mergedSelectorOptions, defaultSelectorOptions, mergo.WithoutDereference); err != nil { + return nil, err + } + + allowNewResource := mergedSelectorOptions.AllowNewResource != nil && *mergedSelectorOptions.AllowNewResource + forceNewResource := mergedSelectorOptions.ForceNewResource != nil && *mergedSelectorOptions.ForceNewResource + + var resources []*T + var selectedIndex *int + + if forceNewResource { + allowNewResource = true + selectedIndex = ux.Ptr(0) + } else { + loadingSpinner := ux.NewSpinner(&ux.SpinnerOptions{ + Text: options.SelectorOptions.LoadingMessage, + }) + + err := loadingSpinner.Run(ctx, func(ctx context.Context) error { + resourceList, err := options.LoadData(ctx) + if err != nil { + return err + } + + resources = resourceList + return nil + }) + if err != nil { + return nil, err + } + + if !allowNewResource && len(resources) == 0 { + return nil, ErrNoResourcesFound + } + + if options.SortResource != nil { + slices.SortFunc(resources, options.SortResource) + } + + var defaultIndex *int + if options.Selected != nil { + for i, resource := range resources { + if options.Selected(resource) { + defaultIndex = &i + break + } + } + } + + hasCustomDisplay := options.DisplayResource != nil + + var choices []string + + if allowNewResource { + choices = make([]string, len(resources)+1) + choices[0] = mergedSelectorOptions.NewResourceMessage + + if defaultIndex != nil { + *defaultIndex++ + } + } else { + choices = make([]string, len(resources)) + } + + for i, resource := range resources { + var displayValue string + + if hasCustomDisplay { + customDisplayValue, err := options.DisplayResource(resource) + if err != nil { + return nil, err + } + + displayValue = customDisplayValue + } else { + displayValue = fmt.Sprintf("%v", resource) + } + + if allowNewResource { + choices[i+1] = displayValue + } else { + choices[i] = displayValue + } + } + + resourceSelector := ux.NewSelect(&ux.SelectOptions{ + Message: mergedSelectorOptions.Message, + HelpMessage: mergedSelectorOptions.HelpMessage, + DisplayCount: mergedSelectorOptions.DisplayCount, + DisplayNumbers: mergedSelectorOptions.DisplayNumbers, + Hint: mergedSelectorOptions.Hint, + EnableFiltering: mergedSelectorOptions.EnableFiltering, + Writer: mergedSelectorOptions.Writer, + Allowed: choices, + SelectedIndex: defaultIndex, + }) + + userSelectedIndex, err := resourceSelector.Ask() + if err != nil { + return nil, err + } + + if userSelectedIndex == nil { + return nil, ErrNoResourceSelected + } + + selectedIndex = userSelectedIndex + } + + var selectedResource *T + + // Create new resource + if allowNewResource && *selectedIndex == 0 { + if options.CreateResource == nil { + return nil, fmt.Errorf("no create resource function provided") + } + + createdResource, err := options.CreateResource(ctx) + if err != nil { + return nil, err + } + + selectedResource = createdResource + } else { + // If a new resource is allowed, decrement the selected index + if allowNewResource { + *selectedIndex-- + } + + selectedResource = resources[*selectedIndex] + } + + return selectedResource, nil +} diff --git a/cli/azd/pkg/prompt/prompt_service_test.go b/cli/azd/pkg/prompt/prompt_service_test.go new file mode 100644 index 00000000000..b1180cbcfbd --- /dev/null +++ b/cli/azd/pkg/prompt/prompt_service_test.go @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package prompt + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockaccount" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockauth" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockazapi" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_PromptService_PromptSubscription(t *testing.T) { + fileConfigManager := config.NewFileConfigManager(config.NewManager()) + userConfigManager := config.NewUserConfigManager(fileConfigManager) + + authManager := &mockauth.MockAuthManager{} + subscriptionService := &mockaccount.MockSubscriptionService{} + resourceService := &mockazapi.MockResourceService{} + + tokenClaims := auth.TokenClaims{ + TenantId: "tenant-1", + } + + authManager. + On("ClaimsForCurrentUser", mock.Anything, mock.Anything). + Return(tokenClaims, nil) + + subscriptionService. + On("ListSubscriptions", mock.Anything, tokenClaims.TenantId). + Return([]*armsubscriptions.Subscription{ + { + ID: to.Ptr("/subscriptions/subscription-1"), + SubscriptionID: to.Ptr("subscription-1"), + TenantID: to.Ptr("tenant-1"), + DisplayName: to.Ptr("Subscription 1"), + }, + { + ID: to.Ptr("/subscriptions/subscription-2"), + SubscriptionID: to.Ptr("subscription-2"), + TenantID: to.Ptr("tenant-1"), + DisplayName: to.Ptr("Subscription 2"), + }, + }, nil) + + promptService := NewPromptService(authManager, userConfigManager, subscriptionService, resourceService) + require.NotNil(t, promptService) +} diff --git a/cli/azd/pkg/prompt/prompter.go b/cli/azd/pkg/prompt/prompter.go index e49fce5a027..aad0b359d8d 100644 --- a/cli/azd/pkg/prompt/prompter.go +++ b/cli/azd/pkg/prompt/prompter.go @@ -204,7 +204,7 @@ func (p *DefaultPrompter) PromptResourceGroupFrom( tagsParam[k] = to.Ptr(v) } - err = p.resourceService.CreateOrUpdateResourceGroup(ctx, subscriptionId, name, location, tagsParam) + _, err = p.resourceService.CreateOrUpdateResourceGroup(ctx, subscriptionId, name, location, tagsParam) if err != nil { return "", fmt.Errorf("creating resource group: %w", err) } diff --git a/cli/azd/pkg/ux/printer.go b/cli/azd/pkg/ux/printer.go index 3320f970c77..699476140cc 100644 --- a/cli/azd/pkg/ux/printer.go +++ b/cli/azd/pkg/ux/printer.go @@ -6,7 +6,6 @@ package ux import ( "fmt" "io" - "log" "math" "os" "regexp" @@ -134,14 +133,10 @@ func (p *printer) Fprintf(format string, a ...any) { fmt.Fprint(p.writer, content) - log.Printf("content: %s", p.currentLine) - visibleContent := specialTextRegex.ReplaceAllString(p.currentLine, "") p.size.Cols = len(visibleContent) p.size.Rows += lineCount - - log.Printf("visibleContent: %s (%d, %d)", visibleContent, p.size.Rows, p.size.Cols) } // Fprintln writes text to the screen followed by a newline character. diff --git a/cli/azd/test/mocks/mockaccount/mock_subscriptions.go b/cli/azd/test/mocks/mockaccount/mock_subscriptions.go new file mode 100644 index 00000000000..8dec3d322f2 --- /dev/null +++ b/cli/azd/test/mocks/mockaccount/mock_subscriptions.go @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package mockaccount + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/stretchr/testify/mock" +) + +type MockSubscriptionService struct { + mock.Mock +} + +func (m *MockSubscriptionService) ListSubscriptions( + ctx context.Context, + tenantId string, +) ([]*armsubscriptions.Subscription, error) { + args := m.Called(ctx, tenantId) + return args.Get(0).([]*armsubscriptions.Subscription), args.Error(1) +} + +func (m *MockSubscriptionService) GetSubscription( + ctx context.Context, + subscriptionId string, + tenantId string, +) (*armsubscriptions.Subscription, error) { + args := m.Called(ctx, subscriptionId, tenantId) + return args.Get(0).(*armsubscriptions.Subscription), args.Error(1) +} + +func (m *MockSubscriptionService) ListSubscriptionLocations( + ctx context.Context, + subscriptionId string, + tenantId string, +) ([]account.Location, error) { + args := m.Called(ctx, subscriptionId, tenantId) + return args.Get(0).([]account.Location), args.Error(1) +} + +func (m *MockSubscriptionService) ListTenants(ctx context.Context) ([]armsubscriptions.TenantIDDescription, error) { + args := m.Called(ctx) + return args.Get(0).([]armsubscriptions.TenantIDDescription), args.Error(1) +} diff --git a/cli/azd/test/mocks/mockauth/mock_auth_manager.go b/cli/azd/test/mocks/mockauth/mock_auth_manager.go new file mode 100644 index 00000000000..31b1d9ca1a6 --- /dev/null +++ b/cli/azd/test/mocks/mockauth/mock_auth_manager.go @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package mockauth + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/stretchr/testify/mock" +) + +type MockAuthManager struct { + mock.Mock +} + +func (m *MockAuthManager) ClaimsForCurrentUser( + ctx context.Context, + options *auth.ClaimsForCurrentUserOptions, +) (auth.TokenClaims, error) { + args := m.Called(ctx, options) + return args.Get(0).(auth.TokenClaims), args.Error(1) +} diff --git a/cli/azd/test/mocks/mockazapi/mock_resource_service.go b/cli/azd/test/mocks/mockazapi/mock_resource_service.go new file mode 100644 index 00000000000..463724433d0 --- /dev/null +++ b/cli/azd/test/mocks/mockazapi/mock_resource_service.go @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package mockazapi + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/stretchr/testify/mock" +) + +type MockResourceService struct { + mock.Mock +} + +func (m *MockResourceService) ListResourceGroup( + ctx context.Context, + subscriptionId string, + listOptions *azapi.ListResourceGroupOptions, +) ([]*azapi.Resource, error) { + args := m.Called(ctx, subscriptionId, listOptions) + return args.Get(0).([]*azapi.Resource), args.Error(1) +} + +func (m *MockResourceService) ListResourceGroupResources( + ctx context.Context, + subscriptionId string, + resourceGroupName string, + listOptions *azapi.ListResourceGroupResourcesOptions, +) ([]*azapi.ResourceExtended, error) { + args := m.Called(ctx, subscriptionId, resourceGroupName, listOptions) + return args.Get(0).([]*azapi.ResourceExtended), args.Error(1) +} + +func (m *MockResourceService) ListSubscriptionResources( + ctx context.Context, + subscriptionId string, + listOptions *armresources.ClientListOptions, +) ([]*azapi.ResourceExtended, error) { + args := m.Called(ctx, subscriptionId, listOptions) + return args.Get(0).([]*azapi.ResourceExtended), args.Error(1) +} + +func (m *MockResourceService) CreateOrUpdateResourceGroup( + ctx context.Context, + subscriptionId string, + resourceGroupName string, + location string, + tags map[string]*string, +) (*azapi.ResourceGroup, error) { + args := m.Called(ctx, subscriptionId, resourceGroupName, location, tags) + return args.Get(0).(*azapi.ResourceGroup), args.Error(1) +} + +func (m *MockResourceService) GetResource( + ctx context.Context, + subscriptionId string, + resourceId string, + apiVersion string, +) (azapi.ResourceExtended, error) { + args := m.Called(ctx, subscriptionId, resourceId, apiVersion) + return args.Get(0).(azapi.ResourceExtended), args.Error(1) +} + +func (m *MockResourceService) DeleteResource(ctx context.Context, subscriptionId string, resourceId string) error { + args := m.Called(ctx, subscriptionId, resourceId) + return args.Error(0) +}