From c2b452b678b3cfe21567c8dd6db23367bf158990 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 13:20:14 -0800 Subject: [PATCH 1/6] Adds support for additional/unknown properties on project/service config --- cli/azd/grpc/proto/models.proto | 2 + cli/azd/pkg/azdext/models.pb.go | 108 ++++--- cli/azd/pkg/project/mapper_registry.go | 55 +++- cli/azd/pkg/project/mapper_registry_test.go | 62 +++- cli/azd/pkg/project/project_config.go | 3 + cli/azd/pkg/project/project_test.go | 285 ++++++++++++++++++ cli/azd/pkg/project/service_config.go | 3 + ...ertiesMarshalling-combined-extensions.snap | 16 + ...sMarshalling-project-level-extensions.snap | 22 ++ ...sMarshalling-service-level-extensions.snap | 29 ++ 10 files changed, 525 insertions(+), 60 deletions(-) create mode 100644 cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-combined-extensions.snap create mode 100644 cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-project-level-extensions.snap create mode 100644 cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-service-level-extensions.snap diff --git a/cli/azd/grpc/proto/models.proto b/cli/azd/grpc/proto/models.proto index 1598c7ba3d5..f3c292ad983 100644 --- a/cli/azd/grpc/proto/models.proto +++ b/cli/azd/grpc/proto/models.proto @@ -70,6 +70,7 @@ message ProjectConfig { ProjectMetadata metadata = 4; map services = 5; InfraOptions infra = 6; + google.protobuf.Struct additional_properties = 7; } // RequiredVersions message definition @@ -95,6 +96,7 @@ message ServiceConfig { string image = 9; DockerProjectOptions docker = 10; google.protobuf.Struct config = 11; + google.protobuf.Struct additional_properties = 12; } // InfraOptions message definition diff --git a/cli/azd/pkg/azdext/models.pb.go b/cli/azd/pkg/azdext/models.pb.go index 0421097ec66..731e5fd2227 100644 --- a/cli/azd/pkg/azdext/models.pb.go +++ b/cli/azd/pkg/azdext/models.pb.go @@ -677,15 +677,16 @@ func (x *ResourceExtended) GetKind() string { // ProjectConfig message definition type ProjectConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - ResourceGroupName string `protobuf:"bytes,2,opt,name=resource_group_name,json=resourceGroupName,proto3" json:"resource_group_name,omitempty"` - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - Metadata *ProjectMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` - Services map[string]*ServiceConfig `protobuf:"bytes,5,rep,name=services,proto3" json:"services,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Infra *InfraOptions `protobuf:"bytes,6,opt,name=infra,proto3" json:"infra,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ResourceGroupName string `protobuf:"bytes,2,opt,name=resource_group_name,json=resourceGroupName,proto3" json:"resource_group_name,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Metadata *ProjectMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + Services map[string]*ServiceConfig `protobuf:"bytes,5,rep,name=services,proto3" json:"services,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Infra *InfraOptions `protobuf:"bytes,6,opt,name=infra,proto3" json:"infra,omitempty"` + AdditionalProperties *structpb.Struct `protobuf:"bytes,7,opt,name=additional_properties,json=additionalProperties,proto3" json:"additional_properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProjectConfig) Reset() { @@ -760,6 +761,13 @@ func (x *ProjectConfig) GetInfra() *InfraOptions { return nil } +func (x *ProjectConfig) GetAdditionalProperties() *structpb.Struct { + if x != nil { + return x.AdditionalProperties + } + return nil +} + // RequiredVersions message definition type RequiredVersions struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -852,20 +860,21 @@ func (x *ProjectMetadata) GetTemplate() string { // ServiceConfig message definition type ServiceConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - ResourceGroupName string `protobuf:"bytes,2,opt,name=resource_group_name,json=resourceGroupName,proto3" json:"resource_group_name,omitempty"` - ResourceName string `protobuf:"bytes,3,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` - ApiVersion string `protobuf:"bytes,4,opt,name=api_version,json=apiVersion,proto3" json:"api_version,omitempty"` - RelativePath string `protobuf:"bytes,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` - Host string `protobuf:"bytes,6,opt,name=host,proto3" json:"host,omitempty"` - Language string `protobuf:"bytes,7,opt,name=language,proto3" json:"language,omitempty"` - OutputPath string `protobuf:"bytes,8,opt,name=output_path,json=outputPath,proto3" json:"output_path,omitempty"` - Image string `protobuf:"bytes,9,opt,name=image,proto3" json:"image,omitempty"` - Docker *DockerProjectOptions `protobuf:"bytes,10,opt,name=docker,proto3" json:"docker,omitempty"` - Config *structpb.Struct `protobuf:"bytes,11,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ResourceGroupName string `protobuf:"bytes,2,opt,name=resource_group_name,json=resourceGroupName,proto3" json:"resource_group_name,omitempty"` + ResourceName string `protobuf:"bytes,3,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` + ApiVersion string `protobuf:"bytes,4,opt,name=api_version,json=apiVersion,proto3" json:"api_version,omitempty"` + RelativePath string `protobuf:"bytes,5,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` + Host string `protobuf:"bytes,6,opt,name=host,proto3" json:"host,omitempty"` + Language string `protobuf:"bytes,7,opt,name=language,proto3" json:"language,omitempty"` + OutputPath string `protobuf:"bytes,8,opt,name=output_path,json=outputPath,proto3" json:"output_path,omitempty"` + Image string `protobuf:"bytes,9,opt,name=image,proto3" json:"image,omitempty"` + Docker *DockerProjectOptions `protobuf:"bytes,10,opt,name=docker,proto3" json:"docker,omitempty"` + Config *structpb.Struct `protobuf:"bytes,11,opt,name=config,proto3" json:"config,omitempty"` + AdditionalProperties *structpb.Struct `protobuf:"bytes,12,opt,name=additional_properties,json=additionalProperties,proto3" json:"additional_properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ServiceConfig) Reset() { @@ -975,6 +984,13 @@ func (x *ServiceConfig) GetConfig() *structpb.Struct { return nil } +func (x *ServiceConfig) GetAdditionalProperties() *structpb.Struct { + if x != nil { + return x.AdditionalProperties + } + return nil +} + // InfraOptions message definition type InfraOptions struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1377,21 +1393,22 @@ const file_models_proto_rawDesc = "" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + "\x04type\x18\x03 \x01(\tR\x04type\x12\x1a\n" + "\blocation\x18\x04 \x01(\tR\blocation\x12\x12\n" + - "\x04kind\x18\x05 \x01(\tR\x04kind\"\xdd\x02\n" + + "\x04kind\x18\x05 \x01(\tR\x04kind\"\xab\x03\n" + "\rProjectConfig\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12.\n" + "\x13resource_group_name\x18\x02 \x01(\tR\x11resourceGroupName\x12\x12\n" + "\x04path\x18\x03 \x01(\tR\x04path\x123\n" + "\bmetadata\x18\x04 \x01(\v2\x17.azdext.ProjectMetadataR\bmetadata\x12?\n" + "\bservices\x18\x05 \x03(\v2#.azdext.ProjectConfig.ServicesEntryR\bservices\x12*\n" + - "\x05infra\x18\x06 \x01(\v2\x14.azdext.InfraOptionsR\x05infra\x1aR\n" + + "\x05infra\x18\x06 \x01(\v2\x14.azdext.InfraOptionsR\x05infra\x12L\n" + + "\x15additional_properties\x18\a \x01(\v2\x17.google.protobuf.StructR\x14additionalProperties\x1aR\n" + "\rServicesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + "\x05value\x18\x02 \x01(\v2\x15.azdext.ServiceConfigR\x05value:\x028\x01\"$\n" + "\x10RequiredVersions\x12\x10\n" + "\x03azd\x18\x01 \x01(\tR\x03azd\"-\n" + "\x0fProjectMetadata\x12\x1a\n" + - "\btemplate\x18\x01 \x01(\tR\btemplate\"\x8c\x03\n" + + "\btemplate\x18\x01 \x01(\tR\btemplate\"\xda\x03\n" + "\rServiceConfig\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12.\n" + "\x13resource_group_name\x18\x02 \x01(\tR\x11resourceGroupName\x12#\n" + @@ -1406,7 +1423,8 @@ const file_models_proto_rawDesc = "" + "\x05image\x18\t \x01(\tR\x05image\x124\n" + "\x06docker\x18\n" + " \x01(\v2\x1c.azdext.DockerProjectOptionsR\x06docker\x12/\n" + - "\x06config\x18\v \x01(\v2\x17.google.protobuf.StructR\x06config\"V\n" + + "\x06config\x18\v \x01(\v2\x17.google.protobuf.StructR\x06config\x12L\n" + + "\x15additional_properties\x18\f \x01(\v2\x17.google.protobuf.StructR\x14additionalProperties\"V\n" + "\fInfraOptions\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x16\n" + @@ -1496,23 +1514,25 @@ var file_models_proto_depIdxs = []int32{ 13, // 1: azdext.ProjectConfig.metadata:type_name -> azdext.ProjectMetadata 20, // 2: azdext.ProjectConfig.services:type_name -> azdext.ProjectConfig.ServicesEntry 15, // 3: azdext.ProjectConfig.infra:type_name -> azdext.InfraOptions - 16, // 4: azdext.ServiceConfig.docker:type_name -> azdext.DockerProjectOptions - 22, // 5: azdext.ServiceConfig.config:type_name -> google.protobuf.Struct - 19, // 6: azdext.ServiceContext.restore:type_name -> azdext.Artifact - 19, // 7: azdext.ServiceContext.build:type_name -> azdext.Artifact - 19, // 8: azdext.ServiceContext.package:type_name -> azdext.Artifact - 19, // 9: azdext.ServiceContext.publish:type_name -> azdext.Artifact - 19, // 10: azdext.ServiceContext.deploy:type_name -> azdext.Artifact - 19, // 11: azdext.ArtifactList.artifacts:type_name -> azdext.Artifact - 0, // 12: azdext.Artifact.kind:type_name -> azdext.ArtifactKind - 1, // 13: azdext.Artifact.location_kind:type_name -> azdext.LocationKind - 21, // 14: azdext.Artifact.metadata:type_name -> azdext.Artifact.MetadataEntry - 14, // 15: azdext.ProjectConfig.ServicesEntry.value:type_name -> azdext.ServiceConfig - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 22, // 4: azdext.ProjectConfig.additional_properties:type_name -> google.protobuf.Struct + 16, // 5: azdext.ServiceConfig.docker:type_name -> azdext.DockerProjectOptions + 22, // 6: azdext.ServiceConfig.config:type_name -> google.protobuf.Struct + 22, // 7: azdext.ServiceConfig.additional_properties:type_name -> google.protobuf.Struct + 19, // 8: azdext.ServiceContext.restore:type_name -> azdext.Artifact + 19, // 9: azdext.ServiceContext.build:type_name -> azdext.Artifact + 19, // 10: azdext.ServiceContext.package:type_name -> azdext.Artifact + 19, // 11: azdext.ServiceContext.publish:type_name -> azdext.Artifact + 19, // 12: azdext.ServiceContext.deploy:type_name -> azdext.Artifact + 19, // 13: azdext.ArtifactList.artifacts:type_name -> azdext.Artifact + 0, // 14: azdext.Artifact.kind:type_name -> azdext.ArtifactKind + 1, // 15: azdext.Artifact.location_kind:type_name -> azdext.LocationKind + 21, // 16: azdext.Artifact.metadata:type_name -> azdext.Artifact.MetadataEntry + 14, // 17: azdext.ProjectConfig.ServicesEntry.value:type_name -> azdext.ServiceConfig + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_models_proto_init() } diff --git a/cli/azd/pkg/project/mapper_registry.go b/cli/azd/pkg/project/mapper_registry.go index c9e5eb9dd1d..c2b60dc094b 100644 --- a/cli/azd/pkg/project/mapper_registry.go +++ b/cli/azd/pkg/project/mapper_registry.go @@ -135,18 +135,29 @@ func registerProjectMappings() { } } + // Convert additional properties if present + var protoAdditionalProperties *structpb.Struct + if src.AdditionalProperties != nil { + var err error + protoAdditionalProperties, err = structpb.NewStruct(src.AdditionalProperties) + if err != nil { + return nil, fmt.Errorf("converting service additional properties to structpb: %w", err) + } + } + return &azdext.ServiceConfig{ - Name: src.Name, - ResourceGroupName: resourceGroupName, - ResourceName: resourceName, - ApiVersion: src.ApiVersion, - RelativePath: src.RelativePath, - Host: string(src.Host), - Language: string(src.Language), - OutputPath: src.OutputPath, - Image: image, - Docker: docker, - Config: protoConfig, + Name: src.Name, + ResourceGroupName: resourceGroupName, + ResourceName: resourceName, + ApiVersion: src.ApiVersion, + RelativePath: src.RelativePath, + Host: string(src.Host), + Language: string(src.Language), + OutputPath: src.OutputPath, + Image: image, + Docker: docker, + Config: protoConfig, + AdditionalProperties: protoAdditionalProperties, }, nil }) @@ -365,6 +376,10 @@ func registerProjectMappings() { result.Config = src.Config.AsMap() } + if src.AdditionalProperties != nil { + result.AdditionalProperties = src.AdditionalProperties.AsMap() + } + return result, nil }) @@ -656,6 +671,16 @@ func registerProjectMappings() { services[i] = serviceConfig } + // Convert additional properties if present + var protoAdditionalProperties *structpb.Struct + if src.AdditionalProperties != nil { + var err error + protoAdditionalProperties, err = structpb.NewStruct(src.AdditionalProperties) + if err != nil { + return nil, fmt.Errorf("converting project additional properties to structpb: %w", err) + } + } + projectConfig := &azdext.ProjectConfig{ Name: src.Name, ResourceGroupName: resourceGroupName, @@ -671,7 +696,8 @@ func registerProjectMappings() { Path: src.Infra.Path, Module: src.Infra.Module, }, - Services: services, + Services: services, + AdditionalProperties: protoAdditionalProperties, } return projectConfig, nil @@ -715,6 +741,11 @@ func registerProjectMappings() { } } + // Convert additional properties if present + if src.AdditionalProperties != nil { + result.AdditionalProperties = src.AdditionalProperties.AsMap() + } + return result, nil }) } diff --git a/cli/azd/pkg/project/mapper_registry_test.go b/cli/azd/pkg/project/mapper_registry_test.go index 60cb2b2a0a2..657471ede4e 100644 --- a/cli/azd/pkg/project/mapper_registry_test.go +++ b/cli/azd/pkg/project/mapper_registry_test.go @@ -51,6 +51,9 @@ func TestServiceConfigMapping(t *testing.T) { Host: ContainerAppTarget, Language: ServiceLanguageDotNet, RelativePath: "./src/api", + AdditionalProperties: map[string]interface{}{ + "customField": "customValue", + }, } var protoConfig *azdext.ServiceConfig @@ -61,6 +64,9 @@ func TestServiceConfigMapping(t *testing.T) { require.Equal(t, string(ContainerAppTarget), protoConfig.Host) require.Equal(t, string(ServiceLanguageDotNet), protoConfig.Language) require.Equal(t, "./src/api", protoConfig.RelativePath) + require.NotNil(t, protoConfig.AdditionalProperties) + additionalPropsMap := protoConfig.AdditionalProperties.AsMap() + require.Equal(t, "customValue", additionalPropsMap["customField"]) } func TestServiceConfigMappingWithResolver(t *testing.T) { @@ -217,6 +223,7 @@ func TestServiceConfigReverseMapping(t *testing.T) { require.Equal(t, ServiceLanguageDotNet, result.Language) require.Equal(t, "./src/api", result.RelativePath) require.Nil(t, result.Config) + require.Nil(t, result.AdditionalProperties) }, }, { @@ -278,6 +285,32 @@ func TestServiceConfigReverseMapping(t *testing.T) { require.Equal(t, "logging", features[1]) }, }, + { + name: "with additional properties in proto", + setupConfig: func() *azdext.ServiceConfig { + additionalPropsData := map[string]any{ + "extensionField": "extensionValue", + "metadata": map[string]any{"version": "1.0.0"}, + } + additionalProps, err := structpb.NewStruct(additionalPropsData) + require.NoError(t, err) + return &azdext.ServiceConfig{ + Name: "test-service", + Host: string(ContainerAppTarget), + Language: string(ServiceLanguageDotNet), + RelativePath: "./src/api", + AdditionalProperties: additionalProps, + } + }, + validateFn: func(t *testing.T, result *ServiceConfig) { + require.Equal(t, "test-service", result.Name) + require.NotNil(t, result.AdditionalProperties) + require.Equal(t, "extensionValue", result.AdditionalProperties["extensionField"]) + metadata, ok := result.AdditionalProperties["metadata"].(map[string]any) + require.True(t, ok) + require.Equal(t, "1.0.0", metadata["version"]) + }, + }, } for _, tt := range tests { @@ -314,9 +347,11 @@ func TestServiceConfigRoundTripMapping(t *testing.T) { Language: ServiceLanguageDotNet, RelativePath: "./src/api", Config: originalConfig, - } - - // Convert to proto + AdditionalProperties: map[string]any{ + "roundTripField": "roundTripValue", + "nestedData": map[string]any{"key": "value"}, + }, + } // Convert to proto var protoConfig *azdext.ServiceConfig err := mapper.Convert(originalServiceConfig, &protoConfig) require.NoError(t, err) @@ -351,6 +386,13 @@ func TestServiceConfigRoundTripMapping(t *testing.T) { require.True(t, ok) require.Equal(t, "inner_value", nested["inner_key"]) require.Equal(t, float64(456), nested["inner_num"]) // Numbers become float64 + + // Verify AdditionalProperties round-trip + require.NotNil(t, roundTripServiceConfig.AdditionalProperties) + require.Equal(t, "roundTripValue", roundTripServiceConfig.AdditionalProperties["roundTripField"]) + nestedData, ok := roundTripServiceConfig.AdditionalProperties["nestedData"].(map[string]any) + require.True(t, ok) + require.Equal(t, "value", nestedData["key"]) } func TestDockerProjectOptionsMapping(t *testing.T) { @@ -943,6 +985,10 @@ func TestProjectConfigMapping(t *testing.T) { RelativePath: "./api", }, }, + AdditionalProperties: map[string]interface{}{ + "projectExtension": "projectValue", + "globalConfig": map[string]interface{}{"enabled": true}, + }, } testResolver := func(key string) string { @@ -969,6 +1015,13 @@ func TestProjectConfigMapping(t *testing.T) { }) t.Run("proto ProjectConfig -> ProjectConfig", func(t *testing.T) { + additionalPropsData := map[string]interface{}{ + "reverseExtension": "reverseValue", + "config": map[string]interface{}{"timeout": 60}, + } + additionalProps, err := structpb.NewStruct(additionalPropsData) + require.NoError(t, err) + protoConfig := &azdext.ProjectConfig{ Name: "reverse-test-project", ResourceGroupName: "reverse-test-rg", @@ -984,10 +1037,11 @@ func TestProjectConfigMapping(t *testing.T) { RelativePath: "./backend", }, }, + AdditionalProperties: additionalProps, } var projectConfig *ProjectConfig - err := mapper.Convert(protoConfig, &projectConfig) + err = mapper.Convert(protoConfig, &projectConfig) require.NoError(t, err) require.NotNil(t, projectConfig) require.Equal(t, "reverse-test-project", projectConfig.Name) diff --git a/cli/azd/pkg/project/project_config.go b/cli/azd/pkg/project/project_config.go index ccb50cd74cf..1925c86e2f7 100644 --- a/cli/azd/pkg/project/project_config.go +++ b/cli/azd/pkg/project/project_config.go @@ -41,6 +41,9 @@ type ProjectConfig struct { Cloud *cloud.Config `yaml:"cloud,omitempty"` Resources map[string]*ResourceConfig `yaml:"resources,omitempty"` + // AdditionalProperties captures any unknown YAML fields for extension support + AdditionalProperties map[string]interface{} `yaml:",inline"` + *ext.EventDispatcher[ProjectLifecycleEventArgs] `yaml:"-"` } diff --git a/cli/azd/pkg/project/project_test.go b/cli/azd/pkg/project/project_test.go index b2f3cc964d0..21f7a7e2741 100644 --- a/cli/azd/pkg/project/project_test.go +++ b/cli/azd/pkg/project/project_test.go @@ -13,6 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/azure" + "github.com/azure/azure-dev/cli/azd/pkg/config" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/ext" "github.com/azure/azure-dev/cli/azd/pkg/infra" @@ -718,3 +719,287 @@ func TestInfraDefaultsNotSavedToYaml(t *testing.T) { assert.Equal(t, provisioning.ProviderKind(""), loadedProject.Infra.Provider) }) } + +func TestAdditionalPropertiesMarshalling(t *testing.T) { + tests := []struct { + name string + project *ProjectConfig + }{ + { + "project-level-extensions", + &ProjectConfig{ + Name: "test-extension-project", + Services: map[string]*ServiceConfig{ + "api": { + Language: ServiceLanguageJavaScript, + Host: ContainerAppTarget, + RelativePath: "./src/api", + }, + }, + AdditionalProperties: map[string]interface{}{ + "customProjectField": "project-level-extension", + "organizationSettings": map[string]interface{}{ + "billing": "department-a", + "compliance": true, + "tags": []interface{}{"production", "critical"}, + }, + "extensionConfig": map[string]interface{}{ + "timeout": 300, + "retries": 3, + "database": map[string]interface{}{ + "host": "localhost", + "port": 5432, + }, + }, + }, + }, + }, + { + "service-level-extensions", + &ProjectConfig{ + Name: "test-service-extension", + Services: map[string]*ServiceConfig{ + "api": { + Language: ServiceLanguageJavaScript, + Host: ContainerAppTarget, + RelativePath: "./src/api", + AdditionalProperties: map[string]interface{}{ + "customServiceField": "service-level-extension", + "monitoring": map[string]interface{}{ + "metrics": true, + "logging": "verbose", + "alerts": []interface{}{"cpu > 80%", "memory > 90%"}, + }, + "extensionSettings": map[string]interface{}{ + "caching": "redis", + "timeout": 30, + "features": map[string]interface{}{ + "featureA": true, + "featureB": false, + }, + }, + }, + }, + "web": { + Language: ServiceLanguageTypeScript, + Host: StaticWebAppTarget, + RelativePath: "./src/web", + AdditionalProperties: map[string]interface{}{ + "deployment": map[string]interface{}{ + "strategy": "blue-green", + "region": "eastus", + }, + }, + }, + }, + }, + }, + { + "combined-extensions", + &ProjectConfig{ + Name: "test-combined-extensions", + Services: map[string]*ServiceConfig{ + "api": { + Language: ServiceLanguageJavaScript, + Host: ContainerAppTarget, + RelativePath: "./src/api", + AdditionalProperties: map[string]interface{}{ + "serviceExtension": "api-specific", + "customConfig": map[string]interface{}{ + "setting1": "value1", + }, + }, + }, + }, + AdditionalProperties: map[string]interface{}{ + "projectExtension": "global-setting", + "sharedConfig": map[string]interface{}{ + "environment": "production", + "version": "1.0.0", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + // First save: write the constructed project to YAML + firstSaveFile := filepath.Join(tempDir, "azure-first.yaml") + err := Save(context.Background(), tt.project, firstSaveFile) + require.NoError(t, err) + + // Load the project back (this initializes all internal fields properly) + loadedProject, err := Load(context.Background(), firstSaveFile) + require.NoError(t, err) + + // Second save: save the loaded project to verify round-trip + secondSaveFile := filepath.Join(tempDir, "azure-second.yaml") + err = Save(context.Background(), loadedProject, secondSaveFile) + require.NoError(t, err) + + // Load the second save and compare with first loaded project + reloadedProject, err := Load(context.Background(), secondSaveFile) + require.NoError(t, err) + + // Verify round-trip preservation with deep equality + assert.Equal(t, loadedProject, reloadedProject) + + // Snapshot the marshalled output to verify structure + savedContents, err := os.ReadFile(firstSaveFile) + require.NoError(t, err) + snapshot.SnapshotT(t, string(savedContents)) + }) + } +} + +// ExtensionConfig represents a type-safe configuration structure that an extension might define +type ExtensionConfig struct { + Timeout int `yaml:"timeout"` + Retries int `yaml:"retries"` + Database DatabaseConfig `yaml:"database"` + Features map[string]interface{} `yaml:"features,omitempty"` +} + +type DatabaseConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` +} + +func TestAdditionalPropertiesExtraction(t *testing.T) { + // Create a project with AdditionalProperties that includes extension configuration + project := &ProjectConfig{ + Name: "test-extension-extraction", + Services: map[string]*ServiceConfig{ + "api": { + Language: ServiceLanguageJavaScript, + Host: ContainerAppTarget, + RelativePath: "./src/api", + AdditionalProperties: map[string]interface{}{ + "customServiceField": "service-extension", + "monitoring": map[string]interface{}{ + "enabled": true, + "level": "verbose", + }, + }, + }, + }, + AdditionalProperties: map[string]interface{}{ + "customProjectField": "project-extension", + "extensionConfig": map[string]interface{}{ + "timeout": 300, + "retries": 3, + "database": map[string]interface{}{ + "host": "localhost", + "port": 5432, + }, + "features": map[string]interface{}{ + "caching": true, + "monitoring": false, + }, + }, + "otherExtension": map[string]interface{}{ + "setting1": "value1", + "setting2": 42, + }, + }, + } + + t.Run("ExtractProjectLevelConfig", func(t *testing.T) { + // Create a config from the AdditionalProperties map + cfg := config.NewConfig(project.AdditionalProperties) + + // Extract the extensionConfig section using GetSection + var extensionConfig ExtensionConfig + found, err := cfg.GetSection("extensionConfig", &extensionConfig) + require.NoError(t, err) + require.True(t, found, "extensionConfig section should be found") + + // Verify the type-safe configuration was extracted correctly + assert.Equal(t, 300, extensionConfig.Timeout) + assert.Equal(t, 3, extensionConfig.Retries) + assert.Equal(t, "localhost", extensionConfig.Database.Host) + assert.Equal(t, 5432, extensionConfig.Database.Port) + assert.Equal(t, true, extensionConfig.Features["caching"]) + assert.Equal(t, false, extensionConfig.Features["monitoring"]) + }) + + t.Run("ExtractServiceLevelConfig", func(t *testing.T) { + apiService := project.Services["api"] + require.NotNil(t, apiService) + + // Create a config from the service AdditionalProperties + serviceCfg := config.NewConfig(apiService.AdditionalProperties) + + // Define a type-safe structure for monitoring config + type MonitoringConfig struct { + Enabled bool `yaml:"enabled"` + Level string `yaml:"level"` + } + + // Extract monitoring configuration using GetSection + var monitoringConfig MonitoringConfig + found, err := serviceCfg.GetSection("monitoring", &monitoringConfig) + require.NoError(t, err) + require.True(t, found, "monitoring section should be found") + + // Verify the type-safe configuration + assert.True(t, monitoringConfig.Enabled) + assert.Equal(t, "verbose", monitoringConfig.Level) + }) + + t.Run("RoundTripWithExtractedConfig", func(t *testing.T) { + tempDir := t.TempDir() + + // Save the original project + originalFile := filepath.Join(tempDir, "original.yaml") + err := Save(context.Background(), project, originalFile) + require.NoError(t, err) + + // Load it back + loadedProject, err := Load(context.Background(), originalFile) + require.NoError(t, err) + + // Extract the extension config using the config system + cfg := config.NewConfig(loadedProject.AdditionalProperties) + var extensionConfig ExtensionConfig + found, err := cfg.GetSection("extensionConfig", &extensionConfig) + require.NoError(t, err) + require.True(t, found, "extensionConfig section should be found") + + // Modify the configuration + extensionConfig.Timeout = 600 + extensionConfig.Database.Host = "production-db" + + // Create a new config from the modified struct and extract as raw map + modifiedCfg := config.NewConfig(map[string]interface{}{ + "extensionConfig": extensionConfig, + }) + modifiedRaw := modifiedCfg.Raw() + + loadedProject.AdditionalProperties["extensionConfig"] = modifiedRaw["extensionConfig"] + + // Save the modified project + modifiedFile := filepath.Join(tempDir, "modified.yaml") + err = Save(context.Background(), loadedProject, modifiedFile) + require.NoError(t, err) + + // Load and verify the changes were preserved + finalProject, err := Load(context.Background(), modifiedFile) + require.NoError(t, err) + + // Extract the final config using the config system + finalCfg := config.NewConfig(finalProject.AdditionalProperties) + var finalExtensionConfig ExtensionConfig + found, err = finalCfg.GetSection("extensionConfig", &finalExtensionConfig) + require.NoError(t, err) + require.True(t, found, "extensionConfig section should be found") + + // Verify the modifications were preserved + assert.Equal(t, 600, finalExtensionConfig.Timeout) + assert.Equal(t, "production-db", finalExtensionConfig.Database.Host) + assert.Equal(t, 3, finalExtensionConfig.Retries) // Unchanged + }) +} diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 16c0791f3e1..584375bbc37 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -58,6 +58,9 @@ type ServiceConfig struct { // Environment variables to set for the service Environment osutil.ExpandableMap `yaml:"env,omitempty"` + // AdditionalProperties captures any unknown YAML fields for extension support + AdditionalProperties map[string]interface{} `yaml:",inline"` + *ext.EventDispatcher[ServiceLifecycleEventArgs] `yaml:"-"` // Turns service into a service that is only to be built but not deployed. diff --git a/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-combined-extensions.snap b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-combined-extensions.snap new file mode 100644 index 00000000000..7ccbae14f0f --- /dev/null +++ b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-combined-extensions.snap @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: test-combined-extensions +services: + api: + project: ./src/api + host: containerapp + language: js + customConfig: + setting1: value1 + serviceExtension: api-specific +projectExtension: global-setting +sharedConfig: + environment: production + version: 1.0.0 + diff --git a/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-project-level-extensions.snap b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-project-level-extensions.snap new file mode 100644 index 00000000000..85d7e4feee5 --- /dev/null +++ b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-project-level-extensions.snap @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: test-extension-project +services: + api: + project: ./src/api + host: containerapp + language: js +customProjectField: project-level-extension +extensionConfig: + database: + host: localhost + port: 5432 + retries: 3 + timeout: 300 +organizationSettings: + billing: department-a + compliance: true + tags: + - production + - critical + diff --git a/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-service-level-extensions.snap b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-service-level-extensions.snap new file mode 100644 index 00000000000..ee9620c01e4 --- /dev/null +++ b/cli/azd/pkg/project/testdata/TestAdditionalPropertiesMarshalling-service-level-extensions.snap @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: test-service-extension +services: + api: + project: ./src/api + host: containerapp + language: js + customServiceField: service-level-extension + extensionSettings: + caching: redis + features: + featureA: true + featureB: false + timeout: 30 + monitoring: + alerts: + - cpu > 80% + - memory > 90% + logging: verbose + metrics: true + web: + project: ./src/web + host: staticwebapp + language: ts + deployment: + region: eastus + strategy: blue-green + From d4c99b85ca6548127a15a28ea276b3355c0fcd08 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 14:52:13 -0800 Subject: [PATCH 2/6] Adds new gRPC functions to get/set project/service configuration --- cli/azd/grpc/proto/project.proto | 114 +++ .../internal/grpcserver/project_service.go | 556 ++++++++++- .../grpcserver/project_service_test.go | 791 +++++++++++++++- cli/azd/pkg/azdext/project.pb.go | 874 +++++++++++++++++- cli/azd/pkg/azdext/project_grpc.pb.go | 404 +++++++- 5 files changed, 2706 insertions(+), 33 deletions(-) diff --git a/cli/azd/grpc/proto/project.proto b/cli/azd/grpc/proto/project.proto index 62df83af009..9b897307ba2 100644 --- a/cli/azd/grpc/proto/project.proto +++ b/cli/azd/grpc/proto/project.proto @@ -7,6 +7,7 @@ package azdext; option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext"; import "models.proto"; +import "include/google/protobuf/struct.proto"; // ProjectService defines methods for managing projects and their configurations. service ProjectService { @@ -15,6 +16,36 @@ service ProjectService { // AddService adds a new service to the project. rpc AddService(AddServiceRequest) returns (EmptyResponse); + + // Gets a configuration section by path. + rpc GetConfigSection(GetProjectConfigSectionRequest) returns (GetProjectConfigSectionResponse); + + // Gets a configuration value by path. + rpc GetConfigValue(GetProjectConfigValueRequest) returns (GetProjectConfigValueResponse); + + // Sets a configuration section by path. + rpc SetConfigSection(SetProjectConfigSectionRequest) returns (EmptyResponse); + + // Sets a configuration value by path. + rpc SetConfigValue(SetProjectConfigValueRequest) returns (EmptyResponse); + + // Removes configuration by path. + rpc UnsetConfig(UnsetProjectConfigRequest) returns (EmptyResponse); + + // Gets a service configuration section by path. + rpc GetServiceConfigSection(GetServiceConfigSectionRequest) returns (GetServiceConfigSectionResponse); + + // Gets a service configuration value by path. + rpc GetServiceConfigValue(GetServiceConfigValueRequest) returns (GetServiceConfigValueResponse); + + // Sets a service configuration section by path. + rpc SetServiceConfigSection(SetServiceConfigSectionRequest) returns (EmptyResponse); + + // Sets a service configuration value by path. + rpc SetServiceConfigValue(SetServiceConfigValueRequest) returns (EmptyResponse); + + // Removes service configuration by path. + rpc UnsetServiceConfig(UnsetServiceConfigRequest) returns (EmptyResponse); } // GetProjectResponse message definition @@ -26,3 +57,86 @@ message GetProjectResponse { message AddServiceRequest { ServiceConfig service = 1; } + +// Request message for GetConfigSection +message GetProjectConfigSectionRequest { + string path = 1; +} + +// Response message for GetConfigSection +message GetProjectConfigSectionResponse { + google.protobuf.Struct section = 1; + bool found = 2; +} + +// Request message for GetConfigValue +message GetProjectConfigValueRequest { + string path = 1; +} + +// Response message for GetConfigValue +message GetProjectConfigValueResponse { + google.protobuf.Value value = 1; + bool found = 2; +} + +// Request message for SetConfigSection +message SetProjectConfigSectionRequest { + string path = 1; + google.protobuf.Struct section = 2; +} + +// Request message for SetConfigValue +message SetProjectConfigValueRequest { + string path = 1; + google.protobuf.Value value = 2; +} + +// Request message for UnsetConfig +message UnsetProjectConfigRequest { + string path = 1; +} + +// Request message for GetServiceConfigSection +message GetServiceConfigSectionRequest { + string service_name = 1; + string path = 2; +} + +// Response message for GetServiceConfigSection +message GetServiceConfigSectionResponse { + google.protobuf.Struct section = 1; + bool found = 2; +} + +// Request message for GetServiceConfigValue +message GetServiceConfigValueRequest { + string service_name = 1; + string path = 2; +} + +// Response message for GetServiceConfigValue +message GetServiceConfigValueResponse { + google.protobuf.Value value = 1; + bool found = 2; +} + +// Request message for SetServiceConfigSection +message SetServiceConfigSectionRequest { + string service_name = 1; + string path = 2; + google.protobuf.Struct section = 3; +} + +// Request message for SetServiceConfigValue +message SetServiceConfigValueRequest { + string service_name = 1; + string path = 2; + google.protobuf.Value value = 3; +} + +// Request message for UnsetServiceConfig +message UnsetServiceConfigRequest { + string service_name = 1; + string path = 2; +} diff --git a/cli/azd/internal/grpcserver/project_service.go b/cli/azd/internal/grpcserver/project_service.go index 04ffadc9ad1..5bc9f4d5dd0 100644 --- a/cli/azd/internal/grpcserver/project_service.go +++ b/cli/azd/internal/grpcserver/project_service.go @@ -9,36 +9,62 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/mapper" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/config" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" "github.com/azure/azure-dev/cli/azd/pkg/lazy" "github.com/azure/azure-dev/cli/azd/pkg/project" + "google.golang.org/protobuf/types/known/structpb" ) type projectService struct { azdext.UnimplementedProjectServiceServer - lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] - lazyEnvManager *lazy.Lazy[environment.Manager] + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] + lazyEnvManager *lazy.Lazy[environment.Manager] + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig] } +// NewProjectService creates a new project service instance with lazy-loaded dependencies. +// The service provides gRPC methods for managing Azure Developer CLI projects, including +// project configuration, service management, and extension configuration through AdditionalProperties. +// +// Parameters: +// - lazyAzdContext: Lazy-loaded Azure Developer CLI context for project directory operations +// - lazyEnvManager: Lazy-loaded environment manager for handling Azure environments +// - lazyProjectConfig: Lazy-loaded project configuration for accessing project settings +// +// Returns an implementation of azdext.ProjectServiceServer. func NewProjectService( lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], lazyEnvManager *lazy.Lazy[environment.Manager], + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], ) azdext.ProjectServiceServer { return &projectService{ - lazyAzdContext: lazyAzdContext, - lazyEnvManager: lazyEnvManager, + lazyAzdContext: lazyAzdContext, + lazyEnvManager: lazyEnvManager, + lazyProjectConfig: lazyProjectConfig, } } +// Get retrieves the complete project configuration including all services and metadata. +// This method resolves environment variables in configuration values using the default environment +// and converts the internal project configuration to the protobuf format for gRPC communication. +// +// The returned project includes: +// - Basic project metadata (name, resource group, path) +// - Infrastructure configuration (provider, path, module) +// - All configured services with their settings +// - Template metadata if available +// +// Environment variable substitution is performed using the default environment's variables. func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*azdext.GetProjectResponse, error) { azdContext, err := s.lazyAzdContext.GetValue() if err != nil { return nil, err } - projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + projectConfig, err := s.lazyProjectConfig.GetValue() if err != nil { return nil, err } @@ -98,13 +124,22 @@ func (s *projectService) Get(ctx context.Context, req *azdext.EmptyRequest) (*az }, nil } +// AddService adds a new service to the project configuration and persists the changes. +// The service configuration is converted from the protobuf format to the internal representation +// and added to the project's services map. The updated project configuration is then saved to disk. +// +// Parameters: +// - req.Service: The service configuration to add, including name, host, language, and other settings +// +// The service name from req.Service.Name is used as the key in the services map. +// If the services map doesn't exist, it will be initialized. func (s *projectService) AddService(ctx context.Context, req *azdext.AddServiceRequest) (*azdext.EmptyResponse, error) { azdContext, err := s.lazyAzdContext.GetValue() if err != nil { return nil, err } - projectConfig, err := project.Load(ctx, azdContext.ProjectPath()) + projectConfig, err := s.lazyProjectConfig.GetValue() if err != nil { return nil, err } @@ -125,3 +160,512 @@ func (s *projectService) AddService(ctx context.Context, req *azdext.AddServiceR return &azdext.EmptyResponse{}, nil } + +// GetConfigSection retrieves a configuration section from the project's AdditionalProperties. +// This method provides access to extension-specific configuration data stored in the project +// configuration using dot-notation paths (e.g., "extension.database.connection"). +// +// Parameters: +// - req.Path: Dot-notation path to the configuration section (e.g., "custom.settings") +// +// Returns: +// - Section: The configuration section as a protobuf Struct if found +// - Found: Boolean indicating whether the section exists +// +// If AdditionalProperties is nil, it's treated as an empty configuration. +func (s *projectService) GetConfigSection( + ctx context.Context, req *azdext.GetProjectConfigSectionRequest, +) (*azdext.GetProjectConfigSectionResponse, error) { + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := projectConfig.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + section, found := cfg.GetMap(req.Path) + + if !found { + return &azdext.GetProjectConfigSectionResponse{ + Found: false, + }, nil + } + + // Convert section to protobuf Struct + protoStruct, err := structpb.NewStruct(section) + if err != nil { + return nil, fmt.Errorf("failed to convert section to protobuf struct: %w", err) + } + + return &azdext.GetProjectConfigSectionResponse{ + Section: protoStruct, + Found: true, + }, nil +} + +// GetConfigValue retrieves a specific configuration value from the project's AdditionalProperties. +// This method provides access to individual configuration values stored in the project +// configuration using dot-notation paths (e.g., "extension.database.port"). +// +// Parameters: +// - req.Path: Dot-notation path to the configuration value (e.g., "custom.settings.timeout") +// +// Returns: +// - Value: The configuration value as a protobuf Value if found +// - Found: Boolean indicating whether the value exists +// +// Supports all JSON types: strings, numbers, booleans, objects, and arrays. +// If AdditionalProperties is nil, it's treated as an empty configuration. +func (s *projectService) GetConfigValue( + ctx context.Context, + req *azdext.GetProjectConfigValueRequest, +) (*azdext.GetProjectConfigValueResponse, error) { + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := projectConfig.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + value, ok := cfg.Get(req.Path) + + if !ok { + return &azdext.GetProjectConfigValueResponse{ + Found: false, + }, nil + } + + // Convert value to protobuf Value + protoValue, err := structpb.NewValue(value) + if err != nil { + return nil, fmt.Errorf("failed to convert value to protobuf value: %w", err) + } + + return &azdext.GetProjectConfigValueResponse{ + Value: protoValue, + Found: true, + }, nil +} + +// SetConfigSection sets or updates a configuration section in the project's AdditionalProperties. +// This method allows extensions to store complex configuration data as nested objects +// using dot-notation paths. The changes are immediately persisted to the project file. +// +// Parameters: +// - req.Path: Dot-notation path where to store the section (e.g., "custom.database") +// - req.Section: The configuration section as a protobuf Struct containing the data +// +// If the path doesn't exist, it will be created. Existing data at the path will be replaced. +// If AdditionalProperties is nil, it will be initialized as an empty map. +func (s *projectService) SetConfigSection( + ctx context.Context, + req *azdext.SetProjectConfigSectionRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := projectConfig.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + + // Convert protobuf Struct to map + sectionMap := req.Section.AsMap() + if err := cfg.Set(req.Path, sectionMap); err != nil { + return nil, fmt.Errorf("failed to set config section: %w", err) + } + + // Update project AdditionalProperties and save + projectConfig.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// SetConfigValue sets or updates a specific configuration value in the project's AdditionalProperties. +// This method allows extensions to store individual configuration values using dot-notation paths. +// The changes are immediately persisted to the project file. +// +// Parameters: +// - req.Path: Dot-notation path where to store the value (e.g., "custom.settings.timeout") +// - req.Value: The configuration value as a protobuf Value (string, number, boolean, etc.) +// +// If the path doesn't exist, intermediate objects will be created automatically. +// Existing data at the path will be replaced. +// If AdditionalProperties is nil, it will be initialized as an empty map. +func (s *projectService) SetConfigValue( + ctx context.Context, + req *azdext.SetProjectConfigValueRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := projectConfig.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + + // Convert protobuf Value to interface{} + value := req.Value.AsInterface() + if err := cfg.Set(req.Path, value); err != nil { + return nil, fmt.Errorf("failed to set config value: %w", err) + } + + // Update project AdditionalProperties and save + projectConfig.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// UnsetConfig removes a configuration value or section from the project's AdditionalProperties. +// This method allows extensions to clean up configuration data using dot-notation paths. +// The changes are immediately persisted to the project file. +// +// Parameters: +// - req.Path: Dot-notation path to the configuration to remove (e.g., "custom.settings.timeout") +// +// If the path points to a value, only that value is removed. +// If the path points to a section, the entire section and all its contents are removed. +// If the path doesn't exist, the operation succeeds without error. +func (s *projectService) UnsetConfig( + ctx context.Context, + req *azdext.UnsetProjectConfigRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := projectConfig.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + if err := cfg.Unset(req.Path); err != nil { + return nil, fmt.Errorf("failed to unset config: %w", err) + } + + // Update project AdditionalProperties and save + projectConfig.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// GetServiceConfigSection retrieves a configuration section from a specific service's AdditionalProperties. +// This method provides access to service-specific extension configuration data using dot-notation paths. +// +// Parameters: +// - req.ServiceName: Name of the service to retrieve configuration from +// - req.Path: Dot-notation path to the configuration section (e.g., "custom.database") +// +// Returns: +// - Section: The configuration section as a protobuf Struct if found +// - Found: Boolean indicating whether the section exists +// +// Returns an error if the specified service doesn't exist in the project. +// If the service's AdditionalProperties is nil, it's treated as an empty configuration. +func (s *projectService) GetServiceConfigSection( + ctx context.Context, + req *azdext.GetServiceConfigSectionRequest, +) (*azdext.GetServiceConfigSectionResponse, error) { + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Check if service exists + service, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, fmt.Errorf("service '%s' not found", req.ServiceName) + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := service.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + section, found := cfg.GetMap(req.Path) + + if !found { + return &azdext.GetServiceConfigSectionResponse{ + Found: false, + }, nil + } + + // Convert section to protobuf Struct + protoStruct, err := structpb.NewStruct(section) + if err != nil { + return nil, fmt.Errorf("failed to convert section to protobuf struct: %w", err) + } + + return &azdext.GetServiceConfigSectionResponse{ + Section: protoStruct, + Found: true, + }, nil +} + +// GetServiceConfigValue retrieves a specific configuration value from a service's AdditionalProperties. +// This method provides access to individual service-specific configuration values using dot-notation paths. +// +// Parameters: +// - req.ServiceName: Name of the service to retrieve configuration from +// - req.Path: Dot-notation path to the configuration value (e.g., "custom.database.port") +// +// Returns: +// - Value: The configuration value as a protobuf Value if found +// - Found: Boolean indicating whether the value exists +// +// Supports all JSON types: strings, numbers, booleans, objects, and arrays. +// Returns an error if the specified service doesn't exist in the project. +// If the service's AdditionalProperties is nil, it's treated as an empty configuration. +func (s *projectService) GetServiceConfigValue( + ctx context.Context, + req *azdext.GetServiceConfigValueRequest, +) (*azdext.GetServiceConfigValueResponse, error) { + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Check if service exists + service, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, fmt.Errorf("service '%s' not found", req.ServiceName) + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := service.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + value, ok := cfg.Get(req.Path) + + if !ok { + return &azdext.GetServiceConfigValueResponse{ + Found: false, + }, nil + } + + // Convert value to protobuf Value + protoValue, err := structpb.NewValue(value) + if err != nil { + return nil, fmt.Errorf("failed to convert value to protobuf value: %w", err) + } + + return &azdext.GetServiceConfigValueResponse{ + Value: protoValue, + Found: true, + }, nil +} + +// SetServiceConfigSection sets or updates a configuration section in a service's AdditionalProperties. +// This method allows extensions to store complex service-specific configuration data as nested objects. +// The changes are immediately persisted to the project file. +// +// Parameters: +// - req.ServiceName: Name of the service to update configuration for +// - req.Path: Dot-notation path where to store the section (e.g., "custom.database") +// - req.Section: The configuration section as a protobuf Struct containing the data +// +// Returns an error if the specified service doesn't exist in the project. +// If the path doesn't exist, it will be created. Existing data at the path will be replaced. +// If the service's AdditionalProperties is nil, it will be initialized as an empty map. +func (s *projectService) SetServiceConfigSection( + ctx context.Context, + req *azdext.SetServiceConfigSectionRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Check if service exists + service, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, fmt.Errorf("service '%s' not found", req.ServiceName) + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := service.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + + // Convert protobuf Struct to map + sectionMap := req.Section.AsMap() + if err := cfg.Set(req.Path, sectionMap); err != nil { + return nil, fmt.Errorf("failed to set service config section: %w", err) + } + + // Update service AdditionalProperties and save + service.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// SetServiceConfigValue sets or updates a specific configuration value in a service's AdditionalProperties. +// This method allows extensions to store individual service-specific configuration values. +// The changes are immediately persisted to the project file. +// +// Parameters: +// - req.ServiceName: Name of the service to update configuration for +// - req.Path: Dot-notation path where to store the value (e.g., "custom.database.port") +// - req.Value: The configuration value as a protobuf Value (string, number, boolean, etc.) +// +// Returns an error if the specified service doesn't exist in the project. +// If the path doesn't exist, intermediate objects will be created automatically. +// Existing data at the path will be replaced. +// If the service's AdditionalProperties is nil, it will be initialized as an empty map. +func (s *projectService) SetServiceConfigValue( + ctx context.Context, + req *azdext.SetServiceConfigValueRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Check if service exists + service, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, fmt.Errorf("service '%s' not found", req.ServiceName) + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := service.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + + // Convert protobuf Value to interface{} + value := req.Value.AsInterface() + if err := cfg.Set(req.Path, value); err != nil { + return nil, fmt.Errorf("failed to set service config value: %w", err) + } + + // Update service AdditionalProperties and save + service.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} + +// UnsetServiceConfig removes a configuration value or section from a service's AdditionalProperties. +// This method allows extensions to clean up service-specific configuration data. +// The changes are immediately persisted to the project file. +// +// Parameters: +// - req.ServiceName: Name of the service to remove configuration from +// - req.Path: Dot-notation path to the configuration to remove (e.g., "custom.database.port") +// +// Returns an error if the specified service doesn't exist in the project. +// If the path points to a value, only that value is removed. +// If the path points to a section, the entire section and all its contents are removed. +// If the path doesn't exist, the operation succeeds without error. +func (s *projectService) UnsetServiceConfig( + ctx context.Context, + req *azdext.UnsetServiceConfigRequest, +) (*azdext.EmptyResponse, error) { + azdContext, err := s.lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, err + } + + // Check if service exists + service, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, fmt.Errorf("service '%s' not found", req.ServiceName) + } + + // Initialize empty map if AdditionalProperties is nil + additionalProps := service.AdditionalProperties + if additionalProps == nil { + additionalProps = make(map[string]any) + } + + cfg := config.NewConfig(additionalProps) + if err := cfg.Unset(req.Path); err != nil { + return nil, fmt.Errorf("failed to unset service config: %w", err) + } + + // Update service AdditionalProperties and save + service.AdditionalProperties = cfg.Raw() + if err := project.Save(ctx, projectConfig, azdContext.ProjectPath()); err != nil { + return nil, err + } + + return &azdext.EmptyResponse{}, nil +} diff --git a/cli/azd/internal/grpcserver/project_service_test.go b/cli/azd/internal/grpcserver/project_service_test.go index ff55a66ef13..be7e9088a32 100644 --- a/cli/azd/internal/grpcserver/project_service_test.go +++ b/cli/azd/internal/grpcserver/project_service_test.go @@ -16,6 +16,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/project" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" ) // Test_ProjectService_NoProject ensures that when no project exists, @@ -31,9 +32,12 @@ func Test_ProjectService_NoProject(t *testing.T) { lazyEnvManager := lazy.NewLazy(func() (environment.Manager, error) { return nil, azdcontext.ErrNoProject }) + lazyProjectConfig := lazy.NewLazy(func() (*project.ProjectConfig, error) { + return nil, azdcontext.ErrNoProject + }) // Create the service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) _, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) require.Error(t, err) } @@ -65,6 +69,7 @@ func Test_ProjectService_Flow(t *testing.T) { // Create lazy-loaded instances. lazyAzdContext := lazy.From(azdContext) lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(&projectConfig) // Create an environment and set an environment variable. testEnv1, err := envManager.Create(*mockContext.Context, environment.Spec{ @@ -77,7 +82,7 @@ func Test_ProjectService_Flow(t *testing.T) { require.NoError(t, err) // Create the service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) // Test: Retrieve project details. getResponse, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) @@ -111,9 +116,10 @@ func Test_ProjectService_AddService(t *testing.T) { // Create lazy-loaded instances. lazyAzdContext := lazy.From(azdContext) lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(&projectConfig) // Create the project service. - service := NewProjectService(lazyAzdContext, lazyEnvManager) + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) // Prepare a new service addition request. serviceRequest := &azdext.AddServiceRequest{ @@ -141,3 +147,782 @@ func Test_ProjectService_AddService(t *testing.T) { require.Equal(t, project.ServiceLanguagePython, serviceConfig.Language) require.Equal(t, project.ContainerAppTarget, serviceConfig.Host) } + +func Test_ProjectService_ConfigSection(t *testing.T) { + // Setup mock context and temporary project directory + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create project config with additional properties + projectConfig := &project.ProjectConfig{ + Name: "test", + AdditionalProperties: map[string]any{ + "database": map[string]any{ + "host": "localhost", + "port": 5432, + "credentials": map[string]any{ + "username": "admin", + "password": "secret", + }, + }, + "feature": map[string]any{ + "enabled": true, + }, + }, + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("GetConfigSection_Success", func(t *testing.T) { + resp, err := service.GetConfigSection(*mockContext.Context, &azdext.GetProjectConfigSectionRequest{ + Path: "database", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Section) + + sectionMap := resp.Section.AsMap() + require.Equal(t, "localhost", sectionMap["host"]) + require.Equal(t, float64(5432), sectionMap["port"]) // JSON numbers are float64 + }) + + t.Run("GetConfigSection_NestedSection", func(t *testing.T) { + resp, err := service.GetConfigSection(*mockContext.Context, &azdext.GetProjectConfigSectionRequest{ + Path: "database.credentials", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Section) + + sectionMap := resp.Section.AsMap() + require.Equal(t, "admin", sectionMap["username"]) + require.Equal(t, "secret", sectionMap["password"]) + }) + + t.Run("GetConfigSection_NotFound", func(t *testing.T) { + resp, err := service.GetConfigSection(*mockContext.Context, &azdext.GetProjectConfigSectionRequest{ + Path: "nonexistent", + }) + require.NoError(t, err) + require.False(t, resp.Found) + require.Nil(t, resp.Section) + }) +} + +func Test_ProjectService_ConfigValue(t *testing.T) { + // Setup mock context and temporary project directory + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create project config with additional properties + projectConfig := &project.ProjectConfig{ + Name: "test", + AdditionalProperties: map[string]any{ + "database": map[string]any{ + "host": "localhost", + "port": 5432, + "enabled": true, + }, + "version": "1.0.0", + }, + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("GetConfigValue_String", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "version", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, "1.0.0", resp.Value.AsInterface()) + }) + + t.Run("GetConfigValue_NestedString", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "database.host", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, "localhost", resp.Value.AsInterface()) + }) + + t.Run("GetConfigValue_Number", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "database.port", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, float64(5432), resp.Value.AsInterface()) + }) + + t.Run("GetConfigValue_Boolean", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "database.enabled", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.Equal(t, true, resp.Value.AsInterface()) + }) + + t.Run("GetConfigValue_NotFound", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "nonexistent", + }) + require.NoError(t, err) + require.False(t, resp.Found) + require.Nil(t, resp.Value) + }) +} + +func Test_ProjectService_SetConfigSection(t *testing.T) { + // Setup mock context and temporary project directory + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create initial project config + projectConfig := &project.ProjectConfig{ + Name: "test", + AdditionalProperties: map[string]any{ + "existing": "value", + }, + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("SetConfigSection_NewSection", func(t *testing.T) { + // Create section data + sectionData := map[string]any{ + "host": "newhost", + "port": 3306, + "ssl": true, + } + sectionStruct, err := structpb.NewStruct(sectionData) + require.NoError(t, err) + + // Set the section + _, err = service.SetConfigSection(*mockContext.Context, &azdext.SetProjectConfigSectionRequest{ + Path: "mysql", + Section: sectionStruct, + }) + require.NoError(t, err) + + // Verify section was set in the project config + require.NotNil(t, projectConfig.AdditionalProperties["mysql"]) + mysqlSection := projectConfig.AdditionalProperties["mysql"].(map[string]any) + require.Equal(t, "newhost", mysqlSection["host"]) + require.Equal(t, float64(3306), mysqlSection["port"]) + require.Equal(t, true, mysqlSection["ssl"]) + }) + + t.Run("SetConfigSection_NestedSection", func(t *testing.T) { + // Create nested section data + sectionData := map[string]any{ + "username": "admin", + "password": "secret123", + } + sectionStruct, err := structpb.NewStruct(sectionData) + require.NoError(t, err) + + // Set the nested section + _, err = service.SetConfigSection(*mockContext.Context, &azdext.SetProjectConfigSectionRequest{ + Path: "mysql.credentials", + Section: sectionStruct, + }) + require.NoError(t, err) + + // Verify nested section was set + mysqlSection := projectConfig.AdditionalProperties["mysql"].(map[string]any) + credentials := mysqlSection["credentials"].(map[string]any) + require.Equal(t, "admin", credentials["username"]) + require.Equal(t, "secret123", credentials["password"]) + }) +} + +func Test_ProjectService_SetConfigValue(t *testing.T) { + // Setup mock context and temporary project directory + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create initial project config + projectConfig := &project.ProjectConfig{ + Name: "test", + AdditionalProperties: map[string]any{ + "existing": "value", + }, + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("SetConfigValue_String", func(t *testing.T) { + value, err := structpb.NewValue("test-string") + require.NoError(t, err) + + _, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{ + Path: "app.name", + Value: value, + }) + require.NoError(t, err) + + // Verify value was set + appSection := projectConfig.AdditionalProperties["app"].(map[string]any) + require.Equal(t, "test-string", appSection["name"]) + }) + + t.Run("SetConfigValue_Number", func(t *testing.T) { + value, err := structpb.NewValue(8080) + require.NoError(t, err) + + _, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{ + Path: "app.port", + Value: value, + }) + require.NoError(t, err) + + // Verify value was set + appSection := projectConfig.AdditionalProperties["app"].(map[string]any) + require.Equal(t, float64(8080), appSection["port"]) + }) + + t.Run("SetConfigValue_Boolean", func(t *testing.T) { + value, err := structpb.NewValue(true) + require.NoError(t, err) + + _, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{ + Path: "app.debug", + Value: value, + }) + require.NoError(t, err) + + // Verify value was set + appSection := projectConfig.AdditionalProperties["app"].(map[string]any) + require.Equal(t, true, appSection["debug"]) + }) +} + +func Test_ProjectService_UnsetConfig(t *testing.T) { + // Setup mock context and temporary project directory + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create project config with additional properties to unset + projectConfig := &project.ProjectConfig{ + Name: "test", + AdditionalProperties: map[string]any{ + "database": map[string]any{ + "host": "localhost", + "port": 5432, + "credentials": map[string]any{ + "username": "admin", + "password": "secret", + }, + }, + "cache": map[string]any{ + "enabled": true, + "ttl": 300, + }, + }, + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("UnsetConfig_NestedValue", func(t *testing.T) { + _, err := service.UnsetConfig(*mockContext.Context, &azdext.UnsetProjectConfigRequest{ + Path: "database.credentials.password", + }) + require.NoError(t, err) + + // Verify nested value was removed + databaseSection := projectConfig.AdditionalProperties["database"].(map[string]any) + credentials := databaseSection["credentials"].(map[string]any) + _, exists := credentials["password"] + require.False(t, exists) + // But username should still exist + require.Equal(t, "admin", credentials["username"]) + }) + + t.Run("UnsetConfig_EntireSection", func(t *testing.T) { + _, err := service.UnsetConfig(*mockContext.Context, &azdext.UnsetProjectConfigRequest{ + Path: "cache", + }) + require.NoError(t, err) + + // Verify entire section was removed + _, exists := projectConfig.AdditionalProperties["cache"] + require.False(t, exists) + // But database section should still exist + _, exists = projectConfig.AdditionalProperties["database"] + require.True(t, exists) + }) + + t.Run("UnsetConfig_NonexistentPath", func(t *testing.T) { + _, err := service.UnsetConfig(*mockContext.Context, &azdext.UnsetProjectConfigRequest{ + Path: "nonexistent.path", + }) + // Should not error even if path doesn't exist + require.NoError(t, err) + }) +} + +func Test_ProjectService_ConfigNilAdditionalProperties(t *testing.T) { + // Test behavior when AdditionalProperties is nil + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + azdContext := azdcontext.NewAzdContextWithDirectory(temp) + + // Create project config WITHOUT additional properties + projectConfig := &project.ProjectConfig{ + Name: "test", + // AdditionalProperties is nil + } + err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath()) + require.NoError(t, err) + + // Setup lazy dependencies + lazyAzdContext := lazy.From(azdContext) + 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) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("GetConfigValue_NilAdditionalProperties", func(t *testing.T) { + resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ + Path: "any.path", + }) + require.NoError(t, err) + require.False(t, resp.Found) + }) + + t.Run("SetConfigValue_NilAdditionalProperties", func(t *testing.T) { + value, err := structpb.NewValue("test-value") + require.NoError(t, err) + + _, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{ + Path: "new.value", + Value: value, + }) + require.NoError(t, err) + + // Verify AdditionalProperties was initialized and value was set + require.NotNil(t, projectConfig.AdditionalProperties) + newSection := projectConfig.AdditionalProperties["new"].(map[string]any) + require.Equal(t, "test-value", newSection["value"]) + }) +} + +// Test_ProjectService_ServiceConfiguration validates service-level configuration operations. +func Test_ProjectService_ServiceConfiguration(t *testing.T) { + // Setup a mock context and temporary project directory. + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + + // Initialize project configuration with a service. + projectConfig := &project.ProjectConfig{ + Name: "test-project", + Path: temp, + Services: map[string]*project.ServiceConfig{ + "api": { + Name: "api", + Host: project.ContainerAppTarget, + Language: "javascript", + OutputPath: "./dist", + AdditionalProperties: map[string]any{ + "custom": map[string]any{ + "setting": "value", + "nested": map[string]any{ + "key": "nested-value", + }, + }, + "database": map[string]any{ + "host": "localhost", + "port": float64(5432), // JSON numbers become float64 + }, + }, + }, + "web": { + Name: "web", + Host: project.StaticWebAppTarget, + Language: "typescript", + }, + }, + } + + // Mock AzdContext with project path. + azdContext := &azdcontext.AzdContext{} + azdContext.SetProjectDirectory(temp) + + // 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 loaders. + lazyAzdContext := lazy.From(azdContext) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + // Create the service. + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("GetServiceConfigSection_Found", func(t *testing.T) { + resp, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ + ServiceName: "api", + Path: "custom", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Section) + + // Verify the section content + sectionMap := resp.Section.AsMap() + require.Equal(t, "value", sectionMap["setting"]) + + nested := sectionMap["nested"].(map[string]any) + require.Equal(t, "nested-value", nested["key"]) + }) + + t.Run("GetServiceConfigSection_NotFound", func(t *testing.T) { + resp, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ + ServiceName: "api", + Path: "nonexistent", + }) + require.NoError(t, err) + require.False(t, resp.Found) + require.Nil(t, resp.Section) + }) + + t.Run("GetServiceConfigSection_ServiceNotFound", func(t *testing.T) { + _, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ + ServiceName: "nonexistent", + Path: "custom", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "service 'nonexistent' not found") + }) + + t.Run("GetServiceConfigValue_Found", func(t *testing.T) { + resp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "api", + Path: "custom.setting", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Value) + require.Equal(t, "value", resp.Value.AsInterface()) + }) + + t.Run("GetServiceConfigValue_NestedValue", func(t *testing.T) { + resp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "api", + Path: "custom.nested.key", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Value) + require.Equal(t, "nested-value", resp.Value.AsInterface()) + }) + + t.Run("GetServiceConfigValue_NumericValue", func(t *testing.T) { + resp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "api", + Path: "database.port", + }) + require.NoError(t, err) + require.True(t, resp.Found) + require.NotNil(t, resp.Value) + require.Equal(t, float64(5432), resp.Value.AsInterface()) + }) + + t.Run("GetServiceConfigValue_NotFound", func(t *testing.T) { + resp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "api", + Path: "nonexistent.path", + }) + require.NoError(t, err) + require.False(t, resp.Found) + require.Nil(t, resp.Value) + }) + + t.Run("GetServiceConfigValue_ServiceNotFound", func(t *testing.T) { + _, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "nonexistent", + Path: "custom.setting", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "service 'nonexistent' not found") + }) + + t.Run("SetServiceConfigSection", func(t *testing.T) { + sectionData := map[string]any{ + "newSetting": "new-value", + "anotherSetting": map[string]any{ + "innerKey": "inner-value", + }, + } + sectionStruct, err := structpb.NewStruct(sectionData) + require.NoError(t, err) + + _, err = service.SetServiceConfigSection(*mockContext.Context, &azdext.SetServiceConfigSectionRequest{ + ServiceName: "api", + Path: "newSection", + Section: sectionStruct, + }) + require.NoError(t, err) + + // Verify the section was set + apiService := projectConfig.Services["api"] + require.NotNil(t, apiService.AdditionalProperties) + newSection := apiService.AdditionalProperties["newSection"].(map[string]any) + require.Equal(t, "new-value", newSection["newSetting"]) + + anotherSetting := newSection["anotherSetting"].(map[string]any) + require.Equal(t, "inner-value", anotherSetting["innerKey"]) + }) + + t.Run("SetServiceConfigSection_ServiceNotFound", func(t *testing.T) { + sectionStruct, err := structpb.NewStruct(map[string]any{"key": "value"}) + require.NoError(t, err) + + _, err = service.SetServiceConfigSection(*mockContext.Context, &azdext.SetServiceConfigSectionRequest{ + ServiceName: "nonexistent", + Path: "section", + Section: sectionStruct, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "service 'nonexistent' not found") + }) + + t.Run("SetServiceConfigValue", func(t *testing.T) { + value, err := structpb.NewValue("updated-value") + require.NoError(t, err) + + _, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{ + ServiceName: "api", + Path: "custom.setting", + Value: value, + }) + require.NoError(t, err) + + // Verify the value was updated + apiService := projectConfig.Services["api"] + customSection := apiService.AdditionalProperties["custom"].(map[string]any) + require.Equal(t, "updated-value", customSection["setting"]) + }) + + t.Run("SetServiceConfigValue_NewPath", func(t *testing.T) { + value, err := structpb.NewValue(float64(8080)) + require.NoError(t, err) + + _, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{ + ServiceName: "api", + Path: "server.port", + Value: value, + }) + require.NoError(t, err) + + // Verify the new path was created + apiService := projectConfig.Services["api"] + serverSection := apiService.AdditionalProperties["server"].(map[string]any) + require.Equal(t, float64(8080), serverSection["port"]) + }) + + t.Run("SetServiceConfigValue_ServiceNotFound", func(t *testing.T) { + value, err := structpb.NewValue("value") + require.NoError(t, err) + + _, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{ + ServiceName: "nonexistent", + Path: "path", + Value: value, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "service 'nonexistent' not found") + }) + + t.Run("UnsetServiceConfig", func(t *testing.T) { + _, err := service.UnsetServiceConfig(*mockContext.Context, &azdext.UnsetServiceConfigRequest{ + ServiceName: "api", + Path: "custom.setting", + }) + require.NoError(t, err) + + // Verify the value was removed + apiService := projectConfig.Services["api"] + customSection := apiService.AdditionalProperties["custom"].(map[string]any) + _, exists := customSection["setting"] + require.False(t, exists) + }) + + t.Run("UnsetServiceConfig_EntireSection", func(t *testing.T) { + _, err := service.UnsetServiceConfig(*mockContext.Context, &azdext.UnsetServiceConfigRequest{ + ServiceName: "api", + Path: "database", + }) + require.NoError(t, err) + + // Verify the entire section was removed + apiService := projectConfig.Services["api"] + _, exists := apiService.AdditionalProperties["database"] + require.False(t, exists) + }) + + t.Run("UnsetServiceConfig_ServiceNotFound", func(t *testing.T) { + _, err := service.UnsetServiceConfig(*mockContext.Context, &azdext.UnsetServiceConfigRequest{ + ServiceName: "nonexistent", + Path: "path", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "service 'nonexistent' not found") + }) + + t.Run("UnsetServiceConfig_NonexistentPath", func(t *testing.T) { + _, err := service.UnsetServiceConfig(*mockContext.Context, &azdext.UnsetServiceConfigRequest{ + ServiceName: "api", + Path: "nonexistent.path", + }) + require.NoError(t, err) // Should not error even if path doesn't exist + }) +} + +// Test_ProjectService_ServiceConfiguration_NilAdditionalProperties validates service configuration +// operations when AdditionalProperties is nil. +func Test_ProjectService_ServiceConfiguration_NilAdditionalProperties(t *testing.T) { + // Setup a mock context and temporary project directory. + mockContext := mocks.NewMockContext(context.Background()) + temp := t.TempDir() + + // Initialize project configuration with a service that has nil AdditionalProperties. + projectConfig := &project.ProjectConfig{ + Name: "test-project", + Path: temp, + Services: map[string]*project.ServiceConfig{ + "api": { + Name: "api", + Host: project.ContainerAppTarget, + Language: "javascript", + // AdditionalProperties is nil + }, + }, + } + + // Mock AzdContext with project path. + azdContext := &azdcontext.AzdContext{} + azdContext.SetProjectDirectory(temp) + + // 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 loaders. + lazyAzdContext := lazy.From(azdContext) + lazyEnvManager := lazy.From(envManager) + lazyProjectConfig := lazy.From(projectConfig) + + // Create the service. + service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig) + + t.Run("GetServiceConfigSection_NilAdditionalProperties", func(t *testing.T) { + resp, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ + ServiceName: "api", + Path: "any.path", + }) + require.NoError(t, err) + require.False(t, resp.Found) + }) + + t.Run("GetServiceConfigValue_NilAdditionalProperties", func(t *testing.T) { + resp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ + ServiceName: "api", + Path: "any.path", + }) + require.NoError(t, err) + require.False(t, resp.Found) + }) + + t.Run("SetServiceConfigValue_NilAdditionalProperties", func(t *testing.T) { + value, err := structpb.NewValue("test-value") + require.NoError(t, err) + + _, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{ + ServiceName: "api", + Path: "new.value", + Value: value, + }) + require.NoError(t, err) + + // Verify AdditionalProperties was initialized and value was set + apiService := projectConfig.Services["api"] + require.NotNil(t, apiService.AdditionalProperties) + newSection := apiService.AdditionalProperties["new"].(map[string]any) + require.Equal(t, "test-value", newSection["value"]) + }) +} diff --git a/cli/azd/pkg/azdext/project.pb.go b/cli/azd/pkg/azdext/project.pb.go index c53579c6577..242e24ea50b 100644 --- a/cli/azd/pkg/azdext/project.pb.go +++ b/cli/azd/pkg/azdext/project.pb.go @@ -12,6 +12,7 @@ package azdext import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -114,19 +115,804 @@ func (x *AddServiceRequest) GetService() *ServiceConfig { return nil } +// Request message for GetConfigSection +type GetProjectConfigSectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectConfigSectionRequest) Reset() { + *x = GetProjectConfigSectionRequest{} + mi := &file_project_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectConfigSectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectConfigSectionRequest) ProtoMessage() {} + +func (x *GetProjectConfigSectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_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 GetProjectConfigSectionRequest.ProtoReflect.Descriptor instead. +func (*GetProjectConfigSectionRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{2} +} + +func (x *GetProjectConfigSectionRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Response message for GetConfigSection +type GetProjectConfigSectionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Section *structpb.Struct `protobuf:"bytes,1,opt,name=section,proto3" json:"section,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectConfigSectionResponse) Reset() { + *x = GetProjectConfigSectionResponse{} + mi := &file_project_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectConfigSectionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectConfigSectionResponse) ProtoMessage() {} + +func (x *GetProjectConfigSectionResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_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 GetProjectConfigSectionResponse.ProtoReflect.Descriptor instead. +func (*GetProjectConfigSectionResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{3} +} + +func (x *GetProjectConfigSectionResponse) GetSection() *structpb.Struct { + if x != nil { + return x.Section + } + return nil +} + +func (x *GetProjectConfigSectionResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +// Request message for GetConfigValue +type GetProjectConfigValueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectConfigValueRequest) Reset() { + *x = GetProjectConfigValueRequest{} + mi := &file_project_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectConfigValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectConfigValueRequest) ProtoMessage() {} + +func (x *GetProjectConfigValueRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProjectConfigValueRequest.ProtoReflect.Descriptor instead. +func (*GetProjectConfigValueRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{4} +} + +func (x *GetProjectConfigValueRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Response message for GetConfigValue +type GetProjectConfigValueResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *structpb.Value `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectConfigValueResponse) Reset() { + *x = GetProjectConfigValueResponse{} + mi := &file_project_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectConfigValueResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectConfigValueResponse) ProtoMessage() {} + +func (x *GetProjectConfigValueResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[5] + 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 GetProjectConfigValueResponse.ProtoReflect.Descriptor instead. +func (*GetProjectConfigValueResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{5} +} + +func (x *GetProjectConfigValueResponse) GetValue() *structpb.Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *GetProjectConfigValueResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +// Request message for SetConfigSection +type SetProjectConfigSectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Section *structpb.Struct `protobuf:"bytes,2,opt,name=section,proto3" json:"section,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetProjectConfigSectionRequest) Reset() { + *x = SetProjectConfigSectionRequest{} + mi := &file_project_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetProjectConfigSectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetProjectConfigSectionRequest) ProtoMessage() {} + +func (x *SetProjectConfigSectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[6] + 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 SetProjectConfigSectionRequest.ProtoReflect.Descriptor instead. +func (*SetProjectConfigSectionRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{6} +} + +func (x *SetProjectConfigSectionRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *SetProjectConfigSectionRequest) GetSection() *structpb.Struct { + if x != nil { + return x.Section + } + return nil +} + +// Request message for SetConfigValue +type SetProjectConfigValueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Value *structpb.Value `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetProjectConfigValueRequest) Reset() { + *x = SetProjectConfigValueRequest{} + mi := &file_project_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetProjectConfigValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetProjectConfigValueRequest) ProtoMessage() {} + +func (x *SetProjectConfigValueRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[7] + 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 SetProjectConfigValueRequest.ProtoReflect.Descriptor instead. +func (*SetProjectConfigValueRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{7} +} + +func (x *SetProjectConfigValueRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *SetProjectConfigValueRequest) GetValue() *structpb.Value { + if x != nil { + return x.Value + } + return nil +} + +// Request message for UnsetConfig +type UnsetProjectConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnsetProjectConfigRequest) Reset() { + *x = UnsetProjectConfigRequest{} + mi := &file_project_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetProjectConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetProjectConfigRequest) ProtoMessage() {} + +func (x *UnsetProjectConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[8] + 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 UnsetProjectConfigRequest.ProtoReflect.Descriptor instead. +func (*UnsetProjectConfigRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{8} +} + +func (x *UnsetProjectConfigRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Request message for GetServiceConfigSection +type GetServiceConfigSectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceConfigSectionRequest) Reset() { + *x = GetServiceConfigSectionRequest{} + mi := &file_project_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceConfigSectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceConfigSectionRequest) ProtoMessage() {} + +func (x *GetServiceConfigSectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[9] + 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 GetServiceConfigSectionRequest.ProtoReflect.Descriptor instead. +func (*GetServiceConfigSectionRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{9} +} + +func (x *GetServiceConfigSectionRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *GetServiceConfigSectionRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Response message for GetServiceConfigSection +type GetServiceConfigSectionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Section *structpb.Struct `protobuf:"bytes,1,opt,name=section,proto3" json:"section,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceConfigSectionResponse) Reset() { + *x = GetServiceConfigSectionResponse{} + mi := &file_project_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceConfigSectionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceConfigSectionResponse) ProtoMessage() {} + +func (x *GetServiceConfigSectionResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[10] + 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 GetServiceConfigSectionResponse.ProtoReflect.Descriptor instead. +func (*GetServiceConfigSectionResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{10} +} + +func (x *GetServiceConfigSectionResponse) GetSection() *structpb.Struct { + if x != nil { + return x.Section + } + return nil +} + +func (x *GetServiceConfigSectionResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +// Request message for GetServiceConfigValue +type GetServiceConfigValueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceConfigValueRequest) Reset() { + *x = GetServiceConfigValueRequest{} + mi := &file_project_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceConfigValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceConfigValueRequest) ProtoMessage() {} + +func (x *GetServiceConfigValueRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[11] + 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 GetServiceConfigValueRequest.ProtoReflect.Descriptor instead. +func (*GetServiceConfigValueRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{11} +} + +func (x *GetServiceConfigValueRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *GetServiceConfigValueRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Response message for GetServiceConfigValue +type GetServiceConfigValueResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *structpb.Value `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceConfigValueResponse) Reset() { + *x = GetServiceConfigValueResponse{} + mi := &file_project_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceConfigValueResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceConfigValueResponse) ProtoMessage() {} + +func (x *GetServiceConfigValueResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[12] + 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 GetServiceConfigValueResponse.ProtoReflect.Descriptor instead. +func (*GetServiceConfigValueResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{12} +} + +func (x *GetServiceConfigValueResponse) GetValue() *structpb.Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *GetServiceConfigValueResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +// Request message for SetServiceConfigSection +type SetServiceConfigSectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Section *structpb.Struct `protobuf:"bytes,3,opt,name=section,proto3" json:"section,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetServiceConfigSectionRequest) Reset() { + *x = SetServiceConfigSectionRequest{} + mi := &file_project_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetServiceConfigSectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetServiceConfigSectionRequest) ProtoMessage() {} + +func (x *SetServiceConfigSectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[13] + 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 SetServiceConfigSectionRequest.ProtoReflect.Descriptor instead. +func (*SetServiceConfigSectionRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{13} +} + +func (x *SetServiceConfigSectionRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *SetServiceConfigSectionRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *SetServiceConfigSectionRequest) GetSection() *structpb.Struct { + if x != nil { + return x.Section + } + return nil +} + +// Request message for SetServiceConfigValue +type SetServiceConfigValueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Value *structpb.Value `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetServiceConfigValueRequest) Reset() { + *x = SetServiceConfigValueRequest{} + mi := &file_project_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetServiceConfigValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetServiceConfigValueRequest) ProtoMessage() {} + +func (x *SetServiceConfigValueRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[14] + 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 SetServiceConfigValueRequest.ProtoReflect.Descriptor instead. +func (*SetServiceConfigValueRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{14} +} + +func (x *SetServiceConfigValueRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *SetServiceConfigValueRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *SetServiceConfigValueRequest) GetValue() *structpb.Value { + if x != nil { + return x.Value + } + return nil +} + +// Request message for UnsetServiceConfig +type UnsetServiceConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnsetServiceConfigRequest) Reset() { + *x = UnsetServiceConfigRequest{} + mi := &file_project_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetServiceConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetServiceConfigRequest) ProtoMessage() {} + +func (x *UnsetServiceConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[15] + 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 UnsetServiceConfigRequest.ProtoReflect.Descriptor instead. +func (*UnsetServiceConfigRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{15} +} + +func (x *UnsetServiceConfigRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *UnsetServiceConfigRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + var File_project_proto protoreflect.FileDescriptor const file_project_proto_rawDesc = "" + "\n" + - "\rproject.proto\x12\x06azdext\x1a\fmodels.proto\"E\n" + + "\rproject.proto\x12\x06azdext\x1a\fmodels.proto\x1a$include/google/protobuf/struct.proto\"E\n" + "\x12GetProjectResponse\x12/\n" + "\aproject\x18\x01 \x01(\v2\x15.azdext.ProjectConfigR\aproject\"D\n" + "\x11AddServiceRequest\x12/\n" + - "\aservice\x18\x01 \x01(\v2\x15.azdext.ServiceConfigR\aservice2\x89\x01\n" + + "\aservice\x18\x01 \x01(\v2\x15.azdext.ServiceConfigR\aservice\"4\n" + + "\x1eGetProjectConfigSectionRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\"j\n" + + "\x1fGetProjectConfigSectionResponse\x121\n" + + "\asection\x18\x01 \x01(\v2\x17.google.protobuf.StructR\asection\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"2\n" + + "\x1cGetProjectConfigValueRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\"c\n" + + "\x1dGetProjectConfigValueResponse\x12,\n" + + "\x05value\x18\x01 \x01(\v2\x16.google.protobuf.ValueR\x05value\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"g\n" + + "\x1eSetProjectConfigSectionRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x121\n" + + "\asection\x18\x02 \x01(\v2\x17.google.protobuf.StructR\asection\"`\n" + + "\x1cSetProjectConfigValueRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12,\n" + + "\x05value\x18\x02 \x01(\v2\x16.google.protobuf.ValueR\x05value\"/\n" + + "\x19UnsetProjectConfigRequest\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\"W\n" + + "\x1eGetServiceConfigSectionRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"j\n" + + "\x1fGetServiceConfigSectionResponse\x121\n" + + "\asection\x18\x01 \x01(\v2\x17.google.protobuf.StructR\asection\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"U\n" + + "\x1cGetServiceConfigValueRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"c\n" + + "\x1dGetServiceConfigValueResponse\x12,\n" + + "\x05value\x18\x01 \x01(\v2\x16.google.protobuf.ValueR\x05value\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"\x8a\x01\n" + + "\x1eSetServiceConfigSectionRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x121\n" + + "\asection\x18\x03 \x01(\v2\x17.google.protobuf.StructR\asection\"\x83\x01\n" + + "\x1cSetServiceConfigValueRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12,\n" + + "\x05value\x18\x03 \x01(\v2\x16.google.protobuf.ValueR\x05value\"R\n" + + "\x19UnsetServiceConfigRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path2\x8a\b\n" + "\x0eProjectService\x127\n" + "\x03Get\x12\x14.azdext.EmptyRequest\x1a\x1a.azdext.GetProjectResponse\x12>\n" + "\n" + - "AddService\x12\x19.azdext.AddServiceRequest\x1a\x15.azdext.EmptyResponseB/Z-gitproxy.zycloud.tk/azure/azure-dev/cli/azd/pkg/azdextb\x06proto3" + "AddService\x12\x19.azdext.AddServiceRequest\x1a\x15.azdext.EmptyResponse\x12c\n" + + "\x10GetConfigSection\x12&.azdext.GetProjectConfigSectionRequest\x1a'.azdext.GetProjectConfigSectionResponse\x12]\n" + + "\x0eGetConfigValue\x12$.azdext.GetProjectConfigValueRequest\x1a%.azdext.GetProjectConfigValueResponse\x12Q\n" + + "\x10SetConfigSection\x12&.azdext.SetProjectConfigSectionRequest\x1a\x15.azdext.EmptyResponse\x12M\n" + + "\x0eSetConfigValue\x12$.azdext.SetProjectConfigValueRequest\x1a\x15.azdext.EmptyResponse\x12G\n" + + "\vUnsetConfig\x12!.azdext.UnsetProjectConfigRequest\x1a\x15.azdext.EmptyResponse\x12j\n" + + "\x17GetServiceConfigSection\x12&.azdext.GetServiceConfigSectionRequest\x1a'.azdext.GetServiceConfigSectionResponse\x12d\n" + + "\x15GetServiceConfigValue\x12$.azdext.GetServiceConfigValueRequest\x1a%.azdext.GetServiceConfigValueResponse\x12X\n" + + "\x17SetServiceConfigSection\x12&.azdext.SetServiceConfigSectionRequest\x1a\x15.azdext.EmptyResponse\x12T\n" + + "\x15SetServiceConfigValue\x12$.azdext.SetServiceConfigValueRequest\x1a\x15.azdext.EmptyResponse\x12N\n" + + "\x12UnsetServiceConfig\x12!.azdext.UnsetServiceConfigRequest\x1a\x15.azdext.EmptyResponseB/Z-gitproxy.zycloud.tk/azure/azure-dev/cli/azd/pkg/azdextb\x06proto3" var ( file_project_proto_rawDescOnce sync.Once @@ -140,27 +926,71 @@ func file_project_proto_rawDescGZIP() []byte { return file_project_proto_rawDescData } -var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_project_proto_goTypes = []any{ - (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse - (*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 + (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse + (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest + (*GetProjectConfigSectionRequest)(nil), // 2: azdext.GetProjectConfigSectionRequest + (*GetProjectConfigSectionResponse)(nil), // 3: azdext.GetProjectConfigSectionResponse + (*GetProjectConfigValueRequest)(nil), // 4: azdext.GetProjectConfigValueRequest + (*GetProjectConfigValueResponse)(nil), // 5: azdext.GetProjectConfigValueResponse + (*SetProjectConfigSectionRequest)(nil), // 6: azdext.SetProjectConfigSectionRequest + (*SetProjectConfigValueRequest)(nil), // 7: azdext.SetProjectConfigValueRequest + (*UnsetProjectConfigRequest)(nil), // 8: azdext.UnsetProjectConfigRequest + (*GetServiceConfigSectionRequest)(nil), // 9: azdext.GetServiceConfigSectionRequest + (*GetServiceConfigSectionResponse)(nil), // 10: azdext.GetServiceConfigSectionResponse + (*GetServiceConfigValueRequest)(nil), // 11: azdext.GetServiceConfigValueRequest + (*GetServiceConfigValueResponse)(nil), // 12: azdext.GetServiceConfigValueResponse + (*SetServiceConfigSectionRequest)(nil), // 13: azdext.SetServiceConfigSectionRequest + (*SetServiceConfigValueRequest)(nil), // 14: azdext.SetServiceConfigValueRequest + (*UnsetServiceConfigRequest)(nil), // 15: azdext.UnsetServiceConfigRequest + (*ProjectConfig)(nil), // 16: azdext.ProjectConfig + (*ServiceConfig)(nil), // 17: azdext.ServiceConfig + (*structpb.Struct)(nil), // 18: google.protobuf.Struct + (*structpb.Value)(nil), // 19: google.protobuf.Value + (*EmptyRequest)(nil), // 20: azdext.EmptyRequest + (*EmptyResponse)(nil), // 21: azdext.EmptyResponse } var file_project_proto_depIdxs = []int32{ - 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 + 16, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig + 17, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig + 18, // 2: azdext.GetProjectConfigSectionResponse.section:type_name -> google.protobuf.Struct + 19, // 3: azdext.GetProjectConfigValueResponse.value:type_name -> google.protobuf.Value + 18, // 4: azdext.SetProjectConfigSectionRequest.section:type_name -> google.protobuf.Struct + 19, // 5: azdext.SetProjectConfigValueRequest.value:type_name -> google.protobuf.Value + 18, // 6: azdext.GetServiceConfigSectionResponse.section:type_name -> google.protobuf.Struct + 19, // 7: azdext.GetServiceConfigValueResponse.value:type_name -> google.protobuf.Value + 18, // 8: azdext.SetServiceConfigSectionRequest.section:type_name -> google.protobuf.Struct + 19, // 9: azdext.SetServiceConfigValueRequest.value:type_name -> google.protobuf.Value + 20, // 10: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest + 1, // 11: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest + 2, // 12: azdext.ProjectService.GetConfigSection:input_type -> azdext.GetProjectConfigSectionRequest + 4, // 13: azdext.ProjectService.GetConfigValue:input_type -> azdext.GetProjectConfigValueRequest + 6, // 14: azdext.ProjectService.SetConfigSection:input_type -> azdext.SetProjectConfigSectionRequest + 7, // 15: azdext.ProjectService.SetConfigValue:input_type -> azdext.SetProjectConfigValueRequest + 8, // 16: azdext.ProjectService.UnsetConfig:input_type -> azdext.UnsetProjectConfigRequest + 9, // 17: azdext.ProjectService.GetServiceConfigSection:input_type -> azdext.GetServiceConfigSectionRequest + 11, // 18: azdext.ProjectService.GetServiceConfigValue:input_type -> azdext.GetServiceConfigValueRequest + 13, // 19: azdext.ProjectService.SetServiceConfigSection:input_type -> azdext.SetServiceConfigSectionRequest + 14, // 20: azdext.ProjectService.SetServiceConfigValue:input_type -> azdext.SetServiceConfigValueRequest + 15, // 21: azdext.ProjectService.UnsetServiceConfig:input_type -> azdext.UnsetServiceConfigRequest + 0, // 22: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse + 21, // 23: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse + 3, // 24: azdext.ProjectService.GetConfigSection:output_type -> azdext.GetProjectConfigSectionResponse + 5, // 25: azdext.ProjectService.GetConfigValue:output_type -> azdext.GetProjectConfigValueResponse + 21, // 26: azdext.ProjectService.SetConfigSection:output_type -> azdext.EmptyResponse + 21, // 27: azdext.ProjectService.SetConfigValue:output_type -> azdext.EmptyResponse + 21, // 28: azdext.ProjectService.UnsetConfig:output_type -> azdext.EmptyResponse + 10, // 29: azdext.ProjectService.GetServiceConfigSection:output_type -> azdext.GetServiceConfigSectionResponse + 12, // 30: azdext.ProjectService.GetServiceConfigValue:output_type -> azdext.GetServiceConfigValueResponse + 21, // 31: azdext.ProjectService.SetServiceConfigSection:output_type -> azdext.EmptyResponse + 21, // 32: azdext.ProjectService.SetServiceConfigValue:output_type -> azdext.EmptyResponse + 21, // 33: azdext.ProjectService.UnsetServiceConfig:output_type -> azdext.EmptyResponse + 22, // [22:34] is the sub-list for method output_type + 10, // [10:22] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_project_proto_init() } @@ -175,7 +1005,7 @@ func file_project_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_project_proto_rawDesc), len(file_project_proto_rawDesc)), NumEnums: 0, - NumMessages: 2, + NumMessages: 16, 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 95824d4a316..552c7c0c64a 100644 --- a/cli/azd/pkg/azdext/project_grpc.pb.go +++ b/cli/azd/pkg/azdext/project_grpc.pb.go @@ -22,8 +22,18 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" - ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" + ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" + ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" + ProjectService_GetConfigSection_FullMethodName = "/azdext.ProjectService/GetConfigSection" + ProjectService_GetConfigValue_FullMethodName = "/azdext.ProjectService/GetConfigValue" + ProjectService_SetConfigSection_FullMethodName = "/azdext.ProjectService/SetConfigSection" + ProjectService_SetConfigValue_FullMethodName = "/azdext.ProjectService/SetConfigValue" + ProjectService_UnsetConfig_FullMethodName = "/azdext.ProjectService/UnsetConfig" + ProjectService_GetServiceConfigSection_FullMethodName = "/azdext.ProjectService/GetServiceConfigSection" + ProjectService_GetServiceConfigValue_FullMethodName = "/azdext.ProjectService/GetServiceConfigValue" + ProjectService_SetServiceConfigSection_FullMethodName = "/azdext.ProjectService/SetServiceConfigSection" + ProjectService_SetServiceConfigValue_FullMethodName = "/azdext.ProjectService/SetServiceConfigValue" + ProjectService_UnsetServiceConfig_FullMethodName = "/azdext.ProjectService/UnsetServiceConfig" ) // ProjectServiceClient is the client API for ProjectService service. @@ -36,6 +46,26 @@ type ProjectServiceClient interface { Get(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) // AddService adds a new service to the project. AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Gets a configuration section by path. + GetConfigSection(ctx context.Context, in *GetProjectConfigSectionRequest, opts ...grpc.CallOption) (*GetProjectConfigSectionResponse, error) + // Gets a configuration value by path. + GetConfigValue(ctx context.Context, in *GetProjectConfigValueRequest, opts ...grpc.CallOption) (*GetProjectConfigValueResponse, error) + // Sets a configuration section by path. + SetConfigSection(ctx context.Context, in *SetProjectConfigSectionRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Sets a configuration value by path. + SetConfigValue(ctx context.Context, in *SetProjectConfigValueRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Removes configuration by path. + UnsetConfig(ctx context.Context, in *UnsetProjectConfigRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Gets a service configuration section by path. + GetServiceConfigSection(ctx context.Context, in *GetServiceConfigSectionRequest, opts ...grpc.CallOption) (*GetServiceConfigSectionResponse, error) + // Gets a service configuration value by path. + GetServiceConfigValue(ctx context.Context, in *GetServiceConfigValueRequest, opts ...grpc.CallOption) (*GetServiceConfigValueResponse, error) + // Sets a service configuration section by path. + SetServiceConfigSection(ctx context.Context, in *SetServiceConfigSectionRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Sets a service configuration value by path. + SetServiceConfigValue(ctx context.Context, in *SetServiceConfigValueRequest, opts ...grpc.CallOption) (*EmptyResponse, error) + // Removes service configuration by path. + UnsetServiceConfig(ctx context.Context, in *UnsetServiceConfigRequest, opts ...grpc.CallOption) (*EmptyResponse, error) } type projectServiceClient struct { @@ -66,6 +96,106 @@ func (c *projectServiceClient) AddService(ctx context.Context, in *AddServiceReq return out, nil } +func (c *projectServiceClient) GetConfigSection(ctx context.Context, in *GetProjectConfigSectionRequest, opts ...grpc.CallOption) (*GetProjectConfigSectionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProjectConfigSectionResponse) + err := c.cc.Invoke(ctx, ProjectService_GetConfigSection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) GetConfigValue(ctx context.Context, in *GetProjectConfigValueRequest, opts ...grpc.CallOption) (*GetProjectConfigValueResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProjectConfigValueResponse) + err := c.cc.Invoke(ctx, ProjectService_GetConfigValue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) SetConfigSection(ctx context.Context, in *SetProjectConfigSectionRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_SetConfigSection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) SetConfigValue(ctx context.Context, in *SetProjectConfigValueRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_SetConfigValue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) UnsetConfig(ctx context.Context, in *UnsetProjectConfigRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_UnsetConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) GetServiceConfigSection(ctx context.Context, in *GetServiceConfigSectionRequest, opts ...grpc.CallOption) (*GetServiceConfigSectionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetServiceConfigSectionResponse) + err := c.cc.Invoke(ctx, ProjectService_GetServiceConfigSection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) GetServiceConfigValue(ctx context.Context, in *GetServiceConfigValueRequest, opts ...grpc.CallOption) (*GetServiceConfigValueResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetServiceConfigValueResponse) + err := c.cc.Invoke(ctx, ProjectService_GetServiceConfigValue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) SetServiceConfigSection(ctx context.Context, in *SetServiceConfigSectionRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_SetServiceConfigSection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) SetServiceConfigValue(ctx context.Context, in *SetServiceConfigValueRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_SetServiceConfigValue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) UnsetServiceConfig(ctx context.Context, in *UnsetServiceConfigRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EmptyResponse) + err := c.cc.Invoke(ctx, ProjectService_UnsetServiceConfig_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. @@ -76,6 +206,26 @@ type ProjectServiceServer interface { Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) // AddService adds a new service to the project. AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) + // Gets a configuration section by path. + GetConfigSection(context.Context, *GetProjectConfigSectionRequest) (*GetProjectConfigSectionResponse, error) + // Gets a configuration value by path. + GetConfigValue(context.Context, *GetProjectConfigValueRequest) (*GetProjectConfigValueResponse, error) + // Sets a configuration section by path. + SetConfigSection(context.Context, *SetProjectConfigSectionRequest) (*EmptyResponse, error) + // Sets a configuration value by path. + SetConfigValue(context.Context, *SetProjectConfigValueRequest) (*EmptyResponse, error) + // Removes configuration by path. + UnsetConfig(context.Context, *UnsetProjectConfigRequest) (*EmptyResponse, error) + // Gets a service configuration section by path. + GetServiceConfigSection(context.Context, *GetServiceConfigSectionRequest) (*GetServiceConfigSectionResponse, error) + // Gets a service configuration value by path. + GetServiceConfigValue(context.Context, *GetServiceConfigValueRequest) (*GetServiceConfigValueResponse, error) + // Sets a service configuration section by path. + SetServiceConfigSection(context.Context, *SetServiceConfigSectionRequest) (*EmptyResponse, error) + // Sets a service configuration value by path. + SetServiceConfigValue(context.Context, *SetServiceConfigValueRequest) (*EmptyResponse, error) + // Removes service configuration by path. + UnsetServiceConfig(context.Context, *UnsetServiceConfigRequest) (*EmptyResponse, error) mustEmbedUnimplementedProjectServiceServer() } @@ -92,6 +242,36 @@ func (UnimplementedProjectServiceServer) Get(context.Context, *EmptyRequest) (*G func (UnimplementedProjectServiceServer) AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddService not implemented") } +func (UnimplementedProjectServiceServer) GetConfigSection(context.Context, *GetProjectConfigSectionRequest) (*GetProjectConfigSectionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetConfigSection not implemented") +} +func (UnimplementedProjectServiceServer) GetConfigValue(context.Context, *GetProjectConfigValueRequest) (*GetProjectConfigValueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetConfigValue not implemented") +} +func (UnimplementedProjectServiceServer) SetConfigSection(context.Context, *SetProjectConfigSectionRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetConfigSection not implemented") +} +func (UnimplementedProjectServiceServer) SetConfigValue(context.Context, *SetProjectConfigValueRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetConfigValue not implemented") +} +func (UnimplementedProjectServiceServer) UnsetConfig(context.Context, *UnsetProjectConfigRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnsetConfig not implemented") +} +func (UnimplementedProjectServiceServer) GetServiceConfigSection(context.Context, *GetServiceConfigSectionRequest) (*GetServiceConfigSectionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServiceConfigSection not implemented") +} +func (UnimplementedProjectServiceServer) GetServiceConfigValue(context.Context, *GetServiceConfigValueRequest) (*GetServiceConfigValueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServiceConfigValue not implemented") +} +func (UnimplementedProjectServiceServer) SetServiceConfigSection(context.Context, *SetServiceConfigSectionRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetServiceConfigSection not implemented") +} +func (UnimplementedProjectServiceServer) SetServiceConfigValue(context.Context, *SetServiceConfigValueRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetServiceConfigValue not implemented") +} +func (UnimplementedProjectServiceServer) UnsetServiceConfig(context.Context, *UnsetServiceConfigRequest) (*EmptyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnsetServiceConfig not implemented") +} func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} @@ -149,6 +329,186 @@ func _ProjectService_AddService_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _ProjectService_GetConfigSection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProjectConfigSectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetConfigSection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetConfigSection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetConfigSection(ctx, req.(*GetProjectConfigSectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_GetConfigValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProjectConfigValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetConfigValue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetConfigValue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetConfigValue(ctx, req.(*GetProjectConfigValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_SetConfigSection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetProjectConfigSectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).SetConfigSection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_SetConfigSection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).SetConfigSection(ctx, req.(*SetProjectConfigSectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_SetConfigValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetProjectConfigValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).SetConfigValue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_SetConfigValue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).SetConfigValue(ctx, req.(*SetProjectConfigValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_UnsetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnsetProjectConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).UnsetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_UnsetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).UnsetConfig(ctx, req.(*UnsetProjectConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_GetServiceConfigSection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceConfigSectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetServiceConfigSection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetServiceConfigSection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetServiceConfigSection(ctx, req.(*GetServiceConfigSectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_GetServiceConfigValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceConfigValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetServiceConfigValue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetServiceConfigValue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetServiceConfigValue(ctx, req.(*GetServiceConfigValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_SetServiceConfigSection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetServiceConfigSectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).SetServiceConfigSection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_SetServiceConfigSection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).SetServiceConfigSection(ctx, req.(*SetServiceConfigSectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_SetServiceConfigValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetServiceConfigValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).SetServiceConfigValue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_SetServiceConfigValue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).SetServiceConfigValue(ctx, req.(*SetServiceConfigValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_UnsetServiceConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnsetServiceConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).UnsetServiceConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_UnsetServiceConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).UnsetServiceConfig(ctx, req.(*UnsetServiceConfigRequest)) + } + 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) @@ -164,6 +524,46 @@ var ProjectService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AddService", Handler: _ProjectService_AddService_Handler, }, + { + MethodName: "GetConfigSection", + Handler: _ProjectService_GetConfigSection_Handler, + }, + { + MethodName: "GetConfigValue", + Handler: _ProjectService_GetConfigValue_Handler, + }, + { + MethodName: "SetConfigSection", + Handler: _ProjectService_SetConfigSection_Handler, + }, + { + MethodName: "SetConfigValue", + Handler: _ProjectService_SetConfigValue_Handler, + }, + { + MethodName: "UnsetConfig", + Handler: _ProjectService_UnsetConfig_Handler, + }, + { + MethodName: "GetServiceConfigSection", + Handler: _ProjectService_GetServiceConfigSection_Handler, + }, + { + MethodName: "GetServiceConfigValue", + Handler: _ProjectService_GetServiceConfigValue_Handler, + }, + { + MethodName: "SetServiceConfigSection", + Handler: _ProjectService_SetServiceConfigSection_Handler, + }, + { + MethodName: "SetServiceConfigValue", + Handler: _ProjectService_SetServiceConfigValue_Handler, + }, + { + MethodName: "UnsetServiceConfig", + Handler: _ProjectService_UnsetServiceConfig_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "project.proto", From e5976697e1c9b5123c7c6c9b2b9f5e865686f81f Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 14:57:26 -0800 Subject: [PATCH 3/6] Enable additional properties at project root and services --- schemas/alpha/azure.yaml.json | 4 ++-- schemas/v1.0/azure.yaml.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index fbf870a4936..6f0393032eb 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -5,7 +5,7 @@ "required": [ "name" ], - "additionalProperties": false, + "additionalProperties": true, "properties": { "name": { "type": "string", @@ -159,7 +159,7 @@ "minProperties": 1, "additionalProperties": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "required": [ "host" ], diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 641784fa78c..2c1159a3aed 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -5,7 +5,7 @@ "required": [ "name" ], - "additionalProperties": false, + "additionalProperties": true, "properties": { "name": { "type": "string", @@ -66,7 +66,7 @@ "minProperties": 1, "additionalProperties": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "required": [ "host" ], From b8c974dff39c87a35cc680cbd1aca2c92c63a63c Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 15:24:58 -0800 Subject: [PATCH 4/6] Adds example of using project/service config --- .../microsoft.azd.demo/internal/cmd/config.go | 316 ++++++++++++++++++ .../microsoft.azd.demo/internal/cmd/root.go | 1 + 2 files changed, 317 insertions(+) create mode 100644 cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go new file mode 100644 index 00000000000..56dba8173e7 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/fatih/color" + "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/structpb" +) + +// MonitoringConfig represents the project-level monitoring configuration +type MonitoringConfig struct { + Enabled bool `json:"enabled"` + Environment string `json:"environment"` + RetentionDays int `json:"retentionDays"` + AlertEmail string `json:"alertEmail"` +} + +// ServiceMonitoringConfig represents service-level monitoring configuration +type ServiceMonitoringConfig struct { + Enabled bool `json:"enabled"` + HealthCheckPath string `json:"healthCheckPath"` + MetricsPort int `json:"metricsPort"` + LogLevel string `json:"logLevel"` + AlertThresholds struct { + ErrorRate float64 `json:"errorRate"` + ResponseTimeMs int `json:"responseTimeMs"` + CPUPercent int `json:"cpuPercent"` + } `json:"alertThresholds"` + Tags []string `json:"tags"` +} + +func newConfigCommand() *cobra.Command { + return &cobra.Command{ + Use: "config", + Short: "Setup monitoring configuration for the project and services", + Long: `This command demonstrates the new configuration management capabilities by setting up +a realistic monitoring configuration scenario. It will: + +1. Check if project-level monitoring config exists, create it if missing +2. Find the first service in the project +3. Check if service-level monitoring config exists, create it if missing +4. Display the final configuration state + +This showcases how extensions can manage both project and service-level configuration +using the new AdditionalProperties gRPC API with strongly-typed Go structs.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := azdext.WithAccessToken(cmd.Context()) + + azdClient, err := azdext.NewAzdClient() + if err != nil { + return fmt.Errorf("failed to create azd client: %w", err) + } + defer azdClient.Close() + + return setupMonitoringConfig(ctx, azdClient) + }, + } +} + +func setupMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient) error { + color.HiCyan("🔧 Setting up monitoring configuration...") + fmt.Println() + + // Step 1: Check and setup project-level monitoring config + projectConfigCreated, err := setupProjectMonitoringConfig(ctx, azdClient) + if err != nil { + return err + } + + // Step 2: Get project to find services + projectResp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("failed to get project: %w", err) + } + + if len(projectResp.Project.Services) == 0 { + color.Yellow("⚠️ No services found in project - skipping service configuration") + return displayConfigurationSummary(ctx, azdClient, "", projectConfigCreated, false) + } + + // Step 3: Setup monitoring for the first service + var firstServiceName string + for serviceName := range projectResp.Project.Services { + firstServiceName = serviceName + break + } + + color.HiWhite("📦 Found service: %s", firstServiceName) + serviceConfigCreated, err := setupServiceMonitoringConfig(ctx, azdClient, firstServiceName) + if err != nil { + return err + } + + // Step 4: Display final configuration state + return displayConfigurationSummary(ctx, azdClient, firstServiceName, projectConfigCreated, serviceConfigCreated) +} + +// Helper functions to convert between type-safe structs and protobuf structs +func structToProtobuf(v interface{}) (*structpb.Struct, error) { + // Convert struct to JSON bytes + jsonBytes, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("failed to marshal struct to JSON: %w", err) + } + + // Convert JSON bytes to map + var m map[string]interface{} + if err := json.Unmarshal(jsonBytes, &m); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err) + } + + // Convert map to protobuf struct + return structpb.NewStruct(m) +} + +func protobufToStruct(pbStruct *structpb.Struct, target interface{}) error { + // Convert protobuf struct to JSON bytes + jsonBytes, err := json.Marshal(pbStruct.AsMap()) + if err != nil { + return fmt.Errorf("failed to marshal protobuf struct to JSON: %w", err) + } + + // Unmarshal JSON into target struct + if err := json.Unmarshal(jsonBytes, target); err != nil { + return fmt.Errorf("failed to unmarshal JSON to target struct: %w", err) + } + + return nil +} + +func setupProjectMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient) (bool, error) { + color.HiWhite("🏢 Checking project-level monitoring configuration...") + + // Check if monitoring config already exists + configResp, err := azdClient.Project().GetConfigSection(ctx, &azdext.GetProjectConfigSectionRequest{ + Path: "monitoring", + }) + if err != nil { + return false, fmt.Errorf("failed to check project monitoring config: %w", err) + } + + if configResp.Found { + color.Green(" ✓ Project monitoring configuration already exists") + + // Demonstrate reading back the configuration into our type-safe struct + var existingConfig MonitoringConfig + if err := protobufToStruct(configResp.Section, &existingConfig); err != nil { + return false, fmt.Errorf("failed to convert existing config: %w", err) + } + color.Cyan(" Current config: Environment=%s, Retention=%d days", + existingConfig.Environment, existingConfig.RetentionDays) + return false, nil // false means it already existed (not created) + } + + // Create default monitoring configuration using type-safe struct + color.Yellow(" ⚙️ Creating project monitoring configuration...") + + monitoringConfig := MonitoringConfig{ + Enabled: true, + Environment: "development", + RetentionDays: 30, + AlertEmail: "admin@company.com", + } + + // Convert type-safe struct to protobuf struct + configStruct, err := structToProtobuf(monitoringConfig) + if err != nil { + return false, fmt.Errorf("failed to convert config struct: %w", err) + } + + _, err = azdClient.Project().SetConfigSection(ctx, &azdext.SetProjectConfigSectionRequest{ + Path: "monitoring", + Section: configStruct, + }) + if err != nil { + return false, fmt.Errorf("failed to set project monitoring config: %w", err) + } + + color.Green(" ✓ Project monitoring configuration created successfully") + return true, nil // true means it was created +} + +func setupServiceMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient, serviceName string) (bool, error) { + color.HiWhite("🔍 Checking service-level monitoring configuration for '%s'...", serviceName) + + // Check if service monitoring config already exists + configResp, err := azdClient.Project().GetServiceConfigSection(ctx, &azdext.GetServiceConfigSectionRequest{ + ServiceName: serviceName, + Path: "monitoring", + }) + if err != nil { + return false, fmt.Errorf("failed to check service monitoring config: %w", err) + } + + if configResp.Found { + color.Green(" ✓ Service monitoring configuration already exists") + + // Demonstrate reading back the configuration into our type-safe struct + var existingConfig ServiceMonitoringConfig + if err := protobufToStruct(configResp.Section, &existingConfig); err != nil { + return false, fmt.Errorf("failed to convert existing service config: %w", err) + } + color.Cyan(" Current config: Port=%d, LogLevel=%s, Tags=%v", + existingConfig.MetricsPort, existingConfig.LogLevel, existingConfig.Tags) + return false, nil // false means it already existed (not created) + } + + // Create default service monitoring configuration using type-safe struct + color.Yellow(" ⚙️ Creating service monitoring configuration...") + + serviceConfig := ServiceMonitoringConfig{ + Enabled: true, + HealthCheckPath: "/health", + MetricsPort: 9090, + LogLevel: "info", + Tags: []string{"web", "api", "production"}, + } + + // Set alert thresholds + serviceConfig.AlertThresholds.ErrorRate = 5.0 + serviceConfig.AlertThresholds.ResponseTimeMs = 2000 + serviceConfig.AlertThresholds.CPUPercent = 80 + + // Convert type-safe struct to protobuf struct + configStruct, err := structToProtobuf(serviceConfig) + if err != nil { + return false, fmt.Errorf("failed to create service config struct: %w", err) + } + + _, err = azdClient.Project().SetServiceConfigSection(ctx, &azdext.SetServiceConfigSectionRequest{ + ServiceName: serviceName, + Path: "monitoring", + Section: configStruct, + }) + if err != nil { + return false, fmt.Errorf("failed to set service monitoring config: %w", err) + } + + color.Green(" ✓ Service monitoring configuration created successfully") + return true, nil // true means it was created +} + +func displayConfigurationSummary(ctx context.Context, azdClient *azdext.AzdClient, serviceName string, projectConfigCreated, serviceConfigCreated bool) error { + fmt.Println() + color.HiCyan("📊 Configuration Summary") + color.HiCyan("========================") + fmt.Println() + + // Display project monitoring config with status + projectStatus := "📋 Already existed" + if projectConfigCreated { + projectStatus = "✨ Newly created" + } + color.HiWhite("🏢 Project Monitoring Configuration (%s):", projectStatus) + projectConfigResp, err := azdClient.Project().GetConfigSection(ctx, &azdext.GetProjectConfigSectionRequest{ + Path: "monitoring", + }) + if err != nil { + return fmt.Errorf("failed to get project monitoring config: %w", err) + } + + if projectConfigResp.Found { + if err := printConfigSection(projectConfigResp.Section.AsMap()); err != nil { + return err + } + } + + fmt.Println() + + // Display service monitoring config with status (only if we have a service) + if serviceName != "" { + serviceStatus := "📋 Already existed" + if serviceConfigCreated { + serviceStatus = "✨ Newly created" + } + color.HiWhite("📦 Service '%s' Monitoring Configuration (%s):", serviceName, serviceStatus) + serviceConfigResp, err := azdClient.Project().GetServiceConfigSection(ctx, &azdext.GetServiceConfigSectionRequest{ + ServiceName: serviceName, + Path: "monitoring", + }) + if err != nil { + return fmt.Errorf("failed to get service monitoring config: %w", err) + } + + if serviceConfigResp.Found { + if err := printConfigSection(serviceConfigResp.Section.AsMap()); err != nil { + return err + } + } + fmt.Println() + } + + color.HiGreen("✅ Monitoring configuration setup complete!") + fmt.Println() + color.HiBlue("💡 This demonstrates how extensions can manage both project and service-level") + color.HiBlue(" configuration using the new AdditionalProperties gRPC API with type-safe") + color.HiBlue(" Go structs for complex configuration scenarios.") + + return nil +} + +func printConfigSection(section map[string]interface{}) error { + jsonBytes, err := json.MarshalIndent(section, " ", " ") + if err != nil { + return fmt.Errorf("failed to format section: %w", err) + } + fmt.Printf(" %s\n", string(jsonBytes)) + return nil +} diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/root.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/root.go index bb41cbbeafa..3853f13f922 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/root.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/root.go @@ -27,6 +27,7 @@ func NewRootCommand() *cobra.Command { rootCmd.AddCommand(newColorsCommand()) rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newMcpCommand()) + rootCmd.AddCommand(newConfigCommand()) return rootCmd } From 638d10c644c7bd21f882854a0a755cfba06acbbd Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 15:25:10 -0800 Subject: [PATCH 5/6] Updates extension framework docs --- cli/azd/.github/copilot-instructions.md | 13 +++ cli/azd/docs/extension-framework.md | 103 +++++++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/cli/azd/.github/copilot-instructions.md b/cli/azd/.github/copilot-instructions.md index 6d204c38632..712486c59ec 100644 --- a/cli/azd/.github/copilot-instructions.md +++ b/cli/azd/.github/copilot-instructions.md @@ -96,6 +96,19 @@ All Go files must include Microsoft copyright header: // Licensed under the MIT License. ``` +## Extensions Development + +### Building Extensions +Extensions are located in `extensions/` directory and use the extension framework: +```bash +# Build and install extension (example using demo extension) +cd extensions/microsoft.azd.demo +azd x build + +# Test extension (using extension's namespace from extension.yaml) +azd demo +``` + ## MCP Tools Development ### Tool Pattern diff --git a/cli/azd/docs/extension-framework.md b/cli/azd/docs/extension-framework.md index 5efb52e2f22..b77b851a58f 100644 --- a/cli/azd/docs/extension-framework.md +++ b/cli/azd/docs/extension-framework.md @@ -1114,7 +1114,7 @@ The following are a list of available gRPC services for extension developer to i ### Project Service -This service manages project configuration retrieval and related operations. +This service manages project configuration retrieval and related operations, including project and service-level configuration management. > See [project.proto](../grpc/proto/project.proto) for more details. @@ -1141,6 +1141,107 @@ Adds a new service to the project. - `service`: _ServiceConfig_ - **Response:** _EmptyResponse_ +#### GetConfigSection + +Retrieves a project configuration section by path from AdditionalProperties. + +- **Request:** _GetProjectConfigSectionRequest_ + - `path` (string): Dot-notation path to the config section +- **Response:** _GetProjectConfigSectionResponse_ + - Contains: + - `section` (google.protobuf.Struct): The configuration section + - `found` (bool): Whether the section exists + +#### SetConfigSection + +Sets a project configuration section at the specified path in AdditionalProperties. + +- **Request:** _SetProjectConfigSectionRequest_ + - `path` (string): Dot-notation path to the config section + - `section` (google.protobuf.Struct): The configuration section to set +- **Response:** _EmptyResponse_ + +#### GetConfigValue + +Retrieves a single project configuration value by path from AdditionalProperties. + +- **Request:** _GetProjectConfigValueRequest_ + - `path` (string): Dot-notation path to the config value +- **Response:** _GetProjectConfigValueResponse_ + - Contains: + - `value` (google.protobuf.Value): The configuration value + - `found` (bool): Whether the value exists + +#### SetConfigValue + +Sets a single project configuration value at the specified path in AdditionalProperties. + +- **Request:** _SetProjectConfigValueRequest_ + - `path` (string): Dot-notation path to the config value + - `value` (google.protobuf.Value): The configuration value to set +- **Response:** _EmptyResponse_ + +#### UnsetConfig + +Removes a project configuration value or section at the specified path from AdditionalProperties. + +- **Request:** _UnsetProjectConfigRequest_ + - `path` (string): Dot-notation path to the config to remove +- **Response:** _EmptyResponse_ + +#### GetServiceConfigSection + +Retrieves a service configuration section by path from service AdditionalProperties. + +- **Request:** _GetServiceConfigSectionRequest_ + - `service_name` (string): Name of the service + - `path` (string): Dot-notation path to the config section +- **Response:** _GetServiceConfigSectionResponse_ + - Contains: + - `section` (google.protobuf.Struct): The configuration section + - `found` (bool): Whether the section exists + +#### SetServiceConfigSection + +Sets a service configuration section at the specified path in service AdditionalProperties. + +- **Request:** _SetServiceConfigSectionRequest_ + - `service_name` (string): Name of the service + - `path` (string): Dot-notation path to the config section + - `section` (google.protobuf.Struct): The configuration section to set +- **Response:** _EmptyResponse_ + +#### GetServiceConfigValue + +Retrieves a single service configuration value by path from service AdditionalProperties. + +- **Request:** _GetServiceConfigValueRequest_ + - `service_name` (string): Name of the service + - `path` (string): Dot-notation path to the config value +- **Response:** _GetServiceConfigValueResponse_ + - Contains: + - `value` (google.protobuf.Value): The configuration value + - `found` (bool): Whether the value exists + +#### SetServiceConfigValue + +Sets a single service configuration value at the specified path in service AdditionalProperties. + +- **Request:** _SetServiceConfigValueRequest_ + - `service_name` (string): Name of the service + - `path` (string): Dot-notation path to the config value + - `value` (google.protobuf.Value): The configuration value to set +- **Response:** _EmptyResponse_ + +#### UnsetServiceConfig + +Removes a service configuration value or section at the specified path from service AdditionalProperties. + +- **Request:** _UnsetServiceConfigRequest_ + - `service_name` (string): Name of the service + - `path` (string): Dot-notation path to the config to remove +- **Response:** _EmptyResponse_ + --- ### Environment Service From 1e33bc83515c7157b3d60362c5ecb14878a9542c Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 14 Nov 2025 15:33:36 -0800 Subject: [PATCH 6/6] Fixes lint issues --- .../extensions/microsoft.azd.demo/internal/cmd/config.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go index 56dba8173e7..51d6240cdcd 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/config.go @@ -247,7 +247,12 @@ func setupServiceMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClie return true, nil // true means it was created } -func displayConfigurationSummary(ctx context.Context, azdClient *azdext.AzdClient, serviceName string, projectConfigCreated, serviceConfigCreated bool) error { +func displayConfigurationSummary( + ctx context.Context, + azdClient *azdext.AzdClient, + serviceName string, + projectConfigCreated, serviceConfigCreated bool, +) error { fmt.Println() color.HiCyan("📊 Configuration Summary") color.HiCyan("========================")