Skip to content

Commit

Permalink
Add buildpacks (pack cli) integration (#2678)
Browse files Browse the repository at this point in the history
Buildpacks allows source to container image transformation without Dockerfiles. When a `Dockerfile` isn't found in `azure.yaml`, and the alpha feature `buildpacks` isn't enabled, `azd` will use [pack](https://buildpacks.io/docs/tools/pack/) CLI to invoke the dockerization build.

Limited build customization is provided with this initial release. The only allowed customization is the builder image to use.

Closes #2677
  • Loading branch information
weikanglim committed Aug 31, 2023
1 parent 070cf83 commit ba25a8c
Show file tree
Hide file tree
Showing 10 changed files with 894 additions and 15 deletions.
1 change: 1 addition & 0 deletions cli/azd/.vscode/cspell-azd-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ otlp
otlpconfig
otlptrace
otlptracehttp
paketobuildpacks
pflag
preinit
proxying
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/internal/tracing/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const BicepInstallEvent = "tools.bicep.install"
// GitHubCliInstallEvent is the name of the event which tracks the overall GitHub cli install operation.
const GitHubCliInstallEvent = "tools.gh.install"

// PackCliInstallEvent is the name of the event which tracks the overall pack cli install operation.
const PackCliInstallEvent = "tools.pack.install"

// AccountSubscriptionsListEvent is the name of the event which tracks listing of account subscriptions .
// See fields.AccountSubscriptionsListTenantsFound for additional event fields.
const AccountSubscriptionsListEvent = "account.subscriptions.list"
1 change: 1 addition & 0 deletions cli/azd/pkg/alpha/alpha_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package alpha

const (
TerraformId FeatureId = "terraform"
Buildpacks FeatureId = "buildpacks"
)
124 changes: 114 additions & 10 deletions cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/docker"
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
)

type DockerProjectOptions struct {
Expand Down Expand Up @@ -65,11 +69,13 @@ func (dpr *dockerPackageResult) MarshalJSON() ([]byte, error) {
}

type dockerProject struct {
env *environment.Environment
docker docker.Docker
framework FrameworkService
containerHelper *ContainerHelper
console input.Console
env *environment.Environment
docker docker.Docker
framework FrameworkService
containerHelper *ContainerHelper
console input.Console
alphaFeatureManager *alpha.FeatureManager
commandRunner exec.CommandRunner
}

// NewDockerProject creates a new instance of a Azd project that
Expand All @@ -79,12 +85,16 @@ func NewDockerProject(
docker docker.Docker,
containerHelper *ContainerHelper,
console input.Console,
alphaFeatureManager *alpha.FeatureManager,
commandRunner exec.CommandRunner,
) CompositeFrameworkService {
return &dockerProject{
env: env,
docker: docker,
containerHelper: containerHelper,
console: console,
env: env,
docker: docker,
containerHelper: containerHelper,
console: console,
alphaFeatureManager: alphaFeatureManager,
commandRunner: commandRunner,
}
}

Expand Down Expand Up @@ -153,9 +163,37 @@ func (p *dockerProject) Build(
strings.ToLower(serviceConfig.Name),
)

path := filepath.Join(serviceConfig.Path(), dockerOptions.Path)
_, err := os.Stat(path)
packBuildEnabled := p.alphaFeatureManager.IsEnabled(alpha.Buildpacks)
if packBuildEnabled {
if err != nil && !errors.Is(err, os.ErrNotExist) {
task.SetError(fmt.Errorf("reading dockerfile: %w", err))
return
}
} else {
if err != nil {
task.SetError(fmt.Errorf("reading dockerfile: %w", err))
return
}
}

if packBuildEnabled && errors.Is(err, os.ErrNotExist) {
// Build the container from source
task.SetProgress(NewServiceProgress("Building Docker image from source"))
res, err := p.packBuild(ctx, serviceConfig, dockerOptions, imageName)
if err != nil {
task.SetError(err)
return
}

res.Restore = restoreOutput
task.SetResult(res)
return
}

// Build the container
task.SetProgress(NewServiceProgress("Building Docker image"))

previewerWriter := p.console.ShowPreviewer(ctx,
&input.ShowPreviewerOptions{
Prefix: " ",
Expand Down Expand Up @@ -230,6 +268,72 @@ func (p *dockerProject) Package(
)
}

// Default builder image to produce container images from source
const DefaultBuilderImage = "paketobuildpacks/builder-jammy-base"

func (p *dockerProject) packBuild(
ctx context.Context,
svc *ServiceConfig,
dockerOptions DockerProjectOptions,
imageName string) (*ServiceBuildResult, error) {
pack, err := pack.NewPackCli(ctx, p.console, p.commandRunner)
if err != nil {
return nil, err
}
builder := DefaultBuilderImage
environ := []string{}

if os.Getenv("AZD_BUILDER_IMAGE") != "" {
builder = os.Getenv("AZD_BUILDER_IMAGE")
}

if builder == DefaultBuilderImage && svc.OutputPath != "" &&
(svc.Language == ServiceLanguageTypeScript ||
svc.Language == ServiceLanguageJavaScript) {
// A dist folder has been set.
// We assume that the service is a front-end service, setting additional configuration to trigger a front-end
// build, with a nginx web server to serve in the run image.
environ = append(environ,
// This is currently not-customizable. We assume the build script is 'build'.
"BP_NODE_RUN_SCRIPTS=build",
"BP_WEB_SERVER=nginx",
"BP_WEB_SERVER_ROOT="+svc.OutputPath,
"BP_WEB_SERVER_ENABLE_PUSH_STATE=true")
}

previewer := p.console.ShowPreviewer(ctx,
&input.ShowPreviewerOptions{
Prefix: " ",
MaxLineCount: 8,
Title: "Docker (pack) Output",
})
err = pack.Build(
ctx,
svc.Path(),
builder,
imageName,
environ,
previewer)
p.console.StopPreviewer(ctx)
if err != nil {
return nil, err
}

imageId, err := p.docker.Inspect(ctx, imageName, "{{.Id}}")
if err != nil {
return nil, err
}
imageId = strings.TrimSpace(imageId)

return &ServiceBuildResult{
BuildOutputPath: imageId,
Details: &dockerBuildResult{
ImageId: imageId,
ImageName: imageName,
},
}, nil
}

func getDockerOptionsWithDefaults(options DockerProjectOptions) DockerProjectOptions {
if options.Path == "" {
options.Path = "./Dockerfile"
Expand Down
47 changes: 42 additions & 5 deletions cli/azd/pkg/project/framework_service_docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package project
import (
"context"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -86,6 +87,12 @@ services:
require.NoError(t, err)
service := projectConfig.Services["web"]

temp := t.TempDir()
service.Project.Path = temp
service.RelativePath = ""
err = os.WriteFile(filepath.Join(temp, "Dockerfile"), []byte("FROM node:14"), 0600)
require.NoError(t, err)

npmCli := npm.NewNpmCli(mockContext.CommandRunner)
docker := docker.NewDocker(mockContext.CommandRunner)

Expand All @@ -95,7 +102,12 @@ services:
progressMessages := []string{}

framework := NewDockerProject(
env, docker, NewContainerHelper(env, clock.NewMock(), nil, docker), mockinput.NewMockConsole())
env,
docker,
NewContainerHelper(env, clock.NewMock(), nil, docker),
mockinput.NewMockConsole(),
mockContext.AlphaFeaturesManager,
mockContext.CommandRunner)
framework.SetSource(internalFramework)

buildTask := framework.Build(*mockContext.Context, service, nil)
Expand Down Expand Up @@ -185,14 +197,24 @@ services:
require.NoError(t, err)

service := projectConfig.Services["web"]
temp := t.TempDir()
service.Project.Path = temp
service.RelativePath = ""
err = os.WriteFile(filepath.Join(temp, "Dockerfile.dev"), []byte("FROM node:14"), 0600)
require.NoError(t, err)

done := make(chan bool)

internalFramework := NewNpmProject(npmCli, env)
status := ""

framework := NewDockerProject(
env, docker, NewContainerHelper(env, clock.NewMock(), nil, docker), mockinput.NewMockConsole())
env,
docker,
NewContainerHelper(env, clock.NewMock(), nil, docker),
mockinput.NewMockConsole(),
mockContext.AlphaFeaturesManager,
mockContext.CommandRunner)
framework.SetSource(internalFramework)

buildTask := framework.Build(*mockContext.Context, service, nil)
Expand Down Expand Up @@ -234,9 +256,19 @@ func Test_DockerProject_Build(t *testing.T) {
env := environment.Ephemeral()
dockerCli := docker.NewDocker(mockContext.CommandRunner)
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
temp := t.TempDir()
serviceConfig.Project.Path = temp
serviceConfig.RelativePath = ""
err := os.WriteFile(filepath.Join(temp, "Dockerfile"), []byte("FROM node:14"), 0600)
require.NoError(t, err)

dockerProject := NewDockerProject(
env, dockerCli, NewContainerHelper(env, clock.NewMock(), nil, dockerCli), mockinput.NewMockConsole())
env,
dockerCli,
NewContainerHelper(env, clock.NewMock(), nil, dockerCli),
mockinput.NewMockConsole(),
mockContext.AlphaFeaturesManager,
mockContext.CommandRunner)
buildTask := dockerProject.Build(*mockContext.Context, serviceConfig, nil)
logProgress(buildTask)

Expand All @@ -245,7 +277,7 @@ func Test_DockerProject_Build(t *testing.T) {
require.NotNil(t, result)
require.Equal(t, "IMAGE_ID", result.BuildOutputPath)
require.Equal(t, "docker", runArgs.Cmd)
require.Equal(t, serviceConfig.RelativePath, runArgs.Cwd)
require.Equal(t, serviceConfig.Path(), runArgs.Cwd)
require.Equal(t,
[]string{
"build",
Expand Down Expand Up @@ -282,7 +314,12 @@ func Test_DockerProject_Package(t *testing.T) {
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)

dockerProject := NewDockerProject(
env, dockerCli, NewContainerHelper(env, clock.NewMock(), nil, dockerCli), mockinput.NewMockConsole())
env,
dockerCli,
NewContainerHelper(env, clock.NewMock(), nil, dockerCli),
mockinput.NewMockConsole(),
mockContext.AlphaFeaturesManager,
mockContext.CommandRunner)
packageTask := dockerProject.Package(
*mockContext.Context,
serviceConfig,
Expand Down
10 changes: 10 additions & 0 deletions cli/azd/pkg/tools/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Docker interface {
) (string, error)
Tag(ctx context.Context, cwd string, imageName string, tag string) error
Push(ctx context.Context, cwd string, tag string) error
Inspect(ctx context.Context, imageName string, format string) (string, error)
}

func NewDocker(commandRunner exec.CommandRunner) Docker {
Expand Down Expand Up @@ -147,6 +148,15 @@ func (d *docker) Push(ctx context.Context, cwd string, tag string) error {
return nil
}

func (d *docker) Inspect(ctx context.Context, imageName string, format string) (string, error) {
out, err := d.executeCommand(ctx, "", "image", "inspect", "--format", format, imageName)
if err != nil {
return "", fmt.Errorf("inspecting image: %w", err)
}

return out.Stdout, nil
}

func (d *docker) versionInfo() tools.VersionInfo {
return tools.VersionInfo{
MinimumVersion: semver.Version{
Expand Down

0 comments on commit ba25a8c

Please sign in to comment.