Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions cli/azd/internal/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ type Resolver func(key string) string
type MapperFunc func(ctx context.Context, src any, dst any) error

type resolverKeyType struct{}
type envSubstKeyType struct{}

var resolverKey = resolverKeyType{}
var envSubstKey = envSubstKeyType{}

var (
registry = make(map[[2]reflect.Type]MapperFunc)
Expand All @@ -127,6 +129,36 @@ type Mapper struct {
// Default mapper instance for convenience functions
var defaultMapper = &Mapper{ctx: context.Background()}

// WithContext sets the context that will be passed to converters (ie, registered with
// [MustRegister]). This allows you to set and pass converter-specific settings per mapping call.
func WithContext(ctx context.Context) *Mapper {
return &Mapper{ctx: ctx}
}

// WithResolver returns a copy configured with an environment variable resolver.
func (m *Mapper) WithResolver(resolver Resolver) *Mapper {
if resolver == nil {
return &Mapper{ctx: m.ctx}
}

ctx := context.WithValue(m.ctx, resolverKey, func(key string) string {
return resolver(key)
})
return &Mapper{ctx: ctx}
}

// WithEnvSubst returns a copy configured to enable or disable environment substitution.
// Environment substitution is enabled by default.
func (m *Mapper) WithEnvSubst(enabled bool) *Mapper {
return &Mapper{ctx: context.WithValue(m.ctx, envSubstKey, enabled)}
}

// EnvSubstEnabled reports whether environment substitution is enabled for a conversion.
func EnvSubstEnabled(ctx context.Context) bool {
enabled, configured := ctx.Value(envSubstKey).(bool)
return !configured || enabled
}

// Register a type converter function that transforms type S to type T.
//
// The mapper framework uses reflection to automatically route conversion requests
Expand Down Expand Up @@ -280,14 +312,7 @@ func Convert(src any, dst any) error {
// return &azdext.Service{Image: expandedImage}, nil
// }
func WithResolver(resolver Resolver) *Mapper {
if resolver == nil {
return &Mapper{ctx: context.Background()}
}

ctx := context.WithValue(context.Background(), resolverKey, func(key string) string {
return resolver(key)
})
return &Mapper{ctx: ctx}
return WithContext(context.Background()).WithResolver(resolver)
}

// Convert performs type conversion using this mapper's context.
Expand Down
30 changes: 30 additions & 0 deletions cli/azd/internal/mapper/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,36 @@ func TestMappingWithoutResolver(t *testing.T) {
assert.True(t, result.Ready)
}

func TestMappingWithContext(t *testing.T) {
clearRegistry()

type conversionModeKey struct{}
Register(func(ctx context.Context, src string) (string, error) {
mode, _ := ctx.Value(conversionModeKey{}).(string)
return mode + ":" + src, nil
})

ctx := context.WithValue(t.Context(), conversionModeKey{}, "template")
var result string
err := WithContext(ctx).Convert("value", &result)

require.NoError(t, err)
assert.Equal(t, "template:value", result)
}

func TestMapperWithEnvSubst(t *testing.T) {
base := WithContext(t.Context())
require.True(t, EnvSubstEnabled(base.ctx))

disabled := base.WithResolver(func(key string) string {
return "resolved-" + key
}).WithEnvSubst(false)
require.False(t, EnvSubstEnabled(disabled.ctx))
require.Equal(t, "resolved-value", GetResolver(disabled.ctx)("value"))
require.True(t, EnvSubstEnabled(base.ctx))
require.Nil(t, GetResolver(base.ctx))
}

func TestMappingWithEnvironmentResolver(t *testing.T) {
clearRegistry()

Expand Down
9 changes: 9 additions & 0 deletions cli/azd/pkg/osutil/expandable_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import "fmt"
// It provides convenient methods for expanding all values in the map.
type ExpandableMap map[string]ExpandableString

// Raw returns all values without environment substitution.
func (em ExpandableMap) Raw() map[string]string {
result := make(map[string]string, len(em))
for key, value := range em {
result[key] = value.Raw()
}
return result
}

// Expand evaluates all ExpandableString values in the map, substituting variables as [ExpandableString.Envsubst] would.
// Returns a map[string]string with all values expanded, or an error if any expansion fails.
// The mapping parameter is a function that returns the value for a given variable name.
Expand Down
23 changes: 23 additions & 0 deletions cli/azd/pkg/osutil/expandable_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ import (
"github.com/stretchr/testify/require"
)

func TestExpandableMap_Raw(t *testing.T) {
values := ExpandableMap{
"TEMPLATE": NewExpandableString("${VALUE}"),
"STATIC": NewExpandableString("static-value"),
}

assert.Equal(t, map[string]string{
"TEMPLATE": "${VALUE}",
"STATIC": "static-value",
}, values.Raw())

// expand it, and you can still get the raw values back
_, err := values.Expand(func(s string) string {
return s + " EXPANDED"
})
require.NoError(t, err)

assert.Equal(t, map[string]string{
"TEMPLATE": "${VALUE}",
"STATIC": "static-value",
}, values.Raw())
}

func TestExpandableMap_Expand(t *testing.T) {
t.Run("EmptyMap", func(t *testing.T) {
em := ExpandableMap{}
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/pkg/osutil/expandable_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func (e ExpandableString) IsZero() bool {
return e.Empty()
}

// Raw returns the unexpanded template.
func (e ExpandableString) Raw() string {
return e.template
}

// Envsubst evaluates the template, substituting values as [envsubst.Eval] would.
func (e ExpandableString) Envsubst(mapping func(string) string) (string, error) {
return envsubst.Eval(e.template, mapping)
Expand Down
54 changes: 35 additions & 19 deletions cli/azd/pkg/project/mapper_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,36 @@ func registerProjectMappings() {
resolver := mapper.GetResolver(ctx)
envResolver := getEnvResolver(resolver)

resourceGroupName, err := src.ResourceGroupName.Envsubst(envResolver)
resourceGroupName, err := envsubstIfEnabled(ctx, src.ResourceGroupName, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst service resource group name: %w", err)
}

resourceName, err := src.ResourceName.Envsubst(envResolver)
resourceName, err := envsubstIfEnabled(ctx, src.ResourceName, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst service resource name: %w", err)
}

image, err := src.Image.Envsubst(envResolver)
image, err := envsubstIfEnabled(ctx, src.Image, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst image: %w", err)
}

var serviceEnv map[string]string
if len(src.Environment) > 0 {
serviceEnv, err = src.Environment.Expand(envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst service environment: %w", err)
if mapper.EnvSubstEnabled(ctx) {
serviceEnv, err = src.Environment.Expand(envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst service environment: %w", err)
}
} else {
serviceEnv = src.Environment.Raw()
}
}

// Convert Docker options
var docker *azdext.DockerProjectOptions
err = mapper.WithResolver(resolver).Convert(src.Docker, &docker)
err = mapper.WithContext(ctx).Convert(src.Docker, &docker)
if err != nil {
return nil, fmt.Errorf("convert docker options: %w", err)
}
Expand Down Expand Up @@ -176,24 +180,24 @@ func registerProjectMappings() {
resolver := mapper.GetResolver(ctx)
envResolver := getEnvResolver(resolver)

registry, err := src.Registry.Envsubst(envResolver)
registry, err := envsubstIfEnabled(ctx, src.Registry, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst docker registry: %w", err)
}

image, err := src.Image.Envsubst(envResolver)
image, err := envsubstIfEnabled(ctx, src.Image, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst docker image: %w", err)
}

tag, err := src.Tag.Envsubst(envResolver)
tag, err := envsubstIfEnabled(ctx, src.Tag, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst docker tag: %w", err)
}

buildArgs := []string{}
for _, arg := range src.BuildArgs {
resolvedArg, err := arg.Envsubst(envResolver)
resolvedArg, err := envsubstIfEnabled(ctx, arg, envResolver)
if err != nil {
return nil, fmt.Errorf("envsubst docker build arg '%s': %w", arg, err)
}
Expand Down Expand Up @@ -393,7 +397,7 @@ func registerProjectMappings() {
// Convert Docker options if present
if src.Docker != nil {
var dockerOptions DockerProjectOptions
err := mapper.Convert(src.Docker, &dockerOptions)
err := mapper.WithContext(ctx).Convert(src.Docker, &dockerOptions)
if err != nil {
return nil, fmt.Errorf("convert docker options: %w", err)
}
Expand All @@ -410,11 +414,12 @@ func registerProjectMappings() {

if len(src.Environment) > 0 {
result.Environment = make(osutil.ExpandableMap, len(src.Environment))
newEnvironmentValue := osutil.NewLiteralExpandableString
if !mapper.EnvSubstEnabled(ctx) {
newEnvironmentValue = osutil.NewExpandableString
}
for key, value := range src.Environment {
// Incoming values are expanded literals, not templates: escape them so a
// later expansion (or a round trip back into azure.yaml) cannot reinterpret
// or corrupt values containing `$`.
result.Environment[key] = osutil.NewLiteralExpandableString(value)
result.Environment[key] = newEnvironmentValue(value)
}
}

Expand Down Expand Up @@ -714,15 +719,15 @@ func registerProjectMappings() {
resolver := mapper.GetResolver(ctx)
envResolver := getEnvResolver(resolver)

resourceGroupName, err := src.ResourceGroupName.Envsubst(envResolver)
resourceGroupName, err := envsubstIfEnabled(ctx, src.ResourceGroupName, envResolver)
if err != nil {
return nil, fmt.Errorf("failed resolving ResourceGroupName, %w", err)
}

services := make(map[string]*azdext.ServiceConfig, len(src.Services))
for i, svc := range src.Services {
var serviceConfig *azdext.ServiceConfig
if err := mapper.WithResolver(resolver).Convert(svc, &serviceConfig); err != nil {
if err := mapper.WithContext(ctx).Convert(svc, &serviceConfig); err != nil {
return nil, fmt.Errorf("converting service %q: %w", i, err)
}

Expand Down Expand Up @@ -770,7 +775,7 @@ func registerProjectMappings() {
services := make(map[string]*ServiceConfig, len(src.Services))
for name, protoSvc := range src.Services {
var serviceConfig *ServiceConfig
if err := mapper.Convert(protoSvc, &serviceConfig); err != nil {
if err := mapper.WithContext(ctx).Convert(protoSvc, &serviceConfig); err != nil {
return nil, fmt.Errorf("converting service %s: %w", name, err)
}
services[name] = serviceConfig
Expand Down Expand Up @@ -817,6 +822,17 @@ func getEnvResolver(resolver mapper.Resolver) func(string) string {
return func(string) string { return "" }
}

func envsubstIfEnabled(
ctx context.Context,
value osutil.ExpandableString,
resolver func(string) string,
) (string, error) {
if !mapper.EnvSubstEnabled(ctx) {
return value.Raw(), nil
}
return value.Envsubst(resolver)
}

// getResourceTypeKinds returns the kinds for a given resource type.
// This corresponds to the addListResourcesKind function in grpcserver/compose_service.go.
func getResourceTypeKinds(resourceType ResourceType) []string {
Expand Down
Loading
Loading