builder/context.go (242 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package builder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"path"
"regexp"
"runtime"
"strings"
"github.com/Azure/acr-builder/graph"
"github.com/Azure/acr-builder/pkg/image"
"github.com/Azure/acr-builder/util"
"github.com/google/uuid"
"github.com/pkg/errors"
)
var (
dependenciesRE = regexp.MustCompile(`(\[{"image.*?\])$`)
)
// getDockerRunArgs populates the args for running a Docker container.
func (b *Builder) getDockerRunArgs(
volMounts map[string]string,
volName string,
workDir string,
disableWorkDirOverride bool,
remove bool,
detach bool,
envs []string,
ports []string,
expose []string,
privilaged bool,
user string,
network string,
isolation string,
cpus string,
entrypoint string,
containerName string,
cmd string) []string {
var args []string
var sb strings.Builder
// Run user commands from a shell instance in order to mirror the shell's field splitting algorithms,
// so we don't have to write our own argv parser for exec.Command.
if runtime.GOOS == util.WindowsOS {
args = []string{"powershell.exe", "-Command"}
} else {
args = []string{"/bin/sh", "-c"}
}
sb.WriteString("docker run")
if remove {
sb.WriteString(" --rm")
}
if detach {
sb.WriteString(" --detach")
}
for _, port := range ports {
sb.WriteString(" -p " + port)
}
for _, exp := range expose {
sb.WriteString(" --expose " + exp)
}
if privilaged {
sb.WriteString(" --privileged")
}
if user != "" {
sb.WriteString(" --user " + user)
}
if network != "" {
sb.WriteString(" --network " + network)
}
if isolation != "" {
sb.WriteString(" --isolation " + isolation)
}
if cpus != "" {
sb.WriteString(" --cpus " + cpus)
}
if entrypoint != "" {
sb.WriteString(" --entrypoint " + entrypoint)
}
sb.WriteString(" --name " + containerName)
sb.WriteString(" --volume " + volName + ":" + containerWorkspaceDir)
sb.WriteString(" --volume " + util.DockerSocketVolumeMapping)
sb.WriteString(" --volume " + homeVol + ":" + homeWorkDir)
if len(volMounts) > 0 {
for key, val := range volMounts {
sb.WriteString(" --volume " + key + ":" + val)
}
}
sb.WriteString(" --env " + homeEnv)
// User environment variables come after any defaults.
// This allows overriding the HOME environment variable for a step.
// NB: this has the assumption that the underlying runtime handles the case of duplicated
// environment variables by only keeping the last specified.
for _, env := range envs {
sb.WriteString(" --env " + env)
}
if !disableWorkDirOverride {
sb.WriteString(" --workdir " + normalizeWorkDir(workDir))
}
sb.WriteString(" " + cmd)
args = append(args, sb.String())
return args
}
// getDockerRunArgsForStep populates the args for running a Docker container for the step.
func (b *Builder) getDockerRunArgsForStep(
volName string,
stepWorkDir string,
step *graph.Step,
entrypoint string,
cmd string) []string {
// Run user commands from a shell instance in order to mirror the shell's field splitting algorithms,
// so we don't have to write our own argv parser for exec.Command.
if runtime.GOOS == util.WindowsOS && step.Isolation == "" && !step.IsBuildStep() {
// Use hyperv isolation for non-build steps.
// Use default isolation for build step to improve performance. It assumes the docker-cli image is compatible with the host os.
step.Isolation = "hyperv"
}
if runtime.GOOS == util.WindowsOS && step.IsBuildStep() {
// Limit build command container cpu to 1
step.CPUS = "1"
}
var volMounts = make(map[string]string)
for _, mount := range step.Mounts {
volMounts[mount.Name] = mount.MountPath
}
return b.getDockerRunArgs(
volMounts,
volName,
stepWorkDir,
step.DisableWorkingDirectoryOverride,
!step.Keep,
step.Detach,
step.Envs,
step.Ports,
step.Expose,
step.Privileged,
step.User,
step.Network,
step.Isolation,
step.CPUS,
entrypoint,
step.ID,
cmd,
)
}
func (b *Builder) scrapeDependencies(
ctx context.Context,
volName string,
stepWorkDir string,
outputDir string,
dockerfile string,
sourceContext string,
tags []string,
buildArgs []string,
target string,
credentials []*graph.RegistryCredential) ([]*image.Dependencies, error) {
containerName := fmt.Sprintf("acb_dep_scanner_%s", uuid.New())
args, censoredArgs, err := getScanArgs(
containerName,
volName,
containerWorkspaceDir,
stepWorkDir,
dockerfile,
outputDir,
tags,
buildArgs,
target,
sourceContext,
credentials)
if err != nil {
return nil, err
}
if b.debug {
log.Printf("Scan args: %v\n", censoredArgs)
}
var buf bytes.Buffer
err = b.procManager.Run(ctx, args, nil, &buf, &buf, "")
output := strings.TrimSpace(buf.String())
if err != nil {
log.Printf("Output from dependency scanning: %s\n", output)
return nil, err
}
return getImageDependencies(output)
}
func getScanArgs(
containerName string,
volName string,
containerWorkspaceDir string,
stepWorkDir string,
dockerfile string,
outputDir string,
tags []string,
buildArgs []string,
target string,
sourceContext string,
credentials []*graph.RegistryCredential) ([]string, []string, error) {
args := []string{
"docker",
"run",
"--rm",
"--name", containerName,
"--volume", volName + ":" + containerWorkspaceDir,
"--workdir", normalizeWorkDir(stepWorkDir),
// Mount home
"--volume", homeVol + ":" + homeWorkDir,
"--env", homeEnv,
scannerImageName,
"scan",
"-f", dockerfile,
"--destination", outputDir,
}
for _, tag := range tags {
args = append(args, "-t", tag)
}
for _, buildArg := range buildArgs {
args = append(args, "--build-arg", buildArg)
}
var censoredArgs = make([]string, len(args))
copy(censoredArgs, args)
for _, cred := range credentials {
serializedCredential, err := cred.String()
if err != nil {
return nil, nil, errors.New("credential serialization failed for given registry credential")
}
censoredArgs = append(censoredArgs, "--credential", "***")
args = append(args, "--credential", serializedCredential)
}
if len(target) > 0 {
censoredArgs = append(censoredArgs, "--target", target)
args = append(args, "--target", target)
}
// Positional context must appear last
censoredArgs = append(censoredArgs, sourceContext)
args = append(args, sourceContext)
return args, censoredArgs, nil
}
func getImageDependencies(s string) ([]*image.Dependencies, error) {
var deps []*image.Dependencies
lines := strings.Split(s, "\n")
for _, line := range lines {
matches := dependenciesRE.FindStringSubmatch(line)
if len(matches) == 2 {
err := json.Unmarshal([]byte(matches[1]), &deps)
if err != nil {
return nil, err
}
break
}
}
return deps, nil
}
// normalizeWorkDir normalizes a working directory.
func normalizeWorkDir(workDir string) string {
// If the directory is absolute, use it instead of /workspace
if path.IsAbs(workDir) {
return path.Clean(workDir)
}
return path.Clean(path.Join(containerWorkspaceDir, workDir))
}