Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cli/azd/.vscode/cspell-azd-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ sstore
staticwebapp
structs
swacli
unmarshaling
unmarshalling
westus2
yacspin
2 changes: 1 addition & 1 deletion cli/azd/pkg/environment/azd_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (c *AzdContext) BicepParameters(env string, module string) (map[string]inte
var unmarshalled map[string]interface{}
err = json.Unmarshal(byts, &unmarshalled)
if err != nil {
return nil, fmt.Errorf("unmarshaling parameters file: %w", err)
return nil, fmt.Errorf("unmarshalling parameters file: %w", err)
}

ret := make(map[string]interface{})
Expand Down
30 changes: 27 additions & 3 deletions cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools"
)

type DockerProjectOptions struct {
Path string `json:"path"`
Context string `json:"context"`
Platform string `json:"platform"`
}

type dockerProject struct {
config *ServiceConfig
env *environment.Environment
Expand All @@ -24,13 +30,15 @@ func (p *dockerProject) RequiredExternalTools() []tools.ExternalTool {
}

func (p *dockerProject) Package(ctx context.Context, progress chan<- string) (string, error) {
log.Printf("building image for service %s (path: %s)", p.config.Name, p.config.Path())
dockerOptions := getDockerOptionsWithDefaults(p.config.Docker)

log.Printf("building image for service %s, cwd: %s, path: %s, context: %s)", p.config.Name, p.config.Path(), dockerOptions.Path, dockerOptions.Context)

// Build the container
progress <- "Building docker image"
imageId, err := p.docker.Build(ctx, "./Dockerfile", p.config.Path())
imageId, err := p.docker.Build(ctx, p.config.Path(), dockerOptions.Path, dockerOptions.Platform, dockerOptions.Context)
if err != nil {
return "", fmt.Errorf("building container: %s at %s: %w", p.config.Name, p.config.Path(), err)
return "", fmt.Errorf("building container: %s at %s: %w", p.config.Name, dockerOptions.Context, err)
}

log.Printf("built image %s for %s", imageId, p.config.Name)
Expand All @@ -51,3 +59,19 @@ func NewDockerProject(config *ServiceConfig, env *environment.Environment, docke
framework: framework,
}
}

func getDockerOptionsWithDefaults(options DockerProjectOptions) DockerProjectOptions {
if options.Path == "" {
options.Path = "./Dockerfile"
}

if options.Platform == "" {
options.Platform = "amd64"
}

if options.Context == "" {
options.Context = "."
}

return options
}
141 changes: 141 additions & 0 deletions cli/azd/pkg/project/framework_service_docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package project

import (
"context"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/executil"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/test/helpers"
"github.com/stretchr/testify/require"
)

func TestDefaultDockerOptions(t *testing.T) {
const testProj = `
name: test-proj
metadata:
template: test-proj-template
services:
web:
project: src/web
language: js
host: containerapp
`

ctx := helpers.CreateTestContext(context.Background(), gblCmdOptions, azCli, mockHttpClient)
env := environment.Environment{Values: make(map[string]string)}
env.SetEnvName("test-env")

projectConfig, _ := ParseProjectConfig(testProj, &env)
prj, _ := projectConfig.GetProject(ctx, &env)
service := prj.Services[0]
ran := false

dockerArgs := tools.DockerArgs{
RunWithResultFn: func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
ran = true

require.Equal(t, []string{
"build", "-q",
"-f", "./Dockerfile",
"--platform", "amd64",
".",
}, args.Args)

return executil.RunResult{
Stdout: "imageId",
Stderr: "",
ExitCode: 0,
}, nil
},
}

docker := tools.NewDocker(dockerArgs)

progress := make(chan string)
defer close(progress)

internalFramework := NewNpmProject(service.Config, &env)
status := ""

go func() {
for value := range progress {
status = value
}
}()

framework := NewDockerProject(service.Config, &env, docker, internalFramework)
res, err := framework.Package(ctx, progress)

require.Equal(t, "imageId", res)
require.Nil(t, err)
require.Equal(t, "Building docker image", status)
require.Equal(t, true, ran)
}

