From a6d543e8d1e9b51a7b1b49548da6b99ed59ba4b1 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 13 Jul 2022 15:30:18 -0700 Subject: [PATCH 01/10] Adds support for custom docker options --- .../pkg/project/framework_service_docker.go | 47 +++++++++++++++++-- cli/azd/pkg/project/service_config.go | 2 + .../project/service_target_containerapp.go | 4 +- cli/azd/pkg/tools/docker.go | 23 +++++---- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index e92f3ab3bf9..80854d44dd2 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -5,6 +5,7 @@ package project import ( "context" + "encoding/json" "fmt" "log" @@ -12,11 +13,18 @@ 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 docker *tools.Docker framework FrameworkService + options DockerProjectOptions } func (p *dockerProject) RequiredExternalTools() []tools.ExternalTool { @@ -24,13 +32,13 @@ 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()) + log.Printf("building image for service %s, cwd: %s, path: %s, context: %s)", p.config.Name, p.config.Path(), p.options.Path, p.options.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(), p.options.Path, p.options.Platform, p.options.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, p.options.Context, err) } log.Printf("built image %s for %s", imageId, p.config.Name) @@ -49,5 +57,38 @@ func NewDockerProject(config *ServiceConfig, env *environment.Environment, docke env: env, docker: docker, framework: framework, + options: createDockerOptions(config), + } +} + +func createDockerOptions(config *ServiceConfig) DockerProjectOptions { + dockerOptions := DockerProjectOptions{ + Path: "./Dockerfile", + Platform: "amd64", + Context: ".", + } + + if len(config.Options) == 0 { + return dockerOptions + } + + dockerMap, ok := config.Options["docker"] + if !ok { + return dockerOptions + } + + log.Printf("found custom docker options %s\n", dockerMap) + + jsonBytes, err := json.Marshal(dockerMap) + if err != nil { + log.Printf("error marshalling project options to JSON: %s", err.Error()) + return dockerOptions + } + + if err := json.Unmarshal(jsonBytes, &dockerOptions); err != nil { + log.Printf("error unmarshalling project to DockerProjectOptions: %s", err.Error()) + return dockerOptions } + + return dockerOptions } diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index a71616ea65d..a48a012064a 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -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 custom service type options + Options map[string]interface{} `yaml:"options"` } // Path returns the fully qualified path to the project diff --git a/cli/azd/pkg/project/service_target_containerapp.go b/cli/azd/pkg/project/service_target_containerapp.go index b3eefaa1866..35a1e80c959 100644 --- a/cli/azd/pkg/project/service_target_containerapp.go +++ b/cli/azd/pkg/project/service_target_containerapp.go @@ -58,7 +58,7 @@ 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) } @@ -66,7 +66,7 @@ func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az // 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) } diff --git a/cli/azd/pkg/tools/docker.go b/cli/azd/pkg/tools/docker.go index 6223b1c62a4..29dabaddd97 100644 --- a/cli/azd/pkg/tools/docker.go +++ b/cli/azd/pkg/tools/docker.go @@ -17,8 +17,12 @@ type Docker struct { // Build runs a docker build for a given Dockerfile, forcing the amd64 platform. If successful, it // 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) } @@ -26,8 +30,8 @@ func (d *Docker) Build(ctx context.Context, dockerFilePath string, buildContext 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) } @@ -35,8 +39,8 @@ func (d *Docker) Tag(ctx context.Context, imageName string, tag string) error { 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) } @@ -58,8 +62,9 @@ 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, + Cmd: "docker", + Args: args, + Cwd: cwd, + EnrichError: true, }) } From 520cdabacfff7cca1c072fb982edc636ea6b4a82 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 13 Jul 2022 15:37:05 -0700 Subject: [PATCH 02/10] Updates cspell configuration --- cli/azd/.vscode/cspell-azd-dictionary.txt | 2 +- cli/azd/pkg/environment/azd_context.go | 2 +- cli/azd/pkg/tools/azcli.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 24dcab93eaa..515758f08e2 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -43,6 +43,6 @@ sstore staticwebapp structs swacli -unmarshaling +unmarshalling westus2 yacspin diff --git a/cli/azd/pkg/environment/azd_context.go b/cli/azd/pkg/environment/azd_context.go index 8882ec82cc4..5175eb19d90 100644 --- a/cli/azd/pkg/environment/azd_context.go +++ b/cli/azd/pkg/environment/azd_context.go @@ -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{}) diff --git a/cli/azd/pkg/tools/azcli.go b/cli/azd/pkg/tools/azcli.go index f5b8e678370..f87c8acab4a 100644 --- a/cli/azd/pkg/tools/azcli.go +++ b/cli/azd/pkg/tools/azcli.go @@ -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 From 9baccdcfe2a1258145635668fdf1b28e689309d7 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 14 Jul 2022 12:30:44 -0700 Subject: [PATCH 03/10] Adds docker tool unit tests --- cli/azd/pkg/tools/docker.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/azd/pkg/tools/docker.go b/cli/azd/pkg/tools/docker.go index 29dabaddd97..14a146463d5 100644 --- a/cli/azd/pkg/tools/docker.go +++ b/cli/azd/pkg/tools/docker.go @@ -9,10 +9,13 @@ import ( ) func NewDocker() *Docker { - return &Docker{} + return &Docker{ + runWithResultFn: executil.RunWithResult, + } } 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 @@ -42,7 +45,7 @@ func (d *Docker) Tag(ctx context.Context, cwd string, imageName string, tag stri 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 @@ -61,7 +64,7 @@ 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{ + return d.runWithResultFn(ctx, executil.RunArgs{ Cmd: "docker", Args: args, Cwd: cwd, From 81d0191f639962470358c949dc600d48b3210e49 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 14 Jul 2022 12:42:05 -0700 Subject: [PATCH 04/10] Adds docker tool unit tests --- cli/azd/pkg/tools/docker_test.go | 246 +++++++++++++++++++++++++++++++ schemas/v1.0/azure.yaml.json | 58 +++++--- 2 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 cli/azd/pkg/tools/docker_test.go diff --git a/cli/azd/pkg/tools/docker_test.go b/cli/azd/pkg/tools/docker_test.go new file mode 100644 index 00000000000..ec6c4e1c747 --- /dev/null +++ b/cli/azd/pkg/tools/docker_test.go @@ -0,0 +1,246 @@ +package tools + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/executil" + "github.com/stretchr/testify/require" +) + +func Test_DockerBuild(t *testing.T) { + docker := NewDocker() + + cwd := "." + dockerFile := "./Dockerfile" + dockerContext := "../" + platform := "amd64" + + t.Run("NoError", func(t *testing.T) { + ran := false + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "build", + "-q", + "-f", dockerFile, + "--platform", platform, + dockerContext, + }, args.Args) + + return executil.RunResult{ + Stdout: "Docker build output", + Stderr: "", + ExitCode: 0, + }, nil + } + + result, err := docker.Build(context.Background(), cwd, dockerFile, platform, dockerContext) + + require.Equal(t, true, ran) + require.Nil(t, err) + require.Equal(t, "Docker build output", result) + }) + + t.Run("WithError", func(t *testing.T) { + ran := false + stdErr := "Error tagging DockerFile" + customErrorMessage := "example error message" + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "build", + "-q", + "-f", dockerFile, + "--platform", platform, + dockerContext, + }, args.Args) + + return executil.RunResult{ + Stdout: "", + Stderr: stdErr, + ExitCode: 1, + }, errors.New(customErrorMessage) + } + + result, err := docker.Build(context.Background(), cwd, dockerFile, platform, dockerContext) + + require.Equal(t, true, ran) + require.NotNil(t, err) + require.Equal(t, fmt.Sprintf("building image: exit code: 1, stdout: , stderr: %s: %s", stdErr, customErrorMessage), err.Error()) + require.Equal(t, "", result) + }) +} + +func Test_DockerBuildEmptyPlatform(t *testing.T) { + docker := NewDocker() + + ran := false + cwd := "." + dockerFile := "./Dockerfile" + dockerContext := "../" + platform := "amd64" + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "build", + "-q", + "-f", dockerFile, + "--platform", platform, + dockerContext, + }, args.Args) + + return executil.RunResult{ + Stdout: "Docker build output", + Stderr: "", + ExitCode: 0, + }, nil + } + + result, err := docker.Build(context.Background(), cwd, dockerFile, "", dockerContext) + + require.Equal(t, true, ran) + require.Nil(t, err) + require.Equal(t, "Docker build output", result) +} + +func Test_DockerTag(t *testing.T) { + docker := NewDocker() + + cwd := "." + imageName := "image-name" + tag := "customTag" + + t.Run("NoError", func(t *testing.T) { + ran := false + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "tag", + imageName, + tag, + }, args.Args) + + return executil.RunResult{ + Stdout: "Docker build output", + Stderr: "", + ExitCode: 0, + }, nil + } + + err := docker.Tag(context.Background(), cwd, imageName, tag) + + require.Equal(t, true, ran) + require.Nil(t, err) + }) + + t.Run("WithError", func(t *testing.T) { + ran := false + stdErr := "Error tagging DockerFile" + customErrorMessage := "example error message" + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "tag", + imageName, + tag, + }, args.Args) + + return executil.RunResult{ + Stdout: "", + Stderr: stdErr, + ExitCode: 1, + }, errors.New(customErrorMessage) + } + + err := docker.Tag(context.Background(), cwd, imageName, tag) + + require.Equal(t, true, ran) + require.NotNil(t, err) + require.Equal(t, fmt.Sprintf("tagging image: exit code: 1, stdout: , stderr: %s: %s", stdErr, customErrorMessage), err.Error()) + }) +} + +func Test_DockerPush(t *testing.T) { + docker := NewDocker() + + cwd := "." + tag := "customTag" + + t.Run("NoError", func(t *testing.T) { + ran := false + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "push", + tag, + }, args.Args) + + return executil.RunResult{ + Stdout: "Docker build output", + Stderr: "", + ExitCode: 0, + }, nil + } + + err := docker.Push(context.Background(), cwd, tag) + + require.Equal(t, true, ran) + require.Nil(t, err) + }) + + t.Run("WithError", func(t *testing.T) { + ran := false + stdErr := "Error pushing DockerFile" + customErrorMessage := "example error message" + + docker.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, "docker", args.Cmd) + require.Equal(t, cwd, args.Cwd) + require.Equal(t, []string{ + "push", + tag, + }, args.Args) + + return executil.RunResult{ + Stdout: "", + Stderr: stdErr, + ExitCode: 1, + }, errors.New(customErrorMessage) + } + + err := docker.Push(context.Background(), cwd, tag) + + require.Equal(t, true, ran) + require.NotNil(t, err) + require.Equal(t, fmt.Sprintf("pushing image: exit code: 1, stdout: , stderr: %s: %s", stdErr, customErrorMessage), err.Error()) + }) +} diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index f0304b6f9e9..423d55dd0d8 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -1,25 +1,28 @@ { "$schema": "http://json-schema.org/schema#", "$id": "https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json", - "type": "object", - "required": [ - "name", "services" + "name", + "services" ], - "properties": { - "name": { - "type": "string", + "name": { + "type": "string", "minLength": 2, "title": "Name of the application" }, - + "resourceGroup": { + "type": "string", + "minLength": 3, + "maxLength": 64, + "title": "Name of the Azure resource group", + "description": "When specified will override the resource group name used for infrastructure provisioning." + }, "metadata": { "type": "object", - "properties": { - "template": { + "template": { "type": "string", "title": "Identifier of the template from which the application was created. Optional.", "examples": [ @@ -28,53 +31,64 @@ } } }, - "services": { "type": "object", "title": "Definition of services that comprise the application", - "additionalProperties": { "type": "object", - "properties": { - "resourceName": { + "resourceName": { "type": "string", "title": "Name of the Azure resource that implements the service", "description": "Optional. If not specified, the resource name will be constructed from current environment name concatenated with service name (, for example 'prodapi')." }, - - "project": { + "project": { "type": "string", "title": "Path to the service source code directory" }, - "host": { "type": "string", "title": "Type of Azure resource used for service implementation", "description": "If omitted, App Service will be assumed.", - "enum": [ "", "appservice", "containerapp", "function", "staticwebapp" ] + "enum": [ + "", + "appservice", + "containerapp", + "function", + "staticwebapp" + ] }, - "language": { "type": "string", "title": "Service implementation language", "description": "If omitted, .NET will be assumed.", - "enum": [ "", "dotnet", "csharp", "fsharp", "py", "python", "js", "ts" ] + "enum": [ + "", + "dotnet", + "csharp", + "fsharp", + "py", + "python", + "js", + "ts" + ] }, - "moduleName": { "type": "string", "title": "Name of the module used to deploy the service", "description": "If omitted, the CLI will assume the module name is the same as the service name." }, - "dist": { "type": "string", "title": "Relative path to service deployment artifacts", "description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included." + }, + "options": { + "type": "object", + "title": "Custom service options", + "description": "When specified, will configure underlying framework and platform targets." } }, - "required": [ "project" ] From 2330d02153f55593a4577f655ec64f783eb48648 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 14 Jul 2022 13:25:14 -0700 Subject: [PATCH 05/10] JSON schema enhancements Updated schema --- .../pkg/project/framework_service_docker.go | 43 ++--- cli/azd/pkg/project/service_config.go | 2 +- schemas/v1.0/azure.yaml.json | 154 +++++++++++------- 3 files changed, 112 insertions(+), 87 deletions(-) diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index 80854d44dd2..cf8752408f0 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -5,7 +5,6 @@ package project import ( "context" - "encoding/json" "fmt" "log" @@ -24,7 +23,6 @@ type dockerProject struct { env *environment.Environment docker *tools.Docker framework FrameworkService - options DockerProjectOptions } func (p *dockerProject) RequiredExternalTools() []tools.ExternalTool { @@ -32,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, cwd: %s, path: %s, context: %s)", p.config.Name, p.config.Path(), p.options.Path, p.options.Context) + 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, p.config.Path(), p.options.Path, p.options.Platform, p.options.Context) + 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.options.Context, 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) @@ -57,38 +57,21 @@ func NewDockerProject(config *ServiceConfig, env *environment.Environment, docke env: env, docker: docker, framework: framework, - options: createDockerOptions(config), } } -func createDockerOptions(config *ServiceConfig) DockerProjectOptions { - dockerOptions := DockerProjectOptions{ - Path: "./Dockerfile", - Platform: "amd64", - Context: ".", - } - - if len(config.Options) == 0 { - return dockerOptions +func getDockerOptionsWithDefaults(options *DockerProjectOptions) DockerProjectOptions { + if options.Path == "" { + options.Path = "./Dockerfile" } - dockerMap, ok := config.Options["docker"] - if !ok { - return dockerOptions - } - - log.Printf("found custom docker options %s\n", dockerMap) - - jsonBytes, err := json.Marshal(dockerMap) - if err != nil { - log.Printf("error marshalling project options to JSON: %s", err.Error()) - return dockerOptions + if options.Platform == "" { + options.Platform = "amd64" } - if err := json.Unmarshal(jsonBytes, &dockerOptions); err != nil { - log.Printf("error unmarshalling project to DockerProjectOptions: %s", err.Error()) - return dockerOptions + if options.Context == "" { + options.Context = "." } - return dockerOptions + return *options } diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index a48a012064a..83896e68e5a 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -28,7 +28,7 @@ type ServiceConfig struct { // The infrastructure module name to use for this project ModuleName string `yaml:"moduleName"` // The custom service type options - Options map[string]interface{} `yaml:"options"` + Docker DockerProjectOptions `yaml:"options"` } // Path returns the fully qualified path to the project diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 423d55dd0d8..c066356b9df 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -1,11 +1,12 @@ { - "$schema": "http://json-schema.org/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json", "type": "object", "required": [ "name", "services" ], + "additionalProperties": false, "properties": { "name": { "type": "string", @@ -35,63 +36,104 @@ "type": "object", "title": "Definition of services that comprise the application", "additionalProperties": { - "type": "object", - "properties": { - "resourceName": { - "type": "string", - "title": "Name of the Azure resource that implements the service", - "description": "Optional. If not specified, the resource name will be constructed from current environment name concatenated with service name (, for example 'prodapi')." - }, - "project": { - "type": "string", - "title": "Path to the service source code directory" - }, - "host": { - "type": "string", - "title": "Type of Azure resource used for service implementation", - "description": "If omitted, App Service will be assumed.", - "enum": [ - "", - "appservice", - "containerapp", - "function", - "staticwebapp" - ] - }, - "language": { - "type": "string", - "title": "Service implementation language", - "description": "If omitted, .NET will be assumed.", - "enum": [ - "", - "dotnet", - "csharp", - "fsharp", - "py", - "python", - "js", - "ts" - ] - }, - "moduleName": { - "type": "string", - "title": "Name of the module used to deploy the service", - "description": "If omitted, the CLI will assume the module name is the same as the service name." - }, - "dist": { - "type": "string", - "title": "Relative path to service deployment artifacts", - "description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included." - }, - "options": { - "type": "object", - "title": "Custom service options", - "description": "When specified, will configure underlying framework and platform targets." + "$ref": "#/$defs/service" + } + } + }, + "$defs": { + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "resourceName": { + "type": "string", + "title": "Name of the Azure resource that implements the service", + "description": "Optional. If not specified, the resource name will be constructed from current environment name concatenated with service name (, for example 'prodapi')." + }, + "project": { + "type": "string", + "title": "Path to the service source code directory" + }, + "host": { + "type": "string", + "title": "Type of Azure resource used for service implementation", + "description": "If omitted, App Service will be assumed.", + "enum": [ + "", + "appservice", + "containerapp", + "function", + "staticwebapp" + ] + }, + "language": { + "type": "string", + "title": "Service implementation language", + "description": "If omitted, .NET will be assumed.", + "enum": [ + "", + "dotnet", + "csharp", + "fsharp", + "py", + "python", + "js", + "ts" + ] + }, + "moduleName": { + "type": "string", + "title": "Name of the module used to deploy the service", + "description": "If omitted, the CLI will assume the module name is the same as the service name." + }, + "dist": { + "type": "string", + "title": "Relative path to service deployment artifacts", + "description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included." + }, + "docker": { + "$ref": "#/$defs/dockerOptions" + } + }, + "if": { + "not": { + "properties": { + "host": { + "const": "containerapp" + } } + } + }, + "then": { + "properties": { + "docker": false + } + }, + "required": [ + "project" + ] + }, + "dockerOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "title": "The path to the Dockerfile", + "description": "Path to the Dockerfile is relative to your service", + "default": "./Dockerfile" }, - "required": [ - "project" - ] + "context": { + "type": "string", + "title": "The docker build context", + "description": "When specified overrides the default context", + "default": "." + }, + "platform": { + "type": "string", + "title": "The platform target", + "default": "amd64" + } } } } From 6f2d74ab35254b8ceb37bf6b7f40abe869a63ccb Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Thu, 14 Jul 2022 16:05:42 -0700 Subject: [PATCH 06/10] Rename to docker options --- cli/azd/pkg/project/service_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 83896e68e5a..3af7b29d72d 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -27,8 +27,8 @@ type ServiceConfig struct { OutputPath string `yaml:"dist"` // The infrastructure module name to use for this project ModuleName string `yaml:"moduleName"` - // The custom service type options - Docker DockerProjectOptions `yaml:"options"` + // The optional docker options + Docker DockerProjectOptions `yaml:"docker"` } // Path returns the fully qualified path to the project From 205a7db18bc8a70471047fe747d124d843c9efea Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 15 Jul 2022 11:40:27 -0700 Subject: [PATCH 07/10] Adds more docker config validation tests --- .../pkg/project/framework_service_docker.go | 6 ++-- cli/azd/pkg/project/project_config_test.go | 29 +++++++++++++++++++ cli/azd/pkg/project/service_config.go | 4 +-- cli/azd/pkg/tools/docker.go | 12 ++++++-- cli/azd/pkg/tools/docker_test.go | 8 ++--- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index cf8752408f0..7d3c1356aaf 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -30,7 +30,7 @@ func (p *dockerProject) RequiredExternalTools() []tools.ExternalTool { } func (p *dockerProject) Package(ctx context.Context, progress chan<- string) (string, error) { - dockerOptions := getDockerOptionsWithDefaults(&p.config.Docker) + 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) @@ -60,7 +60,7 @@ func NewDockerProject(config *ServiceConfig, env *environment.Environment, docke } } -func getDockerOptionsWithDefaults(options *DockerProjectOptions) DockerProjectOptions { +func getDockerOptionsWithDefaults(options DockerProjectOptions) DockerProjectOptions { if options.Path == "" { options.Path = "./Dockerfile" } @@ -73,5 +73,5 @@ func getDockerOptionsWithDefaults(options *DockerProjectOptions) DockerProjectOp options.Context = "." } - return *options + return options } diff --git a/cli/azd/pkg/project/project_config_test.go b/cli/azd/pkg/project/project_config_test.go index 39384e54dc4..7da65934047 100644 --- a/cli/azd/pkg/project/project_config_test.go +++ b/cli/azd/pkg/project/project_config_test.go @@ -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) +} diff --git a/cli/azd/pkg/project/service_config.go b/cli/azd/pkg/project/service_config.go index 3af7b29d72d..6f3403bc572 100644 --- a/cli/azd/pkg/project/service_config.go +++ b/cli/azd/pkg/project/service_config.go @@ -67,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): @@ -97,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 diff --git a/cli/azd/pkg/tools/docker.go b/cli/azd/pkg/tools/docker.go index 14a146463d5..79be6a67961 100644 --- a/cli/azd/pkg/tools/docker.go +++ b/cli/azd/pkg/tools/docker.go @@ -8,12 +8,20 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/executil" ) -func NewDocker() *Docker { +func NewDocker(args DockerArgs) *Docker { + if args.RunWithResultFn == nil { + args.RunWithResultFn = executil.RunWithResult + } + return &Docker{ - runWithResultFn: executil.RunWithResult, + 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) } diff --git a/cli/azd/pkg/tools/docker_test.go b/cli/azd/pkg/tools/docker_test.go index ec6c4e1c747..ac93d407e68 100644 --- a/cli/azd/pkg/tools/docker_test.go +++ b/cli/azd/pkg/tools/docker_test.go @@ -11,7 +11,7 @@ import ( ) func Test_DockerBuild(t *testing.T) { - docker := NewDocker() + docker := NewDocker(DockerArgs{}) cwd := "." dockerFile := "./Dockerfile" @@ -83,7 +83,7 @@ func Test_DockerBuild(t *testing.T) { } func Test_DockerBuildEmptyPlatform(t *testing.T) { - docker := NewDocker() + docker := NewDocker(DockerArgs{}) ran := false cwd := "." @@ -119,7 +119,7 @@ func Test_DockerBuildEmptyPlatform(t *testing.T) { } func Test_DockerTag(t *testing.T) { - docker := NewDocker() + docker := NewDocker(DockerArgs{}) cwd := "." imageName := "image-name" @@ -184,7 +184,7 @@ func Test_DockerTag(t *testing.T) { } func Test_DockerPush(t *testing.T) { - docker := NewDocker() + docker := NewDocker(DockerArgs{}) cwd := "." tag := "customTag" From 4a2f362c06012891e4e8c2a80d2acc0f8175346f Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 15 Jul 2022 11:40:44 -0700 Subject: [PATCH 08/10] Adds more docker config validation tests --- .../project/framework_service_docker_test.go | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 cli/azd/pkg/project/framework_service_docker_test.go diff --git a/cli/azd/pkg/project/framework_service_docker_test.go b/cli/azd/pkg/project/framework_service_docker_test.go new file mode 100644 index 00000000000..763808a9eec --- /dev/null +++ b/cli/azd/pkg/project/framework_service_docker_test.go @@ -0,0 +1,135 @@ +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: "", + 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) + framework.Package(ctx, progress) + 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: "", + 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) + framework.Package(ctx, progress) + require.Equal(t, "Building docker image", status) + require.Equal(t, true, ran) +} From 6b6b3ec6442908fffe9f6c7c6c30df0812c622e3 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 15 Jul 2022 11:43:32 -0700 Subject: [PATCH 09/10] Update cli/azd/pkg/tools/docker.go Co-authored-by: Karol Zadora-Przylecki --- cli/azd/pkg/tools/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/tools/docker.go b/cli/azd/pkg/tools/docker.go index 79be6a67961..bb0bfc96cee 100644 --- a/cli/azd/pkg/tools/docker.go +++ b/cli/azd/pkg/tools/docker.go @@ -26,7 +26,7 @@ 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, cwd string, dockerFilePath string, platform string, buildContext string) (string, error) { if strings.TrimSpace(platform) == "" { From 9b4b47da284f24ed6b38eacb8d348efb8657da10 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Fri, 15 Jul 2022 11:49:54 -0700 Subject: [PATCH 10/10] Checks results of framework.Package(...) func --- .../pkg/project/framework_service_docker_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/azd/pkg/project/framework_service_docker_test.go b/cli/azd/pkg/project/framework_service_docker_test.go index 763808a9eec..28dd146e005 100644 --- a/cli/azd/pkg/project/framework_service_docker_test.go +++ b/cli/azd/pkg/project/framework_service_docker_test.go @@ -44,7 +44,7 @@ services: }, args.Args) return executil.RunResult{ - Stdout: "", + Stdout: "imageId", Stderr: "", ExitCode: 0, }, nil @@ -66,7 +66,10 @@ services: }() framework := NewDockerProject(service.Config, &env, docker, internalFramework) - framework.Package(ctx, progress) + 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) } @@ -107,7 +110,7 @@ services: }, args.Args) return executil.RunResult{ - Stdout: "", + Stdout: "imageId", Stderr: "", ExitCode: 0, }, nil @@ -129,7 +132,10 @@ services: }() framework := NewDockerProject(service.Config, &env, docker, internalFramework) - framework.Package(ctx, progress) + 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) }