diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/proto/environment.proto b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/proto/environment.proto index 48072a13a1e..0b1a3a27a9e 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/proto/environment.proto +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/languages/proto/environment.proto @@ -57,13 +57,13 @@ message SelectEnvironmentRequest { // Request to retrieve a specific key-value pair. message GetEnvRequest { - string env_name = 1; // Name of the environment. + string env_name = 1; // Optional: Name of the environment. If empty, uses default. string key = 2; // Key to retrieve. } // Request to set a key-value pair. message SetEnvRequest { - string env_name = 1; // Name of the environment. + string env_name = 1; // Optional: Name of the environment. If empty, uses default. string key = 2; // Key to set. string value = 3; // Value to set for the key. } @@ -109,6 +109,7 @@ message KeyValue { // Request message for Get message GetConfigRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for Get @@ -120,6 +121,7 @@ message GetConfigResponse { // Request message for GetString message GetConfigStringRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for GetString @@ -131,6 +133,7 @@ message GetConfigStringResponse { // Request message for GetSection message GetConfigSectionRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for GetSection @@ -143,9 +146,11 @@ message GetConfigSectionResponse { message SetConfigRequest { string path = 1; bytes value = 2; + string env_name = 3; // Optional: Name of the environment. If empty, uses default. } // Request message for Unset message UnsetConfigRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } diff --git a/cli/azd/grpc/proto/environment.proto b/cli/azd/grpc/proto/environment.proto index 4dbd2529c85..969b4b9f49f 100644 --- a/cli/azd/grpc/proto/environment.proto +++ b/cli/azd/grpc/proto/environment.proto @@ -58,13 +58,13 @@ message SelectEnvironmentRequest { // Request to retrieve a specific key-value pair. message GetEnvRequest { - string env_name = 1; // Name of the environment. + string env_name = 1; // Optional: Name of the environment. If empty, uses default. string key = 2; // Key to retrieve. } // Request to set a key-value pair. message SetEnvRequest { - string env_name = 1; // Name of the environment. + string env_name = 1; // Optional: Name of the environment. If empty, uses default. string key = 2; // Key to set. string value = 3; // Value to set for the key. } @@ -110,6 +110,7 @@ message KeyValue { // Request message for Get message GetConfigRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for Get @@ -121,6 +122,7 @@ message GetConfigResponse { // Request message for GetString message GetConfigStringRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for GetString @@ -132,6 +134,7 @@ message GetConfigStringResponse { // Request message for GetSection message GetConfigSectionRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } // Response message for GetSection @@ -144,10 +147,12 @@ message GetConfigSectionResponse { message SetConfigRequest { string path = 1; bytes value = 2; + string env_name = 3; // Optional: Name of the environment. If empty, uses default. } // Request message for Unset message UnsetConfigRequest { string path = 1; + string env_name = 2; // Optional: Name of the environment. If empty, uses default. } diff --git a/cli/azd/internal/grpcserver/environment_service.go b/cli/azd/internal/grpcserver/environment_service.go index ad0b684d00f..2b52c9eb4ad 100644 --- a/cli/azd/internal/grpcserver/environment_service.go +++ b/cli/azd/internal/grpcserver/environment_service.go @@ -128,12 +128,7 @@ func (s *environmentService) GetValues( ctx context.Context, req *azdext.GetEnvironmentRequest, ) (*azdext.KeyValueListResponse, error) { - envManager, err := s.lazyEnvManager.GetValue() - if err != nil { - return nil, err - } - - env, err := envManager.Get(ctx, req.Name) + env, err := s.resolveEnvironment(ctx, req.Name) if err != nil { return nil, err } @@ -157,12 +152,7 @@ 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) { - envManager, err := s.lazyEnvManager.GetValue() - if err != nil { - return nil, err - } - - env, err := envManager.Get(ctx, req.EnvName) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -182,7 +172,7 @@ func (s *environmentService) SetValue(ctx context.Context, req *azdext.SetEnvReq return nil, err } - env, err := envManager.Get(ctx, req.EnvName) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -223,12 +213,26 @@ func (s *environmentService) currentEnvironment(ctx context.Context) (*environme return env, nil } +// resolveEnvironment resolves the environment by name if provided, otherwise falls back to the default environment. +func (s *environmentService) resolveEnvironment(ctx context.Context, envName string) (*environment.Environment, error) { + if envName == "" { + return s.currentEnvironment(ctx) + } + + envManager, err := s.lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + return envManager.Get(ctx, envName) +} + // GetConfig retrieves a config value by path. func (s *environmentService) GetConfig( ctx context.Context, req *azdext.GetConfigRequest, ) (*azdext.GetConfigResponse, error) { - env, err := s.currentEnvironment(ctx) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -256,7 +260,7 @@ func (s *environmentService) GetConfigString( ctx context.Context, req *azdext.GetConfigStringRequest, ) (*azdext.GetConfigStringResponse, error) { - env, err := s.currentEnvironment(ctx) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -274,7 +278,7 @@ func (s *environmentService) GetConfigSection( ctx context.Context, req *azdext.GetConfigSectionRequest, ) (*azdext.GetConfigSectionResponse, error) { - env, err := s.currentEnvironment(ctx) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -309,7 +313,7 @@ func (s *environmentService) SetConfig(ctx context.Context, req *azdext.SetConfi return nil, err } - env, err := s.currentEnvironment(ctx) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } @@ -340,7 +344,7 @@ func (s *environmentService) UnsetConfig( return nil, err } - env, err := s.currentEnvironment(ctx) + env, err := s.resolveEnvironment(ctx, req.EnvName) if err != nil { return nil, err } diff --git a/cli/azd/internal/grpcserver/environment_service_test.go b/cli/azd/internal/grpcserver/environment_service_test.go index d9e6263fab2..b63875c99df 100644 --- a/cli/azd/internal/grpcserver/environment_service_test.go +++ b/cli/azd/internal/grpcserver/environment_service_test.go @@ -148,3 +148,187 @@ func Test_EnvironmentService_Flow(t *testing.T) { require.NoError(t, err) require.Equal(t, testEnv2.Name(), getCurrentResponse.Environment.Name) } + +// Test_EnvironmentService_ResolveEnvironment validates that methods use the default environment +// when env_name is empty and the specified environment when env_name is provided. +func Test_EnvironmentService_ResolveEnvironment(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + projectConfig := project.ProjectConfig{Name: "test"} + err := project.Save(*mockContext.Context, &projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + 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) + + // Create two environments with different dotenv and config values. + env1, err := envManager.Create(*mockContext.Context, environment.Spec{Name: "env1"}) + require.NoError(t, err) + env1.DotenvSet("key1", "value1") + require.NoError(t, envManager.Save(*mockContext.Context, env1)) + + env2, err := envManager.Create(*mockContext.Context, environment.Spec{Name: "env2"}) + require.NoError(t, err) + env2.DotenvSet("key1", "value2") + require.NoError(t, envManager.Save(*mockContext.Context, env2)) + + // Set env1 as default. + require.NoError(t, azdContext.SetProjectState(azdcontext.ProjectState{DefaultEnvironment: "env1"})) + + service := NewEnvironmentService(lazy.From(azdContext), lazy.From(envManager)) + ctx := *mockContext.Context + + t.Run("GetValue", func(t *testing.T) { + tests := []struct { + name string + envName string + expected string + }{ + {"empty_env_name_uses_default", "", "value1"}, + {"explicit_env_name_targets_specified", "env2", "value2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := service.GetValue(ctx, &azdext.GetEnvRequest{EnvName: tt.envName, Key: "key1"}) + require.NoError(t, err) + require.Equal(t, tt.expected, resp.Value) + }) + } + }) + + t.Run("GetValues", func(t *testing.T) { + tests := []struct { + name string + envName string + expected string + }{ + {"empty_name_uses_default", "", "value1"}, + {"explicit_name_targets_specified", "env2", "value2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := service.GetValues(ctx, &azdext.GetEnvironmentRequest{Name: tt.envName}) + require.NoError(t, err) + envValues := map[string]string{} + for _, kv := range resp.KeyValues { + envValues[kv.Key] = kv.Value + } + require.Equal(t, tt.expected, envValues["key1"]) + }) + } + }) + + t.Run("SetValue", func(t *testing.T) { + _, err := service.SetValue(ctx, &azdext.SetEnvRequest{Key: "newkey", Value: "newval"}) + require.NoError(t, err) + + resp, err := service.GetValue(ctx, &azdext.GetEnvRequest{EnvName: "env1", Key: "newkey"}) + require.NoError(t, err) + require.Equal(t, "newval", resp.Value) + }) + + // Config subtests share state: SetConfig writes values that subsequent reads and unset verify. + t.Run("Config", func(t *testing.T) { + // Setup: write config to both environments. + _, err := service.SetConfig(ctx, &azdext.SetConfigRequest{ + Path: "test.key", + Value: []byte(`"configval1"`), + }) + require.NoError(t, err) + + _, err = service.SetConfig(ctx, &azdext.SetConfigRequest{ + Path: "test.key", + Value: []byte(`"configval2"`), + EnvName: "env2", + }) + require.NoError(t, err) + + t.Run("GetConfigString", func(t *testing.T) { + tests := []struct { + name string + envName string + expected string + }{ + {"empty_env_name_reads_default", "", "configval1"}, + {"explicit_env_name_reads_specified", "env2", "configval2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := service.GetConfigString(ctx, &azdext.GetConfigStringRequest{ + Path: "test.key", + EnvName: tt.envName, + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, tt.expected, resp.Value) + }) + } + }) + + t.Run("GetConfig", func(t *testing.T) { + tests := []struct { + name string + envName string + }{ + {"empty_env_name_reads_default", ""}, + {"explicit_env_name_reads_specified", "env2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := service.GetConfig(ctx, &azdext.GetConfigRequest{ + Path: "test.key", + EnvName: tt.envName, + }) + require.NoError(t, err) + require.True(t, resp.Found) + }) + } + }) + + t.Run("GetConfigSection", func(t *testing.T) { + tests := []struct { + name string + envName string + }{ + {"empty_env_name_reads_default", ""}, + {"explicit_env_name_reads_specified", "env2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := service.GetConfigSection(ctx, &azdext.GetConfigSectionRequest{ + Path: "test", + EnvName: tt.envName, + }) + require.NoError(t, err) + require.True(t, resp.Found) + }) + } + }) + + t.Run("UnsetConfig", func(t *testing.T) { + _, err := service.UnsetConfig(ctx, &azdext.UnsetConfigRequest{ + Path: "test.key", + EnvName: "env2", + }) + require.NoError(t, err) + + // Verify config was removed from env2. + resp, err := service.GetConfigString(ctx, &azdext.GetConfigStringRequest{ + Path: "test.key", + EnvName: "env2", + }) + require.NoError(t, err) + require.False(t, resp.Found) + + // Verify config still exists in env1 (default). + resp, err = service.GetConfigString(ctx, &azdext.GetConfigStringRequest{Path: "test.key"}) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, "configval1", resp.Value) + }) + }) +} diff --git a/cli/azd/pkg/azdext/environment.pb.go b/cli/azd/pkg/azdext/environment.pb.go index 5b445c1b946..c9b120270d3 100644 --- a/cli/azd/pkg/azdext/environment.pb.go +++ b/cli/azd/pkg/azdext/environment.pb.go @@ -116,7 +116,7 @@ func (x *SelectEnvironmentRequest) GetName() string { // Request to retrieve a specific key-value pair. type GetEnvRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - EnvName string `protobuf:"bytes,1,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Name of the environment. + EnvName string `protobuf:"bytes,1,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Key to retrieve. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -169,7 +169,7 @@ func (x *GetEnvRequest) GetKey() string { // Request to set a key-value pair. type SetEnvRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - EnvName string `protobuf:"bytes,1,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Name of the environment. + EnvName string `protobuf:"bytes,1,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Key to set. Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // Value to set for the key. unknownFields protoimpl.UnknownFields @@ -584,6 +584,7 @@ func (x *KeyValue) GetValue() string { type GetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -625,6 +626,13 @@ func (x *GetConfigRequest) GetPath() string { return "" } +func (x *GetConfigRequest) GetEnvName() string { + if x != nil { + return x.EnvName + } + return "" +} + // Response message for Get type GetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -682,6 +690,7 @@ func (x *GetConfigResponse) GetFound() bool { type GetConfigStringRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -723,6 +732,13 @@ func (x *GetConfigStringRequest) GetPath() string { return "" } +func (x *GetConfigStringRequest) GetEnvName() string { + if x != nil { + return x.EnvName + } + return "" +} + // Response message for GetString type GetConfigStringResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -780,6 +796,7 @@ func (x *GetConfigStringResponse) GetFound() bool { type GetConfigSectionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -821,6 +838,13 @@ func (x *GetConfigSectionRequest) GetPath() string { return "" } +func (x *GetConfigSectionRequest) GetEnvName() string { + if x != nil { + return x.EnvName + } + return "" +} + // Response message for GetSection type GetConfigSectionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -879,6 +903,7 @@ type SetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + EnvName string `protobuf:"bytes,3,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -927,10 +952,18 @@ func (x *SetConfigRequest) GetValue() []byte { return nil } +func (x *SetConfigRequest) GetEnvName() string { + if x != nil { + return x.EnvName + } + return "" +} + // Request message for Unset type UnsetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + EnvName string `protobuf:"bytes,2,opt,name=env_name,json=envName,proto3" json:"env_name,omitempty"` // Optional: Name of the environment. If empty, uses default. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -972,6 +1005,13 @@ func (x *UnsetConfigRequest) GetPath() string { return "" } +func (x *UnsetConfigRequest) GetEnvName() string { + if x != nil { + return x.EnvName + } + return "" +} + var File_environment_proto protoreflect.FileDescriptor const file_environment_proto_rawDesc = "" + @@ -1007,27 +1047,32 @@ const file_environment_proto_rawDesc = "" + "\adefault\x18\x04 \x01(\bR\adefault\"2\n" + "\bKeyValue\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value\"&\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"A\n" + "\x10GetConfigRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\"?\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x19\n" + + "\benv_name\x18\x02 \x01(\tR\aenvName\"?\n" + "\x11GetConfigResponse\x12\x14\n" + "\x05value\x18\x01 \x01(\fR\x05value\x12\x14\n" + - "\x05found\x18\x02 \x01(\bR\x05found\",\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"G\n" + "\x16GetConfigStringRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\"E\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x19\n" + + "\benv_name\x18\x02 \x01(\tR\aenvName\"E\n" + "\x17GetConfigStringResponse\x12\x14\n" + "\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" + - "\x05found\x18\x02 \x01(\bR\x05found\"-\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"H\n" + "\x17GetConfigSectionRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path\"J\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x19\n" + + "\benv_name\x18\x02 \x01(\tR\aenvName\"J\n" + "\x18GetConfigSectionResponse\x12\x18\n" + "\asection\x18\x01 \x01(\fR\asection\x12\x14\n" + - "\x05found\x18\x02 \x01(\bR\x05found\"<\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"W\n" + "\x10SetConfigRequest\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"(\n" + + "\x05value\x18\x02 \x01(\fR\x05value\x12\x19\n" + + "\benv_name\x18\x03 \x01(\tR\aenvName\"C\n" + "\x12UnsetConfigRequest\x12\x12\n" + - "\x04path\x18\x01 \x01(\tR\x04path2\xc8\x06\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x19\n" + + "\benv_name\x18\x02 \x01(\tR\aenvName2\xc8\x06\n" + "\x12EnvironmentService\x12?\n" + "\n" + "GetCurrent\x12\x14.azdext.EmptyRequest\x1a\x1b.azdext.EnvironmentResponse\x12=\n" +