func TestCustomDockerOptions(t *testing.T) {
const testProj = `
name: test-proj
metadata:
template: test-proj-template
services:
web:
project: src/web
language: js
host: containerapp
docker:
path: ./Dockerfile.dev
context: ../
`

ctx := helpers.CreateTestContext(context.Background(), gblCmdOptions, azCli, mockHttpClient)
env := environment.Environment{Values: make(map[string]string)}
env.SetEnvName("test-env")

projectConfig, _ := ParseProjectConfig(testProj, &env)
prj, _ := projectConfig.GetProject(ctx, &env)
service := prj.Services[0]
ran := false

dockerArgs := tools.DockerArgs{
RunWithResultFn: func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
ran = true

require.Equal(t, []string{
"build", "-q",
"-f", "./Dockerfile.dev",
"--platform", "amd64",
"../",
}, args.Args)

return executil.RunResult{
Stdout: "imageId",
Stderr: "",
ExitCode: 0,
}, nil
},
}

docker := tools.NewDocker(dockerArgs)

progress := make(chan string)
defer close(progress)

internalFramework := NewNpmProject(service.Config, &env)
status := ""

go func() {
for value := range progress {
status = value
}
}()

framework := NewDockerProject(service.Config, &env, docker, internalFramework)
res, err := framework.Package(ctx, progress)

require.Equal(t, "imageId", res)
require.Nil(t, err)
require.Equal(t, "Building docker image", status)
require.Equal(t, true, ran)
}
29 changes: 29 additions & 0 deletions cli/azd/pkg/project/project_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,32 @@ services:
require.NotNil(t, svc.Scope)
}
}

func TestProjectWithCustomDockerOptions(t *testing.T) {
const testProj = `
name: test-proj
metadata:
template: test-proj-template
services:
web:
project: src/web
language: js
host: containerapp
docker:
path: ./Dockerfile.dev
context: ../
`

e := environment.Environment{Values: make(map[string]string)}
e.SetEnvName("test-env")

projectConfig, err := ParseProjectConfig(testProj, &e)

require.NotNil(t, projectConfig)
require.Nil(t, err)

service := projectConfig.Services["web"]

require.Equal(t, "./Dockerfile.dev", service.Docker.Path)
require.Equal(t, "../", service.Docker.Context)
}
6 changes: 4 additions & 2 deletions cli/azd/pkg/project/service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type ServiceConfig struct {
OutputPath string `yaml:"dist"`
// The infrastructure module name to use for this project
ModuleName string `yaml:"moduleName"`
// The optional docker options
Docker DockerProjectOptions `yaml:"docker"`
}

// Path returns the fully qualified path to the project
Expand Down Expand Up @@ -65,7 +67,7 @@ func (sc *ServiceConfig) GetServiceTarget(ctx context.Context, env *environment.
case "", string(AppServiceTarget):
target = NewAppServiceTarget(sc, env, scope, azCli)
case string(ContainerAppTarget):
target = NewContainerAppTarget(sc, env, scope, azCli, tools.NewDocker())
target = NewContainerAppTarget(sc, env, scope, azCli, tools.NewDocker(tools.DockerArgs{}))
case string(AzureFunctionTarget):
target = NewFunctionAppTarget(sc, env, scope, azCli)
case string(StaticWebAppTarget):
Expand Down Expand Up @@ -95,7 +97,7 @@ func (sc *ServiceConfig) GetFrameworkService(ctx context.Context, env *environme
// For containerized applications we use a nested framework service
if sc.Host == string(ContainerAppTarget) {
sourceFramework := frameworkService
frameworkService = NewDockerProject(sc, env, tools.NewDocker(), sourceFramework)
frameworkService = NewDockerProject(sc, env, tools.NewDocker(tools.DockerArgs{}), sourceFramework)
}

return &frameworkService, nil
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/project/service_target_containerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
// Tag image.
log.Printf("tagging image %s as %s", path, fullTag)
progress <- "Tagging image"
if err := at.docker.Tag(ctx, path, fullTag); err != nil {
if err := at.docker.Tag(ctx, at.config.Path(), path, fullTag); err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("tagging image: %w", err)
}

log.Printf("pushing %s to registry", fullTag)

// Push image.
progress <- "Pushing container image"
if err := at.docker.Push(ctx, fullTag); err != nil {
if err := at.docker.Push(ctx, at.config.Path(), fullTag); err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("pushing image: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/pkg/tools/azcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (tok *AzCliAccessToken) UnmarshalJSON(data []byte) error {
}

if err := json.Unmarshal(data, &wire); err != nil {
return fmt.Errorf("unmarshaling json: %w", err)
return fmt.Errorf("unmarshalling json: %w", err)
}

tok.AccessToken = wire.AccessToken
Expand Down
44 changes: 30 additions & 14 deletions cli/azd/pkg/tools/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,52 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/executil"
)

func NewDocker() *Docker {
return &Docker{}
func NewDocker(args DockerArgs) *Docker {
if args.RunWithResultFn == nil {
args.RunWithResultFn = executil.RunWithResult
}

return &Docker{
runWithResultFn: args.RunWithResultFn,
}
}

type DockerArgs struct {
RunWithResultFn func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error)
}

type Docker struct {
runWithResultFn func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error)
}

// Build runs a docker build for a given Dockerfile, forcing the amd64 platform. If successful, it
// Runs a Docker build for a given Dockerfile. If the platform is not specified (empty), it defaults to amd64. If the build is successful, the function
// returns the image id of the built image.
func (d *Docker) Build(ctx context.Context, dockerFilePath string, buildContext string) (string, error) {
res, err := d.executeCommand(ctx, buildContext, "build", "-q", "-f", dockerFilePath, "--platform", "amd64", ".")
func (d *Docker) Build(ctx context.Context, cwd string, dockerFilePath string, platform string, buildContext string) (string, error) {
if strings.TrimSpace(platform) == "" {
platform = "amd64"
}

res, err := d.executeCommand(ctx, cwd, "build", "-q", "-f", dockerFilePath, "--platform", platform, buildContext)
if err != nil {
return "", fmt.Errorf("building image: %s: %w", res.String(), err)
}

return strings.TrimSpace(res.Stdout), nil
}

func (d *Docker) Tag(ctx context.Context, imageName string, tag string) error {
res, err := d.executeCommand(ctx, ".", "tag", imageName, tag)
func (d *Docker) Tag(ctx context.Context, cwd string, imageName string, tag string) error {
res, err := d.executeCommand(ctx, cwd, "tag", imageName, tag)
if err != nil {
return fmt.Errorf("tagging image: %s: %w", res.String(), err)
}

return nil
}

func (d *Docker) Push(ctx context.Context, tag string) error {
res, err := d.executeCommand(ctx, ".", "push", tag)
func (d *Docker) Push(ctx context.Context, cwd string, tag string) error {
res, err := d.executeCommand(ctx, cwd, "push", tag)
if err != nil {
return fmt.Errorf("tagging image: %s: %w", res.String(), err)
return fmt.Errorf("pushing image: %s: %w", res.String(), err)
}

return nil
Expand All @@ -57,9 +72,10 @@ func (d *Docker) Name() string {
}

func (d *Docker) executeCommand(ctx context.Context, cwd string, args ...string) (executil.RunResult, error) {
return executil.RunWithResult(ctx, executil.RunArgs{
Cmd: "docker",
Args: args,
Cwd: cwd,
return d.runWithResultFn(ctx, executil.RunArgs{
Cmd: "docker",
Args: args,
Cwd: cwd,
EnrichError: true,
})
}
Loading