Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add buildpacks (pack cli) integration #2678

Merged
merged 8 commits into from
Aug 31, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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