Skip to content

Commit

Permalink
Updates docker buildargs to expandable strings (#4369)
Browse files Browse the repository at this point in the history
Resolves #4062

Updates docker buildargs in azure.yaml to support environment variable substitutions.
  • Loading branch information
wbreza authored Sep 27, 2024
1 parent 22ef3f9 commit 291ba2c
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 23 deletions.
21 changes: 19 additions & 2 deletions cli/azd/pkg/project/dotnet_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,23 @@ func mapToStringSlice(m map[string]string, separator string) []string {
return result
}

// mapToExpandableStringSlice converts a map of strings to a slice of expandable strings.
// Each key-value pair in the map is converted to a string in the format "key:value",
// where the separator is specified by the `separator` parameter.
// If the value is an empty string, only the key is included in the resulting slice.
// The resulting slice is returned without any string interpolation performed.
func mapToExpandableStringSlice(m map[string]string, separator string) []osutil.ExpandableString {
var result []osutil.ExpandableString
for key, value := range m {
if value == "" {
result = append(result, osutil.NewExpandableString(key))
} else {
result = append(result, osutil.NewExpandableString(key+separator+value))
}
}
return result
}

func (ai *DotNetImporter) Services(
ctx context.Context, p *ProjectConfig, svcConfig *ServiceConfig,
) (map[string]*ServiceConfig, error) {
Expand Down Expand Up @@ -234,7 +251,7 @@ func (ai *DotNetImporter) Services(
Docker: DockerProjectOptions{
Path: dockerfile.Path,
Context: dockerfile.Context,
BuildArgs: mapToStringSlice(dockerfile.BuildArgs, "="),
BuildArgs: mapToExpandableStringSlice(dockerfile.BuildArgs, "="),
},
}

Expand Down Expand Up @@ -314,7 +331,7 @@ func (ai *DotNetImporter) Services(
dOptions = DockerProjectOptions{
Path: bContainer.Build.Dockerfile,
Context: bContainer.Build.Context,
BuildArgs: mapToStringSlice(bArgs, "="),
BuildArgs: mapToExpandableStringSlice(bArgs, "="),
BuildSecrets: bArgsArray,
BuildEnv: reqEnv,
}
Expand Down
48 changes: 33 additions & 15 deletions cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ import (
)

type DockerProjectOptions struct {
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Context string `yaml:"context,omitempty" json:"context,omitempty"`
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry,omitempty"`
Image osutil.ExpandableString `yaml:"image,omitempty" json:"image,omitempty"`
Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag,omitempty"`
RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"`
BuildArgs []string `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"`
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Context string `yaml:"context,omitempty" json:"context,omitempty"`
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry,omitempty"`
Image osutil.ExpandableString `yaml:"image,omitempty" json:"image,omitempty"`
Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag,omitempty"`
RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"`
BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"`
// not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially.
// Aspire would pass the secret keys, which are env vars that azd will set just to run docker build.
BuildSecrets []string `yaml:"-" json:"-"`
Expand Down Expand Up @@ -223,14 +223,23 @@ func (p *dockerProject) Build(
}
return result, nil
}

dockerBuildArgs := []string{}
for _, arg := range dockerOptions.BuildArgs {
buildArgValue, err := arg.Envsubst(p.env.Getenv)
if err != nil {
return nil, fmt.Errorf("substituting environment variables in build args: %w", err)
}

dockerBuildArgs = append(dockerBuildArgs, buildArgValue)
}

// resolve parameters for build args and secrets
resolvedBuildArgs, err := resolveParameters(dockerOptions.BuildArgs)
resolvedBuildArgs, err := resolveParameters(dockerBuildArgs)
if err != nil {
return nil, err
}

dockerOptions.BuildArgs = resolvedBuildArgs

resolvedBuildEnv, err := resolveParameters(dockerOptions.BuildEnv)
if err != nil {
return nil, err
Expand All @@ -247,7 +256,7 @@ func (p *dockerProject) Build(
}

buildArgs := []string{}
for _, arg := range dockerOptions.BuildArgs {
for _, arg := range resolvedBuildArgs {
buildArgs = append(buildArgs, exec.RedactSensitiveData(arg))
}

Expand Down Expand Up @@ -286,6 +295,15 @@ func (p *dockerProject) Build(
return res, nil
}

// Include full environment variables for the docker build including:
// 1. Environment variables from the host
// 2. Environment variables from the service configuration
// 3. Environment variables from the docker configuration
dockerEnv := []string{}
dockerEnv = append(dockerEnv, os.Environ()...)
dockerEnv = append(dockerEnv, p.env.Environ()...)
dockerEnv = append(dockerEnv, dockerOptions.BuildEnv...)

// Build the container
progress.SetProgress(NewServiceProgress("Building Docker image"))
previewerWriter := p.console.ShowPreviewer(ctx,
Expand All @@ -302,9 +320,9 @@ func (p *dockerProject) Build(
dockerOptions.Target,
dockerOptions.Context,
imageName,
dockerOptions.BuildArgs,
resolvedBuildArgs,
dockerOptions.BuildSecrets,
dockerOptions.BuildEnv,
dockerEnv,
previewerWriter,
)
p.console.StopPreviewer(ctx, false)
Expand Down
70 changes: 65 additions & 5 deletions cli/azd/pkg/project/framework_service_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,12 @@ services:

func Test_DockerProject_Build(t *testing.T) {
tests := []struct {
// Optional - use for custom initialization
init func(t *testing.T) error
// Optional - use for custom validation
validate func(t *testing.T, result *ServiceBuildResult, dockerBuildArgs *exec.RunArgs) error
name string
env *environment.Environment
project string
language ServiceLanguageKind
dockerOptions DockerProjectOptions
Expand Down Expand Up @@ -342,6 +347,47 @@ func Test_DockerProject_Build(t *testing.T) {
},
expectedDockerBuildArgs: nil,
},
{
name: "With custom environment variables",
project: "./src/api",
language: ServiceLanguageJavaScript,
hasDockerFile: true,
init: func(t *testing.T) error {
os.Setenv("AZD_CUSTOM_OS_VAR", "os-value")
return nil
},
env: environment.NewWithValues("test", map[string]string{
"AZD_CUSTOM_ENV_VAR": "env-value",
}),
dockerOptions: DockerProjectOptions{
BuildEnv: []string{
"AZD_CUSTOM_BUILD_VAR=build-value",
},
BuildArgs: []osutil.ExpandableString{
osutil.NewExpandableString("AZD_CUSTOM_OS_VAR"),
osutil.NewExpandableString("AZD_CUSTOM_ENV_VAR"),
osutil.NewExpandableString("AZD_CUSTOM_BUILD_VAR"),
osutil.NewExpandableString("AZD_CUSTOM_EXPANDED_VAR=${AZD_CUSTOM_ENV_VAR}"),
},
},
validate: func(t *testing.T, result *ServiceBuildResult, dockerBuildArgs *exec.RunArgs) error {
require.NotNil(t, result)
require.NotNil(t, dockerBuildArgs)

// Contains OS, azd env & docker env vars
require.Contains(t, dockerBuildArgs.Env, "AZD_CUSTOM_OS_VAR=os-value")
require.Contains(t, dockerBuildArgs.Env, "AZD_CUSTOM_ENV_VAR=env-value")
require.Contains(t, dockerBuildArgs.Env, "AZD_CUSTOM_BUILD_VAR=build-value")

// Contains docker build args
require.Contains(t, dockerBuildArgs.Args, "AZD_CUSTOM_OS_VAR")
require.Contains(t, dockerBuildArgs.Args, "AZD_CUSTOM_ENV_VAR")
require.Contains(t, dockerBuildArgs.Args, "AZD_CUSTOM_BUILD_VAR")
require.Contains(t, dockerBuildArgs.Args, "AZD_CUSTOM_EXPANDED_VAR=env-value")

return nil
},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -385,9 +431,18 @@ func Test_DockerProject_Build(t *testing.T) {
return exec.NewRunResult(0, "3.0.0", ""), nil
})

if tt.init != nil {
err := tt.init(t)
require.NoError(t, err)
}

temp := t.TempDir()

env := environment.New("test")
env := tt.env
if env == nil {
env = environment.New("test")
}

dockerCli := docker.NewCli(mockContext.CommandRunner)
serviceConfig := createTestServiceConfig(tt.project, ContainerAppTarget, tt.language)
serviceConfig.Project.Path = temp
Expand Down Expand Up @@ -427,10 +482,15 @@ func Test_DockerProject_Build(t *testing.T) {
},
)

require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, tt.expectedBuildResult, result)
require.Equal(t, tt.expectedDockerBuildArgs, dockerBuildArgs.Args)
if tt.validate != nil {
err := tt.validate(t, result, &dockerBuildArgs)
require.NoError(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, tt.expectedBuildResult, result)
require.Equal(t, tt.expectedDockerBuildArgs, dockerBuildArgs.Args)
}
})
}
}
Expand Down
34 changes: 33 additions & 1 deletion cli/azd/pkg/project/project_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/azure/azure-dev/cli/azd/internal"
"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/osutil"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/snapshot"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -126,7 +127,38 @@ services:

require.Equal(t, "./Dockerfile.dev", service.Docker.Path)
require.Equal(t, "../", service.Docker.Context)
require.Equal(t, []string{"foo", "bar"}, service.Docker.BuildArgs)
require.Equal(t, []osutil.ExpandableString{
osutil.NewExpandableString("foo"),
osutil.NewExpandableString("bar"),
}, service.Docker.BuildArgs)
}

func TestProjectWithExpandableDockerArgs(t *testing.T) {
env := environment.NewWithValues("test", map[string]string{
"REGISTRY": "myregistry",
"IMAGE": "myimage",
"TAG": "mytag",
"KEY1": "val1",
"KEY2": "val2",
})

serviceConfig := &ServiceConfig{
Docker: DockerProjectOptions{
Registry: osutil.NewExpandableString("${REGISTRY}"),
Image: osutil.NewExpandableString("${IMAGE}"),
Tag: osutil.NewExpandableString("${TAG}"),
BuildArgs: []osutil.ExpandableString{
osutil.NewExpandableString("key1=${KEY1}"),
osutil.NewExpandableString("key2=${KEY2}"),
},
},
}

require.Equal(t, env.Getenv("REGISTRY"), serviceConfig.Docker.Registry.MustEnvsubst(env.Getenv))
require.Equal(t, env.Getenv("IMAGE"), serviceConfig.Docker.Image.MustEnvsubst(env.Getenv))
require.Equal(t, env.Getenv("TAG"), serviceConfig.Docker.Tag.MustEnvsubst(env.Getenv))
require.Equal(t, fmt.Sprintf("key1=%s", env.Getenv("KEY1")), serviceConfig.Docker.BuildArgs[0].MustEnvsubst(env.Getenv))
require.Equal(t, fmt.Sprintf("key2=%s", env.Getenv("KEY2")), serviceConfig.Docker.BuildArgs[1].MustEnvsubst(env.Getenv))
}

func TestProjectConfigAddHandler(t *testing.T) {
Expand Down

0 comments on commit 291ba2c

Please sign in to comment.