From 8127330f6038aff2d9b06aec94f99f4fd79f16cc Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 10:51:46 -0700 Subject: [PATCH 1/8] Updates to extension framework --- cli/azd/.vscode/cspell.yaml | 5 + cli/azd/cmd/container.go | 2 + cli/azd/grpc/proto/compose.proto | 1 + cli/azd/grpc/proto/project.proto | 8 + cli/azd/grpc/proto/workflow.proto | 33 ++ .../internal/grpcserver/compose_service.go | 231 +++++++++++++ .../grpcserver/environment_service.go | 104 +++--- .../internal/grpcserver/project_service.go | 52 +-- cli/azd/internal/grpcserver/prompt_service.go | 1 - cli/azd/internal/grpcserver/server.go | 10 +- cli/azd/internal/grpcserver/server_test.go | 2 + .../internal/grpcserver/workflow_service.go | 66 ++++ cli/azd/pkg/azdext/azd_client.go | 20 ++ cli/azd/pkg/azdext/compose.pb.go | 85 ++--- cli/azd/pkg/azdext/project.pb.go | 103 ++++-- cli/azd/pkg/azdext/project_grpc.pb.go | 40 ++- cli/azd/pkg/azdext/workflow.pb.go | 307 ++++++++++++++++++ cli/azd/pkg/azdext/workflow_grpc.pb.go | 126 +++++++ cli/azd/pkg/prompt/prompt_models.go | 4 + cli/azd/pkg/prompt/prompt_service.go | 99 ++---- cli/azd/pkg/ux/confirm.go | 9 +- cli/azd/pkg/ux/multi_select.go | 9 +- cli/azd/pkg/ux/printer.go | 3 + cli/azd/pkg/ux/select.go | 9 +- cli/azd/pkg/ux/spinner.go | 94 ++---- 25 files changed, 1140 insertions(+), 283 deletions(-) create mode 100644 cli/azd/grpc/proto/workflow.proto create mode 100644 cli/azd/internal/grpcserver/compose_service.go create mode 100644 cli/azd/internal/grpcserver/workflow_service.go create mode 100644 cli/azd/pkg/azdext/workflow.pb.go create mode 100644 cli/azd/pkg/azdext/workflow_grpc.pb.go diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 94787deb500..cebe961a2bd 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -150,6 +150,11 @@ overrides: - getenv - errorf - println + - filename: extensions/microsoft.azd.ai.builder/internal/cmd/start.go + words: + - dall + - datasource + - vectorizing ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 664b6448a41..983c9fbcaf0 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -825,6 +825,8 @@ func registerCommonDependencies(container *ioc.NestedContainer) { container.MustRegisterScoped(grpcserver.NewDeploymentService) container.MustRegisterScoped(grpcserver.NewEventService) container.MustRegisterSingleton(grpcserver.NewUserConfigService) + container.MustRegisterSingleton(grpcserver.NewComposeService) + container.MustRegisterSingleton(grpcserver.NewWorkflowService) // Required for nested actions called from composite actions like 'up' registerAction[*cmd.ProvisionAction](container, "azd-provision-action") diff --git a/cli/azd/grpc/proto/compose.proto b/cli/azd/grpc/proto/compose.proto index 48a46f0b23d..995101f36ad 100644 --- a/cli/azd/grpc/proto/compose.proto +++ b/cli/azd/grpc/proto/compose.proto @@ -68,6 +68,7 @@ message ComposedResource { string name = 1; string type = 2; bytes config = 3; + repeated string uses = 4; } // ComposedResourceType represents a type of composability resource. diff --git a/cli/azd/grpc/proto/project.proto b/cli/azd/grpc/proto/project.proto index ad32b9e46b9..a4d234cc22b 100644 --- a/cli/azd/grpc/proto/project.proto +++ b/cli/azd/grpc/proto/project.proto @@ -10,9 +10,17 @@ import "models.proto"; service ProjectService { // Gets the current project. rpc Get(EmptyRequest) returns (GetProjectResponse); + + // AddService adds a new service to the project. + rpc AddService(AddServiceRequest) returns (EmptyResponse); } // GetProjectResponse message definition message GetProjectResponse { ProjectConfig project = 1; } + +// AddServiceRequest message definition +message AddServiceRequest { + ServiceConfig service = 1; +} \ No newline at end of file diff --git a/cli/azd/grpc/proto/workflow.proto b/cli/azd/grpc/proto/workflow.proto new file mode 100644 index 00000000000..d00322bf5c6 --- /dev/null +++ b/cli/azd/grpc/proto/workflow.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package azdext; + +option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext;azdext"; + +import "models.proto"; + +service WorkflowService { + // ListResources retrieves all configured composability resources in the current project. + rpc Run(RunWorkflowRequest) returns (EmptyResponse); +} + +// RunWorkflowRequest is a request to run a workflow. +message RunWorkflowRequest { + Workflow workflow = 1; +} + +// Workflow is a collection of steps to be executed in order. +message Workflow { + string name = 1; + repeated WorkflowStep steps = 2; +} + +// WorkflowStep is a single step in a workflow. +message WorkflowStep { + WorkflowCommand command = 1; +} + +// WorkflowCommand is a command to be executed in a workflow step. +message WorkflowCommand { + repeated string args = 1; +} \ No newline at end of file diff --git a/cli/azd/internal/grpcserver/compose_service.go b/cli/azd/internal/grpcserver/compose_service.go new file mode 100644 index 00000000000..8d37664d8be --- /dev/null +++ b/cli/azd/internal/grpcserver/compose_service.go @@ -0,0 +1,231 @@ +// 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/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" + "github.com/azure/azure-dev/cli/azd/pkg/project" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type ComposeService struct { + azdext.UnimplementedComposeServiceServer + + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] +} + +func NewComposeService( + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], +) azdext.ComposeServiceServer { + return &ComposeService{ + lazyAzdContext: lazyAzdContext, + } +} + +// AddResource implements azdext.ComposeServiceServer. +func (c *ComposeService) AddResource( + ctx context.Context, + req *azdext.AddResourceRequest, +) (*azdext.AddResourceResponse, error) { + azdContext, err := c.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + if err != nil { + return nil, err + } + + if projectConfig.Resources == nil { + projectConfig.Resources = make(map[string]*project.ResourceConfig) + } + + resourceProps, err := createResourceProps(req.Resource.Type, req.Resource.Config) + if err != nil { + return nil, fmt.Errorf("creating resource props: %w", err) + } + + projectConfig.Resources[req.Resource.Name] = &project.ResourceConfig{ + Name: req.Resource.Name, + Type: project.ResourceType(req.Resource.Type), + Props: resourceProps, + Uses: req.Resource.Uses, + } + + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.AddResourceResponse{ + Resource: req.Resource, + }, nil +} + +func createResourceProps(resourceType string, config []byte) (any, error) { + switch project.ResourceType(resourceType) { + case project.ResourceTypeHostContainerApp: + props := project.ContainerAppProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeDbCosmos: + props := project.CosmosDBProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeStorage: + props := project.StorageProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeAiProject: + props := project.AiFoundryModelProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeDbMongo: + props := project.CosmosDBProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeMessagingEventHubs: + props := project.EventHubsProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeMessagingServiceBus: + props := project.ServiceBusProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + default: + return nil, nil + } +} + +// GetResource implements azdext.ComposeServiceServer. +func (c *ComposeService) GetResource( + ctx context.Context, + req *azdext.GetResourceRequest, +) (*azdext.GetResourceResponse, error) { + azdContext, err := c.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + if err != nil { + return nil, err + } + + existingResource, has := projectConfig.Resources[req.Name] + if !has { + return nil, status.Errorf(codes.NotFound, "resource %s not found", req.Name) + } + + resourceConfigBytes, err := json.Marshal(existingResource.Props) + if err != nil { + return nil, fmt.Errorf("marshaling resource config: %w", err) + } + + composedResource := &azdext.ComposedResource{ + Name: existingResource.Name, + Type: string(existingResource.Type), + Config: resourceConfigBytes, + Uses: existingResource.Uses, + } + + return &azdext.GetResourceResponse{ + Resource: composedResource, + }, nil +} + +// GetResourceType implements azdext.ComposeServiceServer. +func (c *ComposeService) GetResourceType( + context.Context, + *azdext.GetResourceTypeRequest, +) (*azdext.GetResourceTypeResponse, error) { + panic("unimplemented") +} + +// ListResourceTypes implements azdext.ComposeServiceServer. +func (c *ComposeService) ListResourceTypes( + context.Context, + *azdext.EmptyRequest, +) (*azdext.ListResourceTypesResponse, error) { + panic("unimplemented") +} + +// ListResources implements azdext.ComposeServiceServer. +func (c *ComposeService) ListResources( + ctx context.Context, + req *azdext.EmptyRequest, +) (*azdext.ListResourcesResponse, error) { + azdContext, err := c.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + if err != nil { + return nil, err + } + + existingResources := projectConfig.Resources + composedResources := make([]*azdext.ComposedResource, 0, len(existingResources)) + + for _, resource := range existingResources { + resourceConfigBytes, err := json.Marshal(resource.Props) + if err != nil { + return nil, fmt.Errorf("marshaling resource config: %w", err) + } + composedResource := &azdext.ComposedResource{ + Name: resource.Name, + Type: string(resource.Type), + Config: resourceConfigBytes, + Uses: resource.Uses, + } + composedResources = append(composedResources, composedResource) + } + + return &azdext.ListResourcesResponse{ + Resources: composedResources, + }, nil +} diff --git a/cli/azd/internal/grpcserver/environment_service.go b/cli/azd/internal/grpcserver/environment_service.go index 8b215ca392d..ad0b684d00f 100644 --- a/cli/azd/internal/grpcserver/environment_service.go +++ b/cli/azd/internal/grpcserver/environment_service.go @@ -18,10 +18,6 @@ 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( @@ -35,11 +31,12 @@ func NewEnvironmentService( } func (s *environmentService) List(ctx context.Context, req *azdext.EmptyRequest) (*azdext.EnvironmentListResponse, error) { - if err := s.initialize(); err != nil { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - envList, err := s.envManager.List(ctx) + envList, err := envManager.List(ctx) if err != nil { return nil, err } @@ -63,10 +60,6 @@ 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 @@ -83,11 +76,12 @@ func (s *environmentService) Get( ctx context.Context, req *azdext.GetEnvironmentRequest, ) (*azdext.EnvironmentResponse, error) { - if err := s.initialize(); err != nil { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - env, err := s.envManager.Get(ctx, req.Name) + env, err := envManager.Get(ctx, req.Name) if err != nil { return nil, err } @@ -103,11 +97,17 @@ func (s *environmentService) Select( ctx context.Context, req *azdext.SelectEnvironmentRequest, ) (*azdext.EmptyResponse, error) { - if err := s.initialize(); err != nil { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - env, err := s.envManager.Get(ctx, req.Name) + env, err := envManager.Get(ctx, req.Name) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (s *environmentService) Select( DefaultEnvironment: env.Name(), } - if err := s.azdContext.SetProjectState(projectState); err != nil { + if err := azdContext.SetProjectState(projectState); err != nil { return nil, err } @@ -128,11 +128,12 @@ func (s *environmentService) GetValues( ctx context.Context, req *azdext.GetEnvironmentRequest, ) (*azdext.KeyValueListResponse, error) { - if err := s.initialize(); err != nil { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - env, err := s.envManager.Get(ctx, req.Name) + env, err := envManager.Get(ctx, req.Name) if err != nil { return nil, err } @@ -156,11 +157,12 @@ func (s *environmentService) GetValues( // 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 { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - env, err := s.envManager.Get(ctx, req.EnvName) + env, err := envManager.Get(ctx, req.EnvName) if err != nil { return nil, err } @@ -175,26 +177,36 @@ func (s *environmentService) GetValue(ctx context.Context, req *azdext.GetEnvReq // 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 { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } - env, err := s.envManager.Get(ctx, req.EnvName) + env, err := envManager.Get(ctx, req.EnvName) if err != nil { return nil, err } env.DotenvSet(req.Key, req.Value) + if err := envManager.Save(ctx, env); err != nil { + return nil, fmt.Errorf("failed to save environment: %w", err) + } return &azdext.EmptyResponse{}, nil } func (s *environmentService) currentEnvironment(ctx context.Context) (*environment.Environment, error) { - if err := s.initialize(); err != nil { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { return nil, err } - defaultEnvironment, err := s.azdContext.GetDefaultEnvironmentName() + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + defaultEnvironment, err := azdContext.GetDefaultEnvironmentName() if err != nil { return nil, err } @@ -203,7 +215,7 @@ func (s *environmentService) currentEnvironment(ctx context.Context) (*environme return nil, environment.ErrDefaultEnvironmentNotFound } - env, err := s.envManager.Get(ctx, defaultEnvironment) + env, err := envManager.Get(ctx, defaultEnvironment) if err != nil { return nil, err } @@ -216,10 +228,6 @@ 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 @@ -248,10 +256,6 @@ 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 @@ -270,10 +274,6 @@ 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 @@ -304,7 +304,8 @@ func (s *environmentService) GetConfigSection( // 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 { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } @@ -322,7 +323,7 @@ func (s *environmentService) SetConfig(ctx context.Context, req *azdext.SetConfi return nil, fmt.Errorf("failed to set value: %w", err) } - if err := s.envManager.Save(ctx, env); err != nil { + if err := envManager.Save(ctx, env); err != nil { return nil, fmt.Errorf("failed to save config: %w", err) } @@ -334,7 +335,8 @@ func (s *environmentService) UnsetConfig( ctx context.Context, req *azdext.UnsetConfigRequest, ) (*azdext.EmptyResponse, error) { - if err := s.initialize(); err != nil { + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { return nil, err } @@ -347,31 +349,9 @@ func (s *environmentService) UnsetConfig( return nil, fmt.Errorf("failed to unset value: %w", err) } - if err := s.envManager.Save(ctx, env); err != nil { + if err := 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 index f0f98d0a107..a276d89265d 100644 --- a/cli/azd/internal/grpcserver/project_service.go +++ b/cli/azd/internal/grpcserver/project_service.go @@ -18,11 +18,6 @@ type projectService struct { lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] lazyEnvManager *lazy.Lazy[environment.Manager] - - azdContext *azdcontext.AzdContext - envManager environment.Manager - - initialized bool } func NewProjectService( @@ -36,11 +31,12 @@ func NewProjectService( } func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*azdext.GetProjectResponse, error) { - if err := s.initialize(); err != nil { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { return nil, err } - projectConfig, err := project.Load(ctx, s.azdContext.ProjectPath()) + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) if err != nil { return nil, err } @@ -49,13 +45,18 @@ func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*az return "" } - defaultEnvironment, err := s.azdContext.GetDefaultEnvironmentName() + defaultEnvironment, err := azdContext.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + envManager, err := s.lazyEnvManager.GetValue() if err != nil { return nil, err } if defaultEnvironment != "" { - env, err := s.envManager.Get(ctx, defaultEnvironment) + env, err := envManager.Get(ctx, defaultEnvironment) if err == nil && env != nil { envKeyMapper = env.Getenv } @@ -98,24 +99,33 @@ func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*az }, nil } -func (s *projectService) initialize() error { - if s.initialized { - return nil - } - +func (s *projectService) AddService(ctx context.Context, req *azdext.AddServiceRequest) (*azdext.EmptyResponse, error) { azdContext, err := s.lazyAzdContext.GetValue() if err != nil { - return err + return nil, err } - envManager, err := s.lazyEnvManager.GetValue() + projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) if err != nil { - return err + return nil, err + } + + serviceConfig := &project.ServiceConfig{ + Project: projectConfig, + Name: req.Service.Name, + RelativePath: req.Service.RelativePath, + Language: project.ServiceLanguageKind(req.Service.Language), + Host: project.ServiceTargetKind(req.Service.Host), + } + + if projectConfig.Services == nil { + projectConfig.Services = map[string]*project.ServiceConfig{} } - s.azdContext = azdContext - s.envManager = envManager - s.initialized = true + projectConfig.Services[req.Service.Name] = serviceConfig + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } - return nil + return &azdext.EmptyResponse{}, nil } diff --git a/cli/azd/internal/grpcserver/prompt_service.go b/cli/azd/internal/grpcserver/prompt_service.go index 048056bd0d5..09087c6fbb1 100644 --- a/cli/azd/internal/grpcserver/prompt_service.go +++ b/cli/azd/internal/grpcserver/prompt_service.go @@ -297,7 +297,6 @@ func createResourceOptions(options *azdext.PromptResourceOptions) prompt.Resourc 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, diff --git a/cli/azd/internal/grpcserver/server.go b/cli/azd/internal/grpcserver/server.go index febdfaa363f..0ea0f9f1b4e 100644 --- a/cli/azd/internal/grpcserver/server.go +++ b/cli/azd/internal/grpcserver/server.go @@ -31,6 +31,8 @@ type Server struct { userConfigService azdext.UserConfigServiceServer deploymentService azdext.DeploymentServiceServer eventService azdext.EventServiceServer + composeService azdext.ComposeServiceServer + workflowService azdext.WorkflowServiceServer } func NewServer( @@ -40,6 +42,8 @@ func NewServer( userConfigService azdext.UserConfigServiceServer, deploymentService azdext.DeploymentServiceServer, eventService azdext.EventServiceServer, + composeService azdext.ComposeServiceServer, + workflowService azdext.WorkflowServiceServer, ) *Server { return &Server{ projectService: projectService, @@ -48,6 +52,8 @@ func NewServer( userConfigService: userConfigService, deploymentService: deploymentService, eventService: eventService, + composeService: composeService, + workflowService: workflowService, } } @@ -72,13 +78,15 @@ func (s *Server) Start() (*ServerInfo, error) { // Get the assigned random port randomPort := listener.Addr().(*net.TCPAddr).Port - // Register the Greeter service with the gRPC server + // Register the azd services 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) azdext.RegisterEventServiceServer(s.grpcServer, s.eventService) + azdext.RegisterComposeServiceServer(s.grpcServer, s.composeService) + azdext.RegisterWorkflowServiceServer(s.grpcServer, s.workflowService) serverInfo.Address = fmt.Sprintf("localhost:%d", randomPort) serverInfo.Port = randomPort diff --git a/cli/azd/internal/grpcserver/server_test.go b/cli/azd/internal/grpcserver/server_test.go index b9380ed0e0f..8f0296661b3 100644 --- a/cli/azd/internal/grpcserver/server_test.go +++ b/cli/azd/internal/grpcserver/server_test.go @@ -28,6 +28,8 @@ func Test_Server_Start(t *testing.T) { azdext.UnimplementedUserConfigServiceServer{}, azdext.UnimplementedDeploymentServiceServer{}, azdext.UnimplementedEventServiceServer{}, + azdext.UnimplementedComposeServiceServer{}, + azdext.UnimplementedWorkflowServiceServer{}, ) serverInfo, err := server.Start() diff --git a/cli/azd/internal/grpcserver/workflow_service.go b/cli/azd/internal/grpcserver/workflow_service.go new file mode 100644 index 00000000000..496c3a022d0 --- /dev/null +++ b/cli/azd/internal/grpcserver/workflow_service.go @@ -0,0 +1,66 @@ +// 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/workflow" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type WorkflowService struct { + azdext.UnimplementedWorkflowServiceServer + + runner *workflow.Runner +} + +func NewWorkflowService(runner *workflow.Runner) azdext.WorkflowServiceServer { + return &WorkflowService{ + runner: runner, + } +} + +func (s *WorkflowService) Run(ctx context.Context, request *azdext.RunWorkflowRequest) (*azdext.EmptyResponse, error) { + wf := request.Workflow + if wf == nil || len(wf.Steps) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "workflow is empty") + } + + azdWorkflow, err := convertWorkflow(wf) + if err != nil { + return nil, err + } + + if err := s.runner.Run(ctx, azdWorkflow); err != nil { + return nil, status.Errorf(codes.Internal, "failed to run workflow: %v", err) + } + + return &azdext.EmptyResponse{}, nil +} + +func convertWorkflow(wf *azdext.Workflow) (*workflow.Workflow, error) { + azdWorkflow := workflow.Workflow{ + Name: wf.Name, + Steps: []*workflow.Step{}, + } + + for _, step := range wf.Steps { + if step.Command == nil || len(step.Command.Args) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "step command is empty") + } + + azdStep := &workflow.Step{ + AzdCommand: workflow.Command{ + Args: step.Command.Args, + }, + } + + azdWorkflow.Steps = append(azdWorkflow.Steps, azdStep) + } + + return &azdWorkflow, nil +} diff --git a/cli/azd/pkg/azdext/azd_client.go b/cli/azd/pkg/azdext/azd_client.go index 576f36ad27d..50787403004 100644 --- a/cli/azd/pkg/azdext/azd_client.go +++ b/cli/azd/pkg/azdext/azd_client.go @@ -24,6 +24,8 @@ type AzdClient struct { promptClient PromptServiceClient deploymentClient DeploymentServiceClient eventsClient EventServiceClient + composeClient ComposeServiceClient + workflowClient WorkflowServiceClient } // WithAddress sets the address of the `azd` gRPC server. @@ -125,3 +127,21 @@ func (c *AzdClient) Events() EventServiceClient { return c.eventsClient } + +// Compose returns the compose service client. +func (c *AzdClient) Compose() ComposeServiceClient { + if c.composeClient == nil { + c.composeClient = NewComposeServiceClient(c.connection) + } + + return c.composeClient +} + +// Workflow returns the workflow service client. +func (c *AzdClient) Workflow() WorkflowServiceClient { + if c.workflowClient == nil { + c.workflowClient = NewWorkflowServiceClient(c.connection) + } + + return c.workflowClient +} diff --git a/cli/azd/pkg/azdext/compose.pb.go b/cli/azd/pkg/azdext/compose.pb.go index 28a8a0b8296..7b12f95d724 100644 --- a/cli/azd/pkg/azdext/compose.pb.go +++ b/cli/azd/pkg/azdext/compose.pb.go @@ -397,9 +397,10 @@ type ComposedResource struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Config []byte `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Config []byte `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + Uses []string `protobuf:"bytes,4,rep,name=uses,proto3" json:"uses,omitempty"` } func (x *ComposedResource) Reset() { @@ -453,6 +454,13 @@ func (x *ComposedResource) GetConfig() []byte { return nil } +func (x *ComposedResource) GetUses() []string { + if x != nil { + return x.Uses + } + return nil +} + // ComposedResourceType represents a type of composability resource. type ComposedResourceType struct { state protoimpl.MessageState @@ -549,46 +557,47 @@ var file_compose_proto_rawDesc = []byte{ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x52, 0x0a, + 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x66, 0x0a, 0x10, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x4b, 0x0a, 0x14, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, - 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x32, 0x88, - 0x03, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, - 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, - 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x7a, 0x64, - 0x65, 0x78, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, - 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x1e, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x1a, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, - 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 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, 0x3b, 0x61, 0x7a, 0x64, 0x65, 0x78, - 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x73, 0x22, 0x4b, 0x0a, 0x14, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, + 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x32, 0x88, 0x03, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, + 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x47, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x7a, 0x64, + 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, + 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x64, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, + 0x34, 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, 0x3b, 0x61, + 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/cli/azd/pkg/azdext/project.pb.go b/cli/azd/pkg/azdext/project.pb.go index 5b8418f532d..af787dd3027 100644 --- a/cli/azd/pkg/azdext/project.pb.go +++ b/cli/azd/pkg/azdext/project.pb.go @@ -69,6 +69,51 @@ func (x *GetProjectResponse) GetProject() *ProjectConfig { return nil } +type AddServiceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service *ServiceConfig `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` +} + +func (x *AddServiceRequest) Reset() { + *x = AddServiceRequest{} + mi := &file_project_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddServiceRequest) ProtoMessage() {} + +func (x *AddServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_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 AddServiceRequest.ProtoReflect.Descriptor instead. +func (*AddServiceRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{1} +} + +func (x *AddServiceRequest) GetService() *ServiceConfig { + if x != nil { + return x.Service + } + return nil +} + var File_project_proto protoreflect.FileDescriptor var file_project_proto_rawDesc = []byte{ @@ -78,16 +123,24 @@ var file_project_proto_rawDesc = []byte{ 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x32, 0x49, 0x0a, 0x0e, - 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, - 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x7a, - 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, 0x34, 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, 0x3b, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x66, 0x69, 0x67, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x44, 0x0a, 0x11, + 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x32, 0x89, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x14, 0x2e, 0x61, + 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, + 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x19, 0x2e, 0x61, + 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, + 0x5a, 0x34, 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, 0x3b, + 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -102,21 +155,27 @@ func file_project_proto_rawDescGZIP() []byte { return file_project_proto_rawDescData } -var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_project_proto_goTypes = []any{ (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse - (*ProjectConfig)(nil), // 1: azdext.ProjectConfig - (*EmptyRequest)(nil), // 2: azdext.EmptyRequest + (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest + (*ProjectConfig)(nil), // 2: azdext.ProjectConfig + (*ServiceConfig)(nil), // 3: azdext.ServiceConfig + (*EmptyRequest)(nil), // 4: azdext.EmptyRequest + (*EmptyResponse)(nil), // 5: azdext.EmptyResponse } var file_project_proto_depIdxs = []int32{ - 1, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig - 2, // 1: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest - 0, // 2: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig + 3, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig + 4, // 2: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest + 1, // 3: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest + 0, // 4: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse + 5, // 5: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_project_proto_init() } @@ -131,7 +190,7 @@ func file_project_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_project_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/project_grpc.pb.go b/cli/azd/pkg/azdext/project_grpc.pb.go index ccc547e9553..679bc367dc4 100644 --- a/cli/azd/pkg/azdext/project_grpc.pb.go +++ b/cli/azd/pkg/azdext/project_grpc.pb.go @@ -22,7 +22,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" + ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" + ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" ) // ProjectServiceClient is the client API for ProjectService service. @@ -33,6 +34,7 @@ const ( type ProjectServiceClient interface { // Gets the current project. Get(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) + AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) } type projectServiceClient struct { @@ -53,6 +55,16 @@ func (c *projectServiceClient) Get(ctx context.Context, in *EmptyRequest, opts . return out, nil } +func (c *projectServiceClient) AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_AddService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // ProjectServiceServer is the server API for ProjectService service. // All implementations must embed UnimplementedProjectServiceServer // for forward compatibility. @@ -61,6 +73,7 @@ func (c *projectServiceClient) Get(ctx context.Context, in *EmptyRequest, opts . type ProjectServiceServer interface { // Gets the current project. Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) + AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) mustEmbedUnimplementedProjectServiceServer() } @@ -74,6 +87,9 @@ type UnimplementedProjectServiceServer struct{} func (UnimplementedProjectServiceServer) Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") } +func (UnimplementedProjectServiceServer) AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddService not implemented") +} func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} @@ -113,6 +129,24 @@ func _ProjectService_Get_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _ProjectService_AddService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).AddService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_AddService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).AddService(ctx, req.(*AddServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -124,6 +158,10 @@ var ProjectService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Get", Handler: _ProjectService_Get_Handler, }, + { + MethodName: "AddService", + Handler: _ProjectService_AddService_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "project.proto", diff --git a/cli/azd/pkg/azdext/workflow.pb.go b/cli/azd/pkg/azdext/workflow.pb.go new file mode 100644 index 00000000000..d74880ec21f --- /dev/null +++ b/cli/azd/pkg/azdext/workflow.pb.go @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc v5.29.1 +// source: workflow.proto + +package azdext + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RunWorkflowRequest is a request to run a workflow. +type RunWorkflowRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Workflow *Workflow `protobuf:"bytes,1,opt,name=workflow,proto3" json:"workflow,omitempty"` +} + +func (x *RunWorkflowRequest) Reset() { + *x = RunWorkflowRequest{} + mi := &file_workflow_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunWorkflowRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunWorkflowRequest) ProtoMessage() {} + +func (x *RunWorkflowRequest) ProtoReflect() protoreflect.Message { + mi := &file_workflow_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 RunWorkflowRequest.ProtoReflect.Descriptor instead. +func (*RunWorkflowRequest) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0} +} + +func (x *RunWorkflowRequest) GetWorkflow() *Workflow { + if x != nil { + return x.Workflow + } + return nil +} + +// Workflow is a collection of steps to be executed in order. +type Workflow struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Steps []*WorkflowStep `protobuf:"bytes,2,rep,name=steps,proto3" json:"steps,omitempty"` +} + +func (x *Workflow) Reset() { + *x = Workflow{} + mi := &file_workflow_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Workflow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Workflow) ProtoMessage() {} + +func (x *Workflow) ProtoReflect() protoreflect.Message { + mi := &file_workflow_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 Workflow.ProtoReflect.Descriptor instead. +func (*Workflow) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{1} +} + +func (x *Workflow) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Workflow) GetSteps() []*WorkflowStep { + if x != nil { + return x.Steps + } + return nil +} + +// WorkflowStep is a single step in a workflow. +type WorkflowStep struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Command *WorkflowCommand `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` +} + +func (x *WorkflowStep) Reset() { + *x = WorkflowStep{} + mi := &file_workflow_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowStep) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowStep) ProtoMessage() {} + +func (x *WorkflowStep) ProtoReflect() protoreflect.Message { + mi := &file_workflow_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 WorkflowStep.ProtoReflect.Descriptor instead. +func (*WorkflowStep) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{2} +} + +func (x *WorkflowStep) GetCommand() *WorkflowCommand { + if x != nil { + return x.Command + } + return nil +} + +// WorkflowCommand is a command to be executed in a workflow step. +type WorkflowCommand struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Args []string `protobuf:"bytes,1,rep,name=args,proto3" json:"args,omitempty"` +} + +func (x *WorkflowCommand) Reset() { + *x = WorkflowCommand{} + mi := &file_workflow_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowCommand) ProtoMessage() {} + +func (x *WorkflowCommand) ProtoReflect() protoreflect.Message { + mi := &file_workflow_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 WorkflowCommand.ProtoReflect.Descriptor instead. +func (*WorkflowCommand) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{3} +} + +func (x *WorkflowCommand) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +var File_workflow_proto protoreflect.FileDescriptor + +var file_workflow_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 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, 0x42, 0x0a, 0x12, 0x52, 0x75, 0x6e, 0x57, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x08, + 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, + 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0x4a, 0x0a, 0x08, 0x57, 0x6f, + 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x05, 0x73, 0x74, + 0x65, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x7a, 0x64, 0x65, + 0x78, 0x74, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x53, 0x74, 0x65, 0x70, 0x52, + 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, + 0x6f, 0x77, 0x53, 0x74, 0x65, 0x70, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x25, 0x0a, 0x0f, 0x57, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x61, 0x72, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, + 0x32, 0x4b, 0x0a, 0x0f, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x03, 0x52, 0x75, 0x6e, 0x12, 0x1a, 0x2e, 0x61, 0x7a, 0x64, + 0x65, 0x78, 0x74, 0x2e, 0x52, 0x75, 0x6e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x7a, 0x64, 0x65, 0x78, 0x74, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x36, 0x5a, + 0x34, 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, 0x3b, 0x61, + 0x7a, 0x64, 0x65, 0x78, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_workflow_proto_rawDescOnce sync.Once + file_workflow_proto_rawDescData = file_workflow_proto_rawDesc +) + +func file_workflow_proto_rawDescGZIP() []byte { + file_workflow_proto_rawDescOnce.Do(func() { + file_workflow_proto_rawDescData = protoimpl.X.CompressGZIP(file_workflow_proto_rawDescData) + }) + return file_workflow_proto_rawDescData +} + +var file_workflow_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_workflow_proto_goTypes = []any{ + (*RunWorkflowRequest)(nil), // 0: azdext.RunWorkflowRequest + (*Workflow)(nil), // 1: azdext.Workflow + (*WorkflowStep)(nil), // 2: azdext.WorkflowStep + (*WorkflowCommand)(nil), // 3: azdext.WorkflowCommand + (*EmptyResponse)(nil), // 4: azdext.EmptyResponse +} +var file_workflow_proto_depIdxs = []int32{ + 1, // 0: azdext.RunWorkflowRequest.workflow:type_name -> azdext.Workflow + 2, // 1: azdext.Workflow.steps:type_name -> azdext.WorkflowStep + 3, // 2: azdext.WorkflowStep.command:type_name -> azdext.WorkflowCommand + 0, // 3: azdext.WorkflowService.Run:input_type -> azdext.RunWorkflowRequest + 4, // 4: azdext.WorkflowService.Run:output_type -> azdext.EmptyResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_workflow_proto_init() } +func file_workflow_proto_init() { + if File_workflow_proto != nil { + return + } + file_models_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_workflow_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_workflow_proto_goTypes, + DependencyIndexes: file_workflow_proto_depIdxs, + MessageInfos: file_workflow_proto_msgTypes, + }.Build() + File_workflow_proto = out.File + file_workflow_proto_rawDesc = nil + file_workflow_proto_goTypes = nil + file_workflow_proto_depIdxs = nil +} diff --git a/cli/azd/pkg/azdext/workflow_grpc.pb.go b/cli/azd/pkg/azdext/workflow_grpc.pb.go new file mode 100644 index 00000000000..4ea5c3f44f8 --- /dev/null +++ b/cli/azd/pkg/azdext/workflow_grpc.pb.go @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.1 +// source: workflow.proto + +package azdext + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + WorkflowService_Run_FullMethodName = "/azdext.WorkflowService/Run" +) + +// WorkflowServiceClient is the client API for WorkflowService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WorkflowServiceClient interface { + // ListResources retrieves all configured composability resources in the current project. + Run(ctx context.Context, in *RunWorkflowRequest, opts ...grpc.CallOption) (*EmptyResponse, error) +} + +type workflowServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWorkflowServiceClient(cc grpc.ClientConnInterface) WorkflowServiceClient { + return &workflowServiceClient{cc} +} + +func (c *workflowServiceClient) Run(ctx context.Context, in *RunWorkflowRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, WorkflowService_Run_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WorkflowServiceServer is the server API for WorkflowService service. +// All implementations must embed UnimplementedWorkflowServiceServer +// for forward compatibility. +type WorkflowServiceServer interface { + // ListResources retrieves all configured composability resources in the current project. + Run(context.Context, *RunWorkflowRequest) (*EmptyResponse, error) + mustEmbedUnimplementedWorkflowServiceServer() +} + +// UnimplementedWorkflowServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWorkflowServiceServer struct{} + +func (UnimplementedWorkflowServiceServer) Run(context.Context, *RunWorkflowRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Run not implemented") +} +func (UnimplementedWorkflowServiceServer) mustEmbedUnimplementedWorkflowServiceServer() {} +func (UnimplementedWorkflowServiceServer) testEmbeddedByValue() {} + +// UnsafeWorkflowServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WorkflowServiceServer will +// result in compilation errors. +type UnsafeWorkflowServiceServer interface { + mustEmbedUnimplementedWorkflowServiceServer() +} + +func RegisterWorkflowServiceServer(s grpc.ServiceRegistrar, srv WorkflowServiceServer) { + // If the following call pancis, it indicates UnimplementedWorkflowServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WorkflowService_ServiceDesc, srv) +} + +func _WorkflowService_Run_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunWorkflowRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkflowServiceServer).Run(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkflowService_Run_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkflowServiceServer).Run(ctx, req.(*RunWorkflowRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WorkflowService_ServiceDesc is the grpc.ServiceDesc for WorkflowService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WorkflowService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "azdext.WorkflowService", + HandlerType: (*WorkflowServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Run", + Handler: _WorkflowService_Run_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "workflow.proto", +} diff --git a/cli/azd/pkg/prompt/prompt_models.go b/cli/azd/pkg/prompt/prompt_models.go index 0676ffb2f14..7e27d6eba5e 100644 --- a/cli/azd/pkg/prompt/prompt_models.go +++ b/cli/azd/pkg/prompt/prompt_models.go @@ -39,6 +39,10 @@ func NewAzureResourceList(resourceService ResourceService, resources []*arm.Reso // Add adds an Azure resource to the list. func (arl *AzureResourceList) Add(resourceId string) error { + if resourceId == "new" { + return nil + } + if _, has := arl.FindById(resourceId); has { return nil } diff --git a/cli/azd/pkg/prompt/prompt_service.go b/cli/azd/pkg/prompt/prompt_service.go index e7f1172b5eb..e25a2caed5a 100644 --- a/cli/azd/pkg/prompt/prompt_service.go +++ b/cli/azd/pkg/prompt/prompt_service.go @@ -37,8 +37,6 @@ type ResourceOptions struct { 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 } @@ -55,8 +53,8 @@ type CustomResourceOptions[T any] struct { 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) + // NewResourceValue is the default value returned when creating a new resource. + NewResourceValue T } // ResourceGroupOptions contains options for prompting the user to select a resource group. @@ -73,8 +71,6 @@ type SelectOptions struct { 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. @@ -349,7 +345,6 @@ func (ps *promptService) PromptResourceGroup( 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 { @@ -361,7 +356,8 @@ func (ps *promptService) PromptResourceGroup( } return PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceGroup]{ - SelectorOptions: mergedSelectorOptions, + NewResourceValue: azapi.ResourceGroup{Id: "new"}, + SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceGroup, error) { resourceGroupList, err := ps.resourceService.ListResourceGroup(ctx, azureContext.Scope.SubscriptionId, nil) if err != nil { @@ -389,51 +385,6 @@ func (ps *promptService) PromptResourceGroup( 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(ctx) - 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", output.WithHighLightFormat(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 - }, }) } @@ -458,7 +409,9 @@ func (ps *promptService) PromptSubscriptionResource( } var existingResource *arm.ResourceID + var resourceType string if options.ResourceType != nil { + resourceType = string(*options.ResourceType) match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) if has { existingResource = match @@ -495,7 +448,6 @@ func (ps *promptService) PromptSubscriptionResource( 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 { @@ -506,7 +458,15 @@ func (ps *promptService) PromptSubscriptionResource( return nil, err } + allowNewResource := mergedSelectorOptions.AllowNewResource != nil && *mergedSelectorOptions.AllowNewResource + resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ + NewResourceValue: azapi.ResourceExtended{ + Resource: azapi.Resource{ + Id: "new", + Type: resourceType, + }, + }, SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { var resourceListOptions *armresources.ClientListOptions @@ -534,7 +494,7 @@ func (ps *promptService) PromptSubscriptionResource( } } - if len(filteredResources) == 0 { + if len(filteredResources) == 0 && !allowNewResource { if options.ResourceType == nil { return nil, ErrNoResourcesFound } @@ -556,8 +516,7 @@ func (ps *promptService) PromptSubscriptionResource( output.WithGrayFormat("(%s)", parsedResource.ResourceGroupName), ), nil }, - Selected: options.Selected, - CreateResource: options.CreateResource, + Selected: options.Selected, }) if err != nil { @@ -592,7 +551,9 @@ func (ps *promptService) PromptResourceGroupResource( } var existingResource *arm.ResourceID + var resourceType string if options.ResourceType != nil { + resourceType = string(*options.ResourceType) match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) if has { existingResource = match @@ -625,7 +586,6 @@ func (ps *promptService) PromptResourceGroupResource( 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 { @@ -636,7 +596,15 @@ func (ps *promptService) PromptResourceGroupResource( return nil, err } + allowNewResource := mergedSelectorOptions.AllowNewResource != nil && *mergedSelectorOptions.AllowNewResource + resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ + NewResourceValue: azapi.ResourceExtended{ + Resource: azapi.Resource{ + Id: "new", + Type: resourceType, + }, + }, Selected: options.Selected, SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { @@ -666,7 +634,7 @@ func (ps *promptService) PromptResourceGroupResource( } } - if len(filteredResources) == 0 { + if len(filteredResources) == 0 && !allowNewResource { if options.ResourceType == nil { return nil, ErrNoResourcesFound } @@ -679,7 +647,6 @@ func (ps *promptService) PromptResourceGroupResource( DisplayResource: func(resource *azapi.ResourceExtended) (string, error) { return resource.Name, nil }, - CreateResource: options.CreateResource, }) if err != nil { @@ -710,7 +677,6 @@ func PromptCustomResource[T any](ctx context.Context, options CustomResourceOpti AllowNewResource: ux.Ptr(true), ForceNewResource: ux.Ptr(false), NewResourceMessage: "Create new resource", - CreatingMessage: "Creating new resource...", DisplayNumbers: ux.Ptr(true), DisplayCount: 10, } @@ -839,16 +805,7 @@ func PromptCustomResource[T any](ctx context.Context, options CustomResourceOpti // 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 + selectedResource = &options.NewResourceValue } else { // If a new resource is allowed, decrement the selected index if allowNewResource { diff --git a/cli/azd/pkg/ux/confirm.go b/cli/azd/pkg/ux/confirm.go index 3c502204364..118cb33f338 100644 --- a/cli/azd/pkg/ux/confirm.go +++ b/cli/azd/pkg/ux/confirm.go @@ -115,8 +115,7 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { } inputConfig := &internal.InputConfig{ - InitialValue: p.displayValue, - IgnoreHintKeys: true, + InitialValue: p.displayValue, } if err := p.canvas.Run(); err != nil { @@ -145,7 +144,7 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { if !p.hasValidationError { p.complete = true } - } else { + } else if !p.showHelp { p.hasValidationError = false if args.Value == "" && p.options.DefaultValue != nil { p.value = p.options.DefaultValue @@ -155,7 +154,7 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { if err != nil { p.hasValidationError = true p.value = nil - p.displayValue = args.Value + p.displayValue = "" } else { p.value = value p.displayValue = getBooleanString(*value) @@ -214,7 +213,7 @@ func (p *Confirm) Render(printer Printer) error { } // Validation error - if !p.showHelp && p.hasValidationError { + if p.hasValidationError { printer.Fprintln(output.WithWarningFormat("Enter a valid value")) } diff --git a/cli/azd/pkg/ux/multi_select.go b/cli/azd/pkg/ux/multi_select.go index adb865e3fa0..6cabe20c163 100644 --- a/cli/azd/pkg/ux/multi_select.go +++ b/cli/azd/pkg/ux/multi_select.go @@ -137,6 +137,10 @@ func (p *MultiSelect) Ask(ctx context.Context) ([]*MultiSelectChoice, error) { p.cursor.HideCursor() } + defer func() { + p.cursor.ShowCursor() + }() + if err := p.canvas.Run(); err != nil { return nil, err } @@ -262,7 +266,10 @@ func (p *MultiSelect) applyFilter() { } } - if strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) { + containsValue := strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) + containsLabel := strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) + + if containsValue || containsLabel { p.filteredChoices = append(p.filteredChoices, option) } } diff --git a/cli/azd/pkg/ux/printer.go b/cli/azd/pkg/ux/printer.go index c7b3437ddf3..ec4a50e3194 100644 --- a/cli/azd/pkg/ux/printer.go +++ b/cli/azd/pkg/ux/printer.go @@ -45,6 +45,9 @@ func NewPrinter(writer io.Writer) Printer { } width, _ := consolesize.GetConsoleSize() + if width == 0 { + width = 100 + } printer := &printer{ Cursor: internal.NewCursor(writer), diff --git a/cli/azd/pkg/ux/select.go b/cli/azd/pkg/ux/select.go index c406619f929..04dbd02b84e 100644 --- a/cli/azd/pkg/ux/select.go +++ b/cli/azd/pkg/ux/select.go @@ -137,6 +137,10 @@ func (p *Select) Ask(ctx context.Context) (*int, error) { p.cursor.HideCursor() } + defer func() { + p.cursor.ShowCursor() + }() + if err := p.canvas.Run(); err != nil { return nil, err } @@ -212,7 +216,10 @@ func (p *Select) applyFilter() { } } - if strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) { + containsValue := strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) + containsLabel := strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) + + if containsValue || containsLabel { p.filteredChoices = append(p.filteredChoices, option) } } diff --git a/cli/azd/pkg/ux/spinner.go b/cli/azd/pkg/ux/spinner.go index 638998942fd..9237f514349 100644 --- a/cli/azd/pkg/ux/spinner.go +++ b/cli/azd/pkg/ux/spinner.go @@ -6,10 +6,7 @@ package ux import ( "context" "io" - "log" "os" - "sync" - "sync/atomic" "time" "dario.cat/mergo" @@ -23,11 +20,10 @@ type Spinner struct { cursor internal.Cursor options *SpinnerOptions - running int32 animationIndex int text string clear bool - canvasMutex sync.Mutex + cancel context.CancelFunc } // SpinnerOptions represents the options for the Spinner component. @@ -66,9 +62,6 @@ func NewSpinner(options *SpinnerOptions) *Spinner { // WithCanvas sets the canvas for the spinner. func (s *Spinner) WithCanvas(canvas Canvas) Visual { - s.canvasMutex.Lock() - defer s.canvasMutex.Unlock() - if canvas != nil { s.canvas = canvas } @@ -78,40 +71,57 @@ func (s *Spinner) WithCanvas(canvas Canvas) Visual { // Start starts the spinner. func (s *Spinner) Start(ctx context.Context) error { - s.ensureCanvas() + if s.canvas == nil { + s.canvas = NewCanvas(s).WithWriter(s.options.Writer) + } + + // Use a context to determine when to stop the spinner + cancelCtx, cancel := context.WithCancel(ctx) + s.cancel = cancel s.clear = false - atomic.StoreInt32(&s.running, 1) s.cursor.HideCursor() + if err := s.canvas.Run(); err != nil { + return err + } + + // Periodic update goroutine go func(ctx context.Context) { for { - if atomic.LoadInt32(&s.running) == 0 { - return - } - - if err := s.update(); err != nil { - log.Println("Failed to update spinner:", err) + select { + // Context is stopped, exit + case <-ctx.Done(): return + // Update the spinner on each tick interval + case <-time.After(s.options.Interval): + _ = s.canvas.Update() } - - time.Sleep(s.options.Interval) } - }(ctx) + }(cancelCtx) - return s.run() + return nil } // Stop stops the spinner. func (s *Spinner) Stop(ctx context.Context) error { - s.ensureCanvas() + defer func() { + s.cursor.ShowCursor() + }() - atomic.StoreInt32(&s.running, 0) - s.cursor.ShowCursor() + if s.cancel == nil { + return nil + } if s.options.ClearOnStop { s.clear = true - return s.update() + } + + s.cancel() + s.cancel = nil + + if err := s.canvas.Update(); err != nil { + return err } return nil @@ -119,8 +129,6 @@ func (s *Spinner) Stop(ctx context.Context) error { // Run runs a task with the spinner. func (s *Spinner) Run(ctx context.Context, task func(context.Context) error) error { - s.ensureCanvas() - s.options.ClearOnStop = true if err := s.Start(ctx); err != nil { @@ -145,8 +153,7 @@ func (s *Spinner) Render(printer Printer) error { return nil } - printer.Fprintf(output.WithHintFormat(s.options.Animation[s.animationIndex])) - printer.Fprintf(" %s", s.text) + printer.Fprintf("%s %s", output.WithHintFormat(s.options.Animation[s.animationIndex]), s.text) if s.animationIndex == len(s.options.Animation)-1 { s.animationIndex = 0 @@ -156,34 +163,3 @@ func (s *Spinner) Render(printer Printer) error { return nil } - -func (s *Spinner) ensureCanvas() { - s.canvasMutex.Lock() - defer s.canvasMutex.Unlock() - - if s.canvas == nil { - s.canvas = NewCanvas(s).WithWriter(s.options.Writer) - } -} - -func (s *Spinner) update() error { - s.canvasMutex.Lock() - defer s.canvasMutex.Unlock() - - if s.canvas == nil { - return nil - } - - return s.canvas.Update() -} - -func (s *Spinner) run() error { - s.canvasMutex.Lock() - defer s.canvasMutex.Unlock() - - if s.canvas == nil { - return nil - } - - return s.canvas.Run() -} From 91b053382e6e84b072c0459c43639310e4496d2a Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 11:50:17 -0700 Subject: [PATCH 2/8] Adds unit tests for compose & project services --- .../grpcserver/compose_service_test.go | 158 ++++++++++++++++++ .../grpcserver/project_service_test.go | 57 +++++++ 2 files changed, 215 insertions(+) create mode 100644 cli/azd/internal/grpcserver/compose_service_test.go diff --git a/cli/azd/internal/grpcserver/compose_service_test.go b/cli/azd/internal/grpcserver/compose_service_test.go new file mode 100644 index 00000000000..1db321e1776 --- /dev/null +++ b/cli/azd/internal/grpcserver/compose_service_test.go @@ -0,0 +1,158 @@ +package grpcserver + +import ( + "context" + "encoding/json" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "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" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/stretchr/testify/require" +) + +func Test_ComposeService_AddResource(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdCtx := azdcontext.NewAzdContextWithDirectory(temp) + projectConfig := project.ProjectConfig{ + Name: "test", + } + err := project.Save(*mockContext.Context, &projectConfig, azdCtx.ProjectPath()) + require.NoError(t, err) + lazyAzdContext := lazy.From(azdCtx) + composeService := NewComposeService(lazyAzdContext) + + t.Run("success", func(t *testing.T) { + addReq := &azdext.AddResourceRequest{ + Resource: &azdext.ComposedResource{ + Name: "resource1", + Type: "Storage", + Config: []byte("{}"), + Uses: []string{}, + }, + } + addResp, err := composeService.AddResource(*mockContext.Context, addReq) + require.NoError(t, err) + require.NotNil(t, addResp) + require.Equal(t, addReq.Resource.Name, addResp.Resource.Name) + + updatedConfig, err := project.Load(*mockContext.Context, azdCtx.ProjectPath()) + require.NoError(t, err) + res, exists := updatedConfig.Resources["resource1"] + require.True(t, exists) + require.Equal(t, "resource1", res.Name) + require.Equal(t, project.ResourceType("Storage"), res.Type) + }) + + t.Run("invalid config", func(t *testing.T) { + // reuse the same common setup. + addReq := &azdext.AddResourceRequest{ + Resource: &azdext.ComposedResource{ + Name: "invalid", + Type: "storage", + Config: []byte("invalid json"), + Uses: []string{}, + }, + } + _, err := composeService.AddResource(*mockContext.Context, addReq) + require.Error(t, err) + }) +} + +func Test_ComposeService_GetResource(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdCtx := azdcontext.NewAzdContextWithDirectory(temp) + projectConfig := project.ProjectConfig{ + Name: "test", + Resources: map[string]*project.ResourceConfig{ + "resource1": { + Name: "resource1", + Type: project.ResourceTypeStorage, + Props: project.StorageProps{}, + Uses: []string{}, + }, + }, + } + err := project.Save(*mockContext.Context, &projectConfig, azdCtx.ProjectPath()) + require.NoError(t, err) + lazyAzdContext := lazy.From(azdCtx) + composeService := NewComposeService(lazyAzdContext) + + t.Run("success", func(t *testing.T) { + getReq := &azdext.GetResourceRequest{ + Name: "resource1", + } + getResp, err := composeService.GetResource(*mockContext.Context, getReq) + require.NoError(t, err) + require.NotNil(t, getResp) + require.Equal(t, "resource1", getResp.Resource.Name) + require.Equal(t, "storage", getResp.Resource.Type) + + configBytes, err := json.Marshal(project.StorageProps{}) + require.NoError(t, err) + require.JSONEq(t, string(configBytes), string(getResp.Resource.Config)) + }) + + t.Run("resource not found", func(t *testing.T) { + getReq := &azdext.GetResourceRequest{ + Name: "nonexistent", + } + _, err = composeService.GetResource(*mockContext.Context, getReq) + require.Error(t, err) + }) +} + +func Test_ComposeService_ListResources(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + t.Run("success", func(t *testing.T) { + temp := t.TempDir() + azdCtx := azdcontext.NewAzdContextWithDirectory(temp) + projectConfig := project.ProjectConfig{ + Name: "test", + Resources: map[string]*project.ResourceConfig{ + "resource1": { + Name: "resource1", + Type: project.ResourceTypeStorage, + Props: project.StorageProps{}, + Uses: []string{}, + }, + "resource2": { + Name: "resource2", + Type: project.ResourceTypeDbCosmos, + Props: project.CosmosDBProps{}, + Uses: []string{"resource1"}, + }, + }, + } + err := project.Save(*mockContext.Context, &projectConfig, azdCtx.ProjectPath()) + require.NoError(t, err) + lazyAzdContext := lazy.From(azdCtx) + composeService := NewComposeService(lazyAzdContext) + + listResp, err := composeService.ListResources(*mockContext.Context, &azdext.EmptyRequest{}) + require.NoError(t, err) + require.NotNil(t, listResp) + require.Len(t, listResp.Resources, 2) + names := map[string]bool{} + for _, res := range listResp.Resources { + names[res.Name] = true + } + require.True(t, names["resource1"]) + require.True(t, names["resource2"]) + }) + + t.Run("no project", func(t *testing.T) { + // For this subtest, simulate no project using a lazy context + lazyAzdContext := lazy.NewLazy(func() (*azdcontext.AzdContext, error) { + return nil, azdcontext.ErrNoProject + }) + composeService := NewComposeService(lazyAzdContext) + _, err := composeService.ListResources(*mockContext.Context, &azdext.EmptyRequest{}) + require.Error(t, err) + }) +} diff --git a/cli/azd/internal/grpcserver/project_service_test.go b/cli/azd/internal/grpcserver/project_service_test.go index 4ec2663a2bf..ff55a66ef13 100644 --- a/cli/azd/internal/grpcserver/project_service_test.go +++ b/cli/azd/internal/grpcserver/project_service_test.go @@ -5,6 +5,7 @@ package grpcserver import ( "context" + "path/filepath" "testing" "github.com/azure/azure-dev/cli/azd/pkg/azdext" @@ -84,3 +85,59 @@ func Test_ProjectService_Flow(t *testing.T) { require.NotNil(t, getResponse) require.Equal(t, projectConfig.Name, getResponse.Project.Name) } + +func Test_ProjectService_AddService(t *testing.T) { + // Setup a mock context and temporary project directory. + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + + // Initialize AzdContext with the temporary directory. + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Define and save project configuration. + projectConfig := project.ProjectConfig{ + Name: "test", + } + err := project.Save(*mockContext.Context, &projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Configure and initialize environment manager. + fileConfigManager := config.NewFileConfigManager(config.NewManager()) + localDataStore := environment.NewLocalFileDataStore(azdContext, fileConfigManager) + envManager, err := environment.NewManager(mockContext.Container, azdContext, mockContext.Console, localDataStore, nil) + require.NoError(t, err) + require.NotNil(t, envManager) + + // Create lazy-loaded instances. + lazyAzdContext := lazy.From(azdContext) + lazyEnvManager := lazy.From(envManager) + + // Create the project service. + service := NewProjectService(lazyAzdContext, lazyEnvManager) + + // Prepare a new service addition request. + serviceRequest := &azdext.AddServiceRequest{ + Service: &azdext.ServiceConfig{ + Name: "service1", + RelativePath: filepath.Join("src", "service1"), + Language: "python", + Host: "containerapp", + }, + } + + // Call AddService. + emptyResponse, err := service.AddService(*mockContext.Context, serviceRequest) + require.NoError(t, err) + require.NotNil(t, emptyResponse) + + // Reload the project configuration and verify the service was added. + updatedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath()) + require.NoError(t, err) + require.NotNil(t, updatedConfig.Services) + serviceConfig, exists := updatedConfig.Services["service1"] + require.True(t, exists) + require.Equal(t, "service1", serviceConfig.Name) + require.Equal(t, filepath.Join("src", "service1"), serviceConfig.RelativePath) + require.Equal(t, project.ServiceLanguagePython, serviceConfig.Language) + require.Equal(t, project.ContainerAppTarget, serviceConfig.Host) +} From 2ed37542091fe9631ce28f5915b24bdc16fdc38f Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 13:48:49 -0700 Subject: [PATCH 3/8] Adds unit tests for workflow service --- .../grpcserver/workflow_service_test.go | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 cli/azd/internal/grpcserver/workflow_service_test.go diff --git a/cli/azd/internal/grpcserver/workflow_service_test.go b/cli/azd/internal/grpcserver/workflow_service_test.go new file mode 100644 index 00000000000..67111ae5ef8 --- /dev/null +++ b/cli/azd/internal/grpcserver/workflow_service_test.go @@ -0,0 +1,102 @@ +package grpcserver + +import ( + "context" + "errors" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/workflow" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_WorkflowService_Run_Success(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + t.Run("Success", func(t *testing.T) { + testRunner := &TestWorkflowRunner{} + runner := workflow.NewRunner(testRunner, mockContext.Console) + testRunner.On("SetArgs", mock.Anything) + testRunner.On("ExecuteContext", *mockContext.Context).Return(nil) + + service := NewWorkflowService(runner) + + // Create a valid, non-empty workflow. + req := &azdext.RunWorkflowRequest{ + Workflow: &azdext.Workflow{ + Name: "testWorkflow", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"provision"}, + }, + }, + }, + }, + } + + // Act + resp, err := service.Run(*mockContext.Context, req) + + // Assert + require.NoError(t, err) + require.NotNil(t, resp) + + // Verify that the runner's Run method was invoked. + testRunner.AssertCalled(t, "SetArgs", []string{"provision"}) + testRunner.AssertCalled(t, "ExecuteContext", *mockContext.Context) + }) + + t.Run("Failure", func(t *testing.T) { + expectedErr := errors.New("execution failed") + testRunner := &TestWorkflowRunner{} + runner := workflow.NewRunner(testRunner, mockContext.Console) + testRunner.On("SetArgs", mock.Anything) + testRunner.On("ExecuteContext", *mockContext.Context).Return(expectedErr) + + service := NewWorkflowService(runner) + + // Create a valid, non-empty workflow. + req := &azdext.RunWorkflowRequest{ + Workflow: &azdext.Workflow{ + Name: "testWorkflow", + Steps: []*azdext.WorkflowStep{ + { + Command: &azdext.WorkflowCommand{ + Args: []string{"provision"}, + }, + }, + }, + }, + } + + // Act + resp, err := service.Run(*mockContext.Context, req) + + // Assert + require.Error(t, err) + require.Nil(t, resp) + + // Verify that the runner's Run method was invoked. + testRunner.AssertCalled(t, "SetArgs", []string{"provision"}) + testRunner.AssertCalled(t, "ExecuteContext", *mockContext.Context) + }) +} + +// Updated TestWorkflowRunner using testify/mock. +type TestWorkflowRunner struct { + mock.Mock +} + +// Modified SetArgs to use testify/mock. +func (r *TestWorkflowRunner) SetArgs(args []string) { + r.Called(args) +} + +// Modified ExecuteContext to use testify/mock. +func (r *TestWorkflowRunner) ExecuteContext(ctx context.Context) error { + ret := r.Called(ctx) + return ret.Error(0) +} From 20353aad5b40548c96b43078b3212b0037ffd198 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 13:50:59 -0700 Subject: [PATCH 4/8] Rollback us and prompt updates --- cli/azd/.vscode/cspell.yaml | 5 -- cli/azd/pkg/prompt/prompt_models.go | 4 -- cli/azd/pkg/prompt/prompt_service.go | 99 ++++++++++++++++++++-------- cli/azd/pkg/ux/confirm.go | 9 +-- cli/azd/pkg/ux/multi_select.go | 9 +-- cli/azd/pkg/ux/printer.go | 3 - cli/azd/pkg/ux/select.go | 9 +-- cli/azd/pkg/ux/spinner.go | 94 ++++++++++++++++---------- 8 files changed, 137 insertions(+), 95 deletions(-) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index cebe961a2bd..94787deb500 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -150,11 +150,6 @@ overrides: - getenv - errorf - println - - filename: extensions/microsoft.azd.ai.builder/internal/cmd/start.go - words: - - dall - - datasource - - vectorizing ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/pkg/prompt/prompt_models.go b/cli/azd/pkg/prompt/prompt_models.go index 7e27d6eba5e..0676ffb2f14 100644 --- a/cli/azd/pkg/prompt/prompt_models.go +++ b/cli/azd/pkg/prompt/prompt_models.go @@ -39,10 +39,6 @@ func NewAzureResourceList(resourceService ResourceService, resources []*arm.Reso // Add adds an Azure resource to the list. func (arl *AzureResourceList) Add(resourceId string) error { - if resourceId == "new" { - return nil - } - if _, has := arl.FindById(resourceId); has { return nil } diff --git a/cli/azd/pkg/prompt/prompt_service.go b/cli/azd/pkg/prompt/prompt_service.go index e25a2caed5a..e7f1172b5eb 100644 --- a/cli/azd/pkg/prompt/prompt_service.go +++ b/cli/azd/pkg/prompt/prompt_service.go @@ -37,6 +37,8 @@ type ResourceOptions struct { 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 } @@ -53,8 +55,8 @@ type CustomResourceOptions[T any] struct { SortResource func(a *T, b *T) int // Selected is a function that determines if a resource is selected Selected func(resource *T) bool - // NewResourceValue is the default value returned when creating a new resource. - NewResourceValue T + // 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. @@ -71,6 +73,8 @@ type SelectOptions struct { 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. @@ -345,6 +349,7 @@ func (ps *promptService) PromptResourceGroup( 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 { @@ -356,8 +361,7 @@ func (ps *promptService) PromptResourceGroup( } return PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceGroup]{ - NewResourceValue: azapi.ResourceGroup{Id: "new"}, - SelectorOptions: mergedSelectorOptions, + SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceGroup, error) { resourceGroupList, err := ps.resourceService.ListResourceGroup(ctx, azureContext.Scope.SubscriptionId, nil) if err != nil { @@ -385,6 +389,51 @@ func (ps *promptService) PromptResourceGroup( 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(ctx) + 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", output.WithHighLightFormat(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 + }, }) } @@ -409,9 +458,7 @@ func (ps *promptService) PromptSubscriptionResource( } var existingResource *arm.ResourceID - var resourceType string if options.ResourceType != nil { - resourceType = string(*options.ResourceType) match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) if has { existingResource = match @@ -448,6 +495,7 @@ func (ps *promptService) PromptSubscriptionResource( 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 { @@ -458,15 +506,7 @@ func (ps *promptService) PromptSubscriptionResource( return nil, err } - allowNewResource := mergedSelectorOptions.AllowNewResource != nil && *mergedSelectorOptions.AllowNewResource - resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ - NewResourceValue: azapi.ResourceExtended{ - Resource: azapi.Resource{ - Id: "new", - Type: resourceType, - }, - }, SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { var resourceListOptions *armresources.ClientListOptions @@ -494,7 +534,7 @@ func (ps *promptService) PromptSubscriptionResource( } } - if len(filteredResources) == 0 && !allowNewResource { + if len(filteredResources) == 0 { if options.ResourceType == nil { return nil, ErrNoResourcesFound } @@ -516,7 +556,8 @@ func (ps *promptService) PromptSubscriptionResource( output.WithGrayFormat("(%s)", parsedResource.ResourceGroupName), ), nil }, - Selected: options.Selected, + Selected: options.Selected, + CreateResource: options.CreateResource, }) if err != nil { @@ -551,9 +592,7 @@ func (ps *promptService) PromptResourceGroupResource( } var existingResource *arm.ResourceID - var resourceType string if options.ResourceType != nil { - resourceType = string(*options.ResourceType) match, has := azureContext.Resources.FindByTypeAndKind(ctx, *options.ResourceType, options.Kinds) if has { existingResource = match @@ -586,6 +625,7 @@ func (ps *promptService) PromptResourceGroupResource( 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 { @@ -596,15 +636,7 @@ func (ps *promptService) PromptResourceGroupResource( return nil, err } - allowNewResource := mergedSelectorOptions.AllowNewResource != nil && *mergedSelectorOptions.AllowNewResource - resource, err := PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceExtended]{ - NewResourceValue: azapi.ResourceExtended{ - Resource: azapi.Resource{ - Id: "new", - Type: resourceType, - }, - }, Selected: options.Selected, SelectorOptions: mergedSelectorOptions, LoadData: func(ctx context.Context) ([]*azapi.ResourceExtended, error) { @@ -634,7 +666,7 @@ func (ps *promptService) PromptResourceGroupResource( } } - if len(filteredResources) == 0 && !allowNewResource { + if len(filteredResources) == 0 { if options.ResourceType == nil { return nil, ErrNoResourcesFound } @@ -647,6 +679,7 @@ func (ps *promptService) PromptResourceGroupResource( DisplayResource: func(resource *azapi.ResourceExtended) (string, error) { return resource.Name, nil }, + CreateResource: options.CreateResource, }) if err != nil { @@ -677,6 +710,7 @@ func PromptCustomResource[T any](ctx context.Context, options CustomResourceOpti AllowNewResource: ux.Ptr(true), ForceNewResource: ux.Ptr(false), NewResourceMessage: "Create new resource", + CreatingMessage: "Creating new resource...", DisplayNumbers: ux.Ptr(true), DisplayCount: 10, } @@ -805,7 +839,16 @@ func PromptCustomResource[T any](ctx context.Context, options CustomResourceOpti // Create new resource if allowNewResource && *selectedIndex == 0 { - selectedResource = &options.NewResourceValue + 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 { diff --git a/cli/azd/pkg/ux/confirm.go b/cli/azd/pkg/ux/confirm.go index 118cb33f338..3c502204364 100644 --- a/cli/azd/pkg/ux/confirm.go +++ b/cli/azd/pkg/ux/confirm.go @@ -115,7 +115,8 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { } inputConfig := &internal.InputConfig{ - InitialValue: p.displayValue, + InitialValue: p.displayValue, + IgnoreHintKeys: true, } if err := p.canvas.Run(); err != nil { @@ -144,7 +145,7 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { if !p.hasValidationError { p.complete = true } - } else if !p.showHelp { + } else { p.hasValidationError = false if args.Value == "" && p.options.DefaultValue != nil { p.value = p.options.DefaultValue @@ -154,7 +155,7 @@ func (p *Confirm) Ask(ctx context.Context) (*bool, error) { if err != nil { p.hasValidationError = true p.value = nil - p.displayValue = "" + p.displayValue = args.Value } else { p.value = value p.displayValue = getBooleanString(*value) @@ -213,7 +214,7 @@ func (p *Confirm) Render(printer Printer) error { } // Validation error - if p.hasValidationError { + if !p.showHelp && p.hasValidationError { printer.Fprintln(output.WithWarningFormat("Enter a valid value")) } diff --git a/cli/azd/pkg/ux/multi_select.go b/cli/azd/pkg/ux/multi_select.go index 6cabe20c163..adb865e3fa0 100644 --- a/cli/azd/pkg/ux/multi_select.go +++ b/cli/azd/pkg/ux/multi_select.go @@ -137,10 +137,6 @@ func (p *MultiSelect) Ask(ctx context.Context) ([]*MultiSelectChoice, error) { p.cursor.HideCursor() } - defer func() { - p.cursor.ShowCursor() - }() - if err := p.canvas.Run(); err != nil { return nil, err } @@ -266,10 +262,7 @@ func (p *MultiSelect) applyFilter() { } } - containsValue := strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) - containsLabel := strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) - - if containsValue || containsLabel { + if strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) { p.filteredChoices = append(p.filteredChoices, option) } } diff --git a/cli/azd/pkg/ux/printer.go b/cli/azd/pkg/ux/printer.go index ec4a50e3194..c7b3437ddf3 100644 --- a/cli/azd/pkg/ux/printer.go +++ b/cli/azd/pkg/ux/printer.go @@ -45,9 +45,6 @@ func NewPrinter(writer io.Writer) Printer { } width, _ := consolesize.GetConsoleSize() - if width == 0 { - width = 100 - } printer := &printer{ Cursor: internal.NewCursor(writer), diff --git a/cli/azd/pkg/ux/select.go b/cli/azd/pkg/ux/select.go index 04dbd02b84e..c406619f929 100644 --- a/cli/azd/pkg/ux/select.go +++ b/cli/azd/pkg/ux/select.go @@ -137,10 +137,6 @@ func (p *Select) Ask(ctx context.Context) (*int, error) { p.cursor.HideCursor() } - defer func() { - p.cursor.ShowCursor() - }() - if err := p.canvas.Run(); err != nil { return nil, err } @@ -216,10 +212,7 @@ func (p *Select) applyFilter() { } } - containsValue := strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) - containsLabel := strings.Contains(strings.ToLower(option.Label), strings.ToLower(p.filter)) - - if containsValue || containsLabel { + if strings.Contains(strings.ToLower(option.Value), strings.ToLower(p.filter)) { p.filteredChoices = append(p.filteredChoices, option) } } diff --git a/cli/azd/pkg/ux/spinner.go b/cli/azd/pkg/ux/spinner.go index 9237f514349..638998942fd 100644 --- a/cli/azd/pkg/ux/spinner.go +++ b/cli/azd/pkg/ux/spinner.go @@ -6,7 +6,10 @@ package ux import ( "context" "io" + "log" "os" + "sync" + "sync/atomic" "time" "dario.cat/mergo" @@ -20,10 +23,11 @@ type Spinner struct { cursor internal.Cursor options *SpinnerOptions + running int32 animationIndex int text string clear bool - cancel context.CancelFunc + canvasMutex sync.Mutex } // SpinnerOptions represents the options for the Spinner component. @@ -62,6 +66,9 @@ func NewSpinner(options *SpinnerOptions) *Spinner { // WithCanvas sets the canvas for the spinner. func (s *Spinner) WithCanvas(canvas Canvas) Visual { + s.canvasMutex.Lock() + defer s.canvasMutex.Unlock() + if canvas != nil { s.canvas = canvas } @@ -71,57 +78,40 @@ func (s *Spinner) WithCanvas(canvas Canvas) Visual { // Start starts the spinner. func (s *Spinner) Start(ctx context.Context) error { - if s.canvas == nil { - s.canvas = NewCanvas(s).WithWriter(s.options.Writer) - } - - // Use a context to determine when to stop the spinner - cancelCtx, cancel := context.WithCancel(ctx) - s.cancel = cancel + s.ensureCanvas() s.clear = false + atomic.StoreInt32(&s.running, 1) s.cursor.HideCursor() - if err := s.canvas.Run(); err != nil { - return err - } - - // Periodic update goroutine go func(ctx context.Context) { for { - select { - // Context is stopped, exit - case <-ctx.Done(): + if atomic.LoadInt32(&s.running) == 0 { + return + } + + if err := s.update(); err != nil { + log.Println("Failed to update spinner:", err) return - // Update the spinner on each tick interval - case <-time.After(s.options.Interval): - _ = s.canvas.Update() } + + time.Sleep(s.options.Interval) } - }(cancelCtx) + }(ctx) - return nil + return s.run() } // Stop stops the spinner. func (s *Spinner) Stop(ctx context.Context) error { - defer func() { - s.cursor.ShowCursor() - }() + s.ensureCanvas() - if s.cancel == nil { - return nil - } + atomic.StoreInt32(&s.running, 0) + s.cursor.ShowCursor() if s.options.ClearOnStop { s.clear = true - } - - s.cancel() - s.cancel = nil - - if err := s.canvas.Update(); err != nil { - return err + return s.update() } return nil @@ -129,6 +119,8 @@ func (s *Spinner) Stop(ctx context.Context) error { // Run runs a task with the spinner. func (s *Spinner) Run(ctx context.Context, task func(context.Context) error) error { + s.ensureCanvas() + s.options.ClearOnStop = true if err := s.Start(ctx); err != nil { @@ -153,7 +145,8 @@ func (s *Spinner) Render(printer Printer) error { return nil } - printer.Fprintf("%s %s", output.WithHintFormat(s.options.Animation[s.animationIndex]), s.text) + printer.Fprintf(output.WithHintFormat(s.options.Animation[s.animationIndex])) + printer.Fprintf(" %s", s.text) if s.animationIndex == len(s.options.Animation)-1 { s.animationIndex = 0 @@ -163,3 +156,34 @@ func (s *Spinner) Render(printer Printer) error { return nil } + +func (s *Spinner) ensureCanvas() { + s.canvasMutex.Lock() + defer s.canvasMutex.Unlock() + + if s.canvas == nil { + s.canvas = NewCanvas(s).WithWriter(s.options.Writer) + } +} + +func (s *Spinner) update() error { + s.canvasMutex.Lock() + defer s.canvasMutex.Unlock() + + if s.canvas == nil { + return nil + } + + return s.canvas.Update() +} + +func (s *Spinner) run() error { + s.canvasMutex.Lock() + defer s.canvasMutex.Unlock() + + if s.canvas == nil { + return nil + } + + return s.canvas.Run() +} From 3d6a9adba7e70fa96ad1fabf36fc0ecdec68daca Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 13:58:50 -0700 Subject: [PATCH 5/8] Makes grpc services internal and updates inline docs --- .../internal/grpcserver/compose_service.go | 167 +++++++++--------- .../internal/grpcserver/workflow_service.go | 10 +- 2 files changed, 92 insertions(+), 85 deletions(-) diff --git a/cli/azd/internal/grpcserver/compose_service.go b/cli/azd/internal/grpcserver/compose_service.go index 8d37664d8be..9985b2a87f3 100644 --- a/cli/azd/internal/grpcserver/compose_service.go +++ b/cli/azd/internal/grpcserver/compose_service.go @@ -16,7 +16,8 @@ import ( "google.golang.org/grpc/status" ) -type ComposeService struct { +// composeService exposes features of the AZD composability model to the Extensions Framework layer. +type composeService struct { azdext.UnimplementedComposeServiceServer lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] @@ -25,13 +26,13 @@ type ComposeService struct { func NewComposeService( lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], ) azdext.ComposeServiceServer { - return &ComposeService{ + return &composeService{ lazyAzdContext: lazyAzdContext, } } -// AddResource implements azdext.ComposeServiceServer. -func (c *ComposeService) AddResource( +// AddResource adds or updates a resource with the given name in the project configuration. +func (c *composeService) AddResource( ctx context.Context, req *azdext.AddResourceRequest, ) (*azdext.AddResourceResponse, error) { @@ -70,78 +71,9 @@ func (c *ComposeService) AddResource( }, nil } -func createResourceProps(resourceType string, config []byte) (any, error) { - switch project.ResourceType(resourceType) { - case project.ResourceTypeHostContainerApp: - props := project.ContainerAppProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeDbCosmos: - props := project.CosmosDBProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeStorage: - props := project.StorageProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeAiProject: - props := project.AiFoundryModelProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeDbMongo: - props := project.CosmosDBProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeMessagingEventHubs: - props := project.EventHubsProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - case project.ResourceTypeMessagingServiceBus: - props := project.ServiceBusProps{} - if len(config) == 0 { - return props, nil - } - if err := json.Unmarshal(config, &props); err != nil { - return nil, err - } - return props, nil - default: - return nil, nil - } -} - -// GetResource implements azdext.ComposeServiceServer. -func (c *ComposeService) GetResource( +// GetResource retrieves a resource by its name from the project configuration. +// If the resource does not exist, it returns a NotFound error. +func (c *composeService) GetResource( ctx context.Context, req *azdext.GetResourceRequest, ) (*azdext.GetResourceResponse, error) { @@ -177,24 +109,24 @@ func (c *ComposeService) GetResource( }, nil } -// GetResourceType implements azdext.ComposeServiceServer. -func (c *ComposeService) GetResourceType( +// GetResourceType gets the resource type configuration schema by the specified name. +func (c *composeService) GetResourceType( context.Context, *azdext.GetResourceTypeRequest, ) (*azdext.GetResourceTypeResponse, error) { panic("unimplemented") } -// ListResourceTypes implements azdext.ComposeServiceServer. -func (c *ComposeService) ListResourceTypes( +// ListResourceTypes lists all available resource types. +func (c *composeService) ListResourceTypes( context.Context, *azdext.EmptyRequest, ) (*azdext.ListResourceTypesResponse, error) { panic("unimplemented") } -// ListResources implements azdext.ComposeServiceServer. -func (c *ComposeService) ListResources( +// ListResources lists all resources in the project configuration. +func (c *composeService) ListResources( ctx context.Context, req *azdext.EmptyRequest, ) (*azdext.ListResourcesResponse, error) { @@ -229,3 +161,74 @@ func (c *ComposeService) ListResources( Resources: composedResources, }, nil } + +// createResourceProps unmarshals the resource configuration bytes into the appropriate struct based on the resource type. +func createResourceProps(resourceType string, config []byte) (any, error) { + switch project.ResourceType(resourceType) { + case project.ResourceTypeHostContainerApp: + props := project.ContainerAppProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeDbCosmos: + props := project.CosmosDBProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeStorage: + props := project.StorageProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeAiProject: + props := project.AiFoundryModelProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeDbMongo: + props := project.CosmosDBProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeMessagingEventHubs: + props := project.EventHubsProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + case project.ResourceTypeMessagingServiceBus: + props := project.ServiceBusProps{} + if len(config) == 0 { + return props, nil + } + if err := json.Unmarshal(config, &props); err != nil { + return nil, err + } + return props, nil + default: + return nil, nil + } +} diff --git a/cli/azd/internal/grpcserver/workflow_service.go b/cli/azd/internal/grpcserver/workflow_service.go index 496c3a022d0..1092fa44b86 100644 --- a/cli/azd/internal/grpcserver/workflow_service.go +++ b/cli/azd/internal/grpcserver/workflow_service.go @@ -12,19 +12,22 @@ import ( "google.golang.org/grpc/status" ) -type WorkflowService struct { +// workflowService exposes features of AZD workflows to the Extensions Framework layer. +type workflowService struct { azdext.UnimplementedWorkflowServiceServer runner *workflow.Runner } +// NewWorkflowService creates a new instance of the workflow service. func NewWorkflowService(runner *workflow.Runner) azdext.WorkflowServiceServer { - return &WorkflowService{ + return &workflowService{ runner: runner, } } -func (s *WorkflowService) Run(ctx context.Context, request *azdext.RunWorkflowRequest) (*azdext.EmptyResponse, error) { +// Run executes the specified workflow. +func (s *workflowService) Run(ctx context.Context, request *azdext.RunWorkflowRequest) (*azdext.EmptyResponse, error) { wf := request.Workflow if wf == nil || len(wf.Steps) == 0 { return nil, status.Errorf(codes.InvalidArgument, "workflow is empty") @@ -42,6 +45,7 @@ func (s *WorkflowService) Run(ctx context.Context, request *azdext.RunWorkflowRe return &azdext.EmptyResponse{}, nil } +// convertWorkflow converts an azdext.Workflow to a workflow.Workflow. func convertWorkflow(wf *azdext.Workflow) (*workflow.Workflow, error) { azdWorkflow := workflow.Workflow{ Name: wf.Name, From 09aec54160eb3c48b1b52e5d07ec25d31f23ce35 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 14:03:38 -0700 Subject: [PATCH 6/8] Updates extension framework docs --- cli/azd/docs/extension-framework.md | 91 ++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/cli/azd/docs/extension-framework.md b/cli/azd/docs/extension-framework.md index 9d527e93a3e..9fed6246d6d 100644 --- a/cli/azd/docs/extension-framework.md +++ b/cli/azd/docs/extension-framework.md @@ -11,6 +11,15 @@ Table of Contents - [Managing Extensions](#managing-extensions) - [Developing Extensions](#developing-extensions) - [Developer Artifacts](#developer-artifacts) +- [gRPC Services](#grpc-services) + - [Project Service](#project-service) + - [Environment Service](#environment-service) + - [User Config Service](#user-config-service) + - [Deployment Service](#deployment-service) + - [Prompt Service](#prompt-service) + - [Event Service](#event-service) + - [Compose Service](#compose-service) + - [Workflow Service](#workflow-service) ## Capabilities @@ -326,6 +335,8 @@ The following are a list of available gRPC services for extension developer to i - [Deployment Service](#deployment-service) - [Prompt Service](#prompt-service) - [Event Service](#event-service) +- [Compose Service](#compose-service) +- [Workflow Service](#workflow-service) ### Project Service @@ -345,9 +356,13 @@ Gets the current project configuration. - `services`: map of *ServiceConfig* - `infra`: *InfraOptions* -*See [project.proto](../grpc/proto/project.proto).* +#### AddService +Adds a new service to the project. ------------------------------------------- +- **Request:** *AddServiceRequest* + - Contains: + - `service`: *ServiceConfig* +- **Response:** *EmptyResponse* ### Environment Service @@ -467,8 +482,6 @@ Removes a config value at a given path. - `path` (string) - **Response:** *EmptyResponse* ------------------------------------------- - ### User Config Service This service manages user-specific configuration retrieval and updates. @@ -527,8 +540,6 @@ Removes a user configuration value. - Contains: - `status` (string) ------------------------------------------- - ### Deployment Service This service provides operations for deployment retrieval and context management. @@ -565,8 +576,6 @@ Retrieves the current deployment context. - `resourceGroup` (string) - `resources` (repeated string) ------------------------------------------- - ### Prompt Service This service manages user prompt interactions for subscriptions, locations, resources, and confirmations. @@ -738,3 +747,69 @@ Clients can subscribe to events and receive notifications via a bidirectional st - `service_name`: The name of the service. - `status`: Status such as "running", "completed", or "failed". - `message`: Optional additional details. + +### Compose Service + +This service manages composability resources in an AZD project. + +#### ListResources + +Lists all configured composability resources. + +- **Request:** *EmptyRequest* +- **Response:** *ListResourcesResponse* + - Contains a list of **ComposedResource** + +#### GetResource + +Retrieves the configuration of a specific composability resource. + +- **Request:** *GetResourceRequest* + - Contains: + - `name` (string) +- **Response:** *GetResourceResponse* + - Contains: + - `resource`: *ComposedResource* + +#### ListResourceTypes + +Lists all supported composability resource types. + +- **Request:** *EmptyRequest* +- **Response:** *ListResourceTypesResponse* + - Contains a list of **ComposedResourceType** + +#### GetResourceType + +Retrieves the schema of a specific composability resource type. + +- **Request:** *GetResourceTypeRequest* + - Contains: + - `type_name` (string) +- **Response:** *GetResourceTypeResponse* + - Contains: + - `resource_type`: *ComposedResourceType* + +#### AddResource + +Adds a new composability resource to the project. + +- **Request:** *AddResourceRequest* + - Contains: + - `resource`: *ComposedResource* +- **Response:** *AddResourceResponse* + - Contains: + - `resource`: *ComposedResource* + +### Workflow Service + +This service executes workflows defined within the project. + +#### Run + +Executes a workflow consisting of sequential steps. + +- **Request:** *RunWorkflowRequest* + - Contains: + - `workflow`: *Workflow* (with `name` and `steps`) +- **Response:** *EmptyResponse* From f3ec9789b3a08ab33e31d0d6a04fa700d005e3cc Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 25 Mar 2025 17:22:51 -0700 Subject: [PATCH 7/8] Fixes copyright lint check --- cli/azd/internal/grpcserver/compose_service_test.go | 3 +++ cli/azd/internal/grpcserver/workflow_service_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli/azd/internal/grpcserver/compose_service_test.go b/cli/azd/internal/grpcserver/compose_service_test.go index 1db321e1776..b7c3aac7fca 100644 --- a/cli/azd/internal/grpcserver/compose_service_test.go +++ b/cli/azd/internal/grpcserver/compose_service_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package grpcserver import ( diff --git a/cli/azd/internal/grpcserver/workflow_service_test.go b/cli/azd/internal/grpcserver/workflow_service_test.go index 67111ae5ef8..8fcaaab69a5 100644 --- a/cli/azd/internal/grpcserver/workflow_service_test.go +++ b/cli/azd/internal/grpcserver/workflow_service_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package grpcserver import ( From 39d2ac5a5c815091c683786db06c6ed0439f568c Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 27 Mar 2025 16:07:02 -0700 Subject: [PATCH 8/8] Adds not about marshalling of resource config props --- cli/azd/internal/grpcserver/compose_service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/internal/grpcserver/compose_service.go b/cli/azd/internal/grpcserver/compose_service.go index 9985b2a87f3..1e3a18dc57b 100644 --- a/cli/azd/internal/grpcserver/compose_service.go +++ b/cli/azd/internal/grpcserver/compose_service.go @@ -163,6 +163,8 @@ func (c *composeService) ListResources( } // createResourceProps unmarshals the resource configuration bytes into the appropriate struct based on the resource type. +// For the short term this marshalling of resource properties needs to stay in sync with `pkg\project\resources.go` +// In the future we will converge this into a common component. func createResourceProps(resourceType string, config []byte) (any, error) { switch project.ResourceType(resourceType) { case project.ResourceTypeHostContainerApp: