builder/builder.go (413 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package builder import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "log" "os" "runtime" "strings" "time" "github.com/Azure/acr-builder/graph" "github.com/Azure/acr-builder/pkg/image" "github.com/Azure/acr-builder/pkg/procmanager" "github.com/Azure/acr-builder/pkg/volume" "github.com/Azure/acr-builder/util" "github.com/pkg/errors" ) const ( dockerImg = "docker" buildxImg = "buildx" ) // Builder builds images. type Builder struct { procManager *procmanager.ProcManager workspaceDir string debug bool } // NewBuilder creates a new Builder. func NewBuilder(pm *procmanager.ProcManager, debug bool, workspaceDir string) *Builder { return &Builder{ procManager: pm, debug: debug, workspaceDir: workspaceDir, } } // RunTask executes a Task. func (b *Builder) RunTask(ctx context.Context, task *graph.Task) error { for _, network := range task.Networks { if network.SkipCreation { log.Printf("Skip creating network: %s\n", network.Name) continue } log.Printf("Creating Docker network: %s, driver: '%s'\n", network.Name, network.Driver) if msg, err := network.Create(ctx, b.procManager); err != nil { return fmt.Errorf("failed to create network: %s, err: %v, msg: %s", network.Name, err, msg) } log.Printf("Successfully set up Docker network: %s\n", network.Name) } log.Println("Setting up Docker configuration...") timeout := time.Duration(configTimeoutInSec) * time.Second configCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() if err := b.setupConfig(configCtx); err != nil { return err } log.Println("Successfully set up Docker configuration") if task.UsingRegistryCreds() { timeout := time.Duration(loginTimeoutInSec) * time.Second for registry, cred := range task.RegistryLoginCredentials { loginCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() log.Printf("Logging in to registry: %s\n", registry) if err := b.dockerLoginWithRetries(loginCtx, registry, cred.Username.ResolvedValue, cred.Password.ResolvedValue, 0); err != nil { return err } log.Printf("Successfully logged into %s\n", registry) } } var completedChans []chan bool errorChan := make(chan error) for _, node := range task.Dag.Nodes { completedChans = append(completedChans, node.Value.CompletedChan) } if task.InitBuildkitContainer { log.Println("Task will use build cache, initializing buildkitd container") // --workdir = /workspace args := b.getDockerRunArgs( make(map[string]string), b.workspaceDir, "", false, true, true, []string{}, []string{}, []string{}, false, "", "", "", "", "", buildkitdContainerName, buildxImg+" create --use", ) if b.debug { log.Printf("buildkitd container args: %v\n", strings.Join(args, ", ")) } timeout := time.Duration(buildkitdContainerRunTimeoutInSeconds) * time.Second buildkitCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() err := b.procManager.RunRepeatWithRetries( buildkitCtx, args, nil, os.Stdout, os.Stderr, "", buildkitdContainerInitRetries, nil, buildkitdContainerInitRetryDelay, buildkitdContainerName, buildkitdContainerInitRepeat) if err != nil { log.Printf("buildx create --use failed with error: '%v'", err) } } for _, volMount := range task.Volumes { // create and populate volume for specified source if err := b.prepareVolumeSource(ctx, volMount); err != nil { return err } } for _, child := range task.Dag.Root.Children() { go b.processVertex(ctx, task, task.Dag.Root, child, errorChan) } // Block until either: // - The global context expires // - A step has an error // - All steps have been processed for _, ch := range completedChans { select { case <-ctx.Done(): return ctx.Err() case <-ch: continue case err := <-errorChan: return err } } var deps []*image.Dependencies for _, step := range task.Steps { log.Printf("Step ID: %v marked as %v (elapsed time in seconds: %f)\n", step.ID, step.StepStatus, step.EndTime.Sub(step.StartTime).Seconds()) if len(step.ImageDependencies) > 0 { log.Printf("Populating digests for step ID: %s...\n", step.ID) timeout := time.Duration(digestsTimeoutInSec) * time.Second digestCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() usingBuildkit := false if (step.UseBuildCacheForBuildStep() && runtime.GOOS == util.LinuxOS) || step.UsesBuildkit { log.Printf("Image was built using buildkit, fetching Digest from remote...") usingBuildkit = true } if err := b.getPopulateDigests(digestCtx, step.ImageDependencies, usingBuildkit, task.RegistryLoginCredentials); err != nil { return err } log.Printf("Successfully populated digests for step ID: %s\n", step.ID) deps = append(deps, step.ImageDependencies...) } } if len(deps) > 0 { depBytes, err := json.Marshal(deps) if err != nil { return errors.Wrap(err, "failed to unmarshal image dependencies") } log.Println("The following dependencies were found:") log.Println("\n" + string(depBytes)) } return nil } // CleanTask iterates through all build steps and removes // their corresponding containers. func (b *Builder) CleanTask(ctx context.Context, task *graph.Task) { args := []string{"docker", "rm", "-f"} for _, n := range task.Dag.Nodes { step := n.Value if step.StepStatus != graph.Skipped { killArgs := append(args, step.ID) _ = b.procManager.Run(ctx, killArgs, nil, nil, nil, "") } } for _, network := range task.Networks { if network.SkipCreation { log.Printf("Skip deleting network: %s\n", network.Name) continue } if msg, err := network.Delete(ctx, b.procManager); err != nil { log.Printf("Failed to delete network: %s, err: %v, msg: %s\n", network.Name, err, msg) } } _ = b.procManager.Stop() } func (b *Builder) processVertex(ctx context.Context, task *graph.Task, parent *graph.Node, child *graph.Node, errorChan chan error) { err := task.Dag.RemoveEdge(parent.Name, child.Name) if err != nil { errorChan <- errors.Wrap(err, "failed to remove edge") return } degree := child.GetDegree() if degree == 0 { step := child.Value err := b.runStep(ctx, step, task.Credentials) if err != nil && step.IgnoreErrors { log.Printf("Step ID: %s encountered an error: %v, but is set to ignore errors. Continuing...\n", step.ID, err) step.StepStatus = graph.Successful for _, c := range child.Children() { go b.processVertex(ctx, task, child, c, errorChan) } } else if err != nil { step.StepStatus = graph.Failed errorChan <- errors.Wrapf(err, "failed to run step ID: %s", step.ID) } else { step.StepStatus = graph.Successful for _, c := range child.Children() { go b.processVertex(ctx, task, child, c, errorChan) } } // Step must always be marked as complete. step.CompletedChan <- true } } func (b *Builder) runStep(ctx context.Context, step *graph.Step, credentials []*graph.RegistryCredential) error { log.Printf("Executing step ID: %s. Timeout(sec): %d, Working directory: '%s', Network: '%s'\n", step.ID, step.Timeout, step.WorkingDirectory, step.Network) if step.StartDelay > 0 { log.Printf("Waiting %d seconds before executing step ID: %s\n", step.StartDelay, step.ID) time.Sleep(time.Duration(step.StartDelay) * time.Second) } if step.IsCmdStep() && step.Pull { log.Printf("Step specified pull. Performing an explicit pull...\n") if err := b.pullImageBeforeRun(ctx, step.Cmd, step.CmdDownloadRetries, step.CmdDownloadRetryDelayInSeconds); err != nil { return err } } step.StepStatus = graph.InProgress step.StartTime = time.Now() defer func() { step.EndTime = time.Now() }() var args []string if step.IsBuildStep() { dockerfile, target, dockerContext := parseDockerBuildCmd(step.Build) volName := b.workspaceDir // Print out a warning message if a remote context doesn't appear to be valid, i.e. doesn't end with .git. validateDockerContext(dockerContext) log.Println("Scanning for dependencies...") timeout := time.Duration(scrapeTimeoutInSec) * time.Second scrapeCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() deps, err := b.scrapeDependencies(scrapeCtx, volName, step.WorkingDirectory, step.ID, dockerfile, dockerContext, step.Tags, step.BuildArgs, target, credentials) if err != nil { return errors.Wrap(err, "failed to scan dependencies") } log.Println("Successfully scanned dependencies") step.ImageDependencies = deps workingDirectory := step.WorkingDirectory // Modify the Run command if it's a tar or a git URL. if !util.IsLocalContext(dockerContext) { // NB: use step.ID as the working directory if the context is remote, // since we obtained the source code from the scanner and put it in this location. // If the remote context also has additional context specified, we have to append it // to the working directory. if util.IsSourceControlURL(dockerContext) { workingDirectory = step.ID + "/" + getContextFromGitURL(dockerContext) } else { workingDirectory = step.ID } step.Build = replacePositionalContext(step.Build, ".") } step.UpdateBuildStepWithDefaults() if step.UseBuildCacheForBuildStep() { args = b.getDockerRunArgsForStep(volName, workingDirectory, step, "", buildxImg+" build "+step.Build) } else { args = b.getDockerRunArgsForStep(volName, workingDirectory, step, "", dockerImg+" build "+step.Build) } } else if step.IsPushStep() { timeout := time.Duration(step.Timeout) * time.Second pushCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() return b.pushWithRetries(pushCtx, step.Push) } else { args = b.getDockerRunArgsForStep(b.workspaceDir, step.WorkingDirectory, step, step.EntryPoint, step.Cmd) } if b.debug { log.Printf("Step args: %v\n", strings.Join(args, ", ")) } timeout := time.Duration(step.Timeout) * time.Second stepCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() return b.procManager.RunRepeatWithRetries( stepCtx, args, nil, os.Stdout, os.Stderr, "", step.Retries, step.RetryOnErrors, step.RetryDelayInSeconds, step.ID, step.Repeat) } // getPopulateDigests populates digests on dependencies func (b *Builder) getPopulateDigests(ctx context.Context, dependencies []*image.Dependencies, usingBuildkit bool, registryCreds graph.RegistryLoginCredentials) error { dockerStoreDigester := newDockerStoreDigest(b.procManager, b.debug) var baseImgDigester DigestHelper baseImgDigester = dockerStoreDigester if usingBuildkit { baseImgDigester = newRemoteDigest(registryCreds) } for _, entry := range dependencies { // Always check 'entry.Image' in the Docker store, // If it was pushed, 'docker inspect' will return a Digest, if not, it will return empty. if err := dockerStoreDigester.PopulateDigest(ctx, entry.Image); err != nil { return err } if err := baseImgDigester.PopulateDigest(ctx, entry.Runtime); err != nil { return err } for _, buildtime := range entry.Buildtime { if err := baseImgDigester.PopulateDigest(ctx, buildtime); err != nil { return err } } } return nil } func validateDockerContext(sourceContext string) { sourceContext = strings.ToLower(sourceContext) if strings.Contains(sourceContext, "github") && !strings.Contains(sourceContext, ".git") { log.Printf("WARNING: %s might not be valid context. Valid Git repositories should end with .git.\n", sourceContext) } } func (b *Builder) pullImageBeforeRun(ctx context.Context, cmdArgs string, retries, retryDelayInSeconds int) error { imageName := parseImageNameFromArgs(cmdArgs) args := []string{ "docker", "run", "--rm", "--volume", util.DockerSocketVolumeMapping, "docker", "pull", imageName, } if b.debug { log.Printf("pull image args: %v\n", args) } return b.procManager.RunWithRetries(ctx, args, nil, os.Stdout, os.Stdout, "", retries, nil, retryDelayInSeconds, "") } // parseImageNameFromArgs parses an image's name from a command step's arguments. func parseImageNameFromArgs(cmdArgs string) string { idx := strings.Index(cmdArgs, " ") if idx < 0 { return cmdArgs } return cmdArgs[:idx] } // prepareVolumeSource creates and populates the host file and volume for the specified source type func (b *Builder) prepareVolumeSource(ctx context.Context, volMount *volume.Volume) error { switch { case volMount.Source.Secret != nil: if err := b.createSecretFiles(ctx, volMount); err != nil { return err } if err := b.populateSecretVolume(ctx, volMount); err != nil { return err } log.Println("Volume source " + volMount.Name + " successfully created") return nil default: return errors.New("volume source type not supported yet") } } // createSecretFiles creates necessary files for source type Secret func (b *Builder) createSecretFiles(ctx context.Context, volMount *volume.Volume) error { var args []string args = getShell() args = append(args, "mkdir "+volMount.Name) var buf bytes.Buffer if err := b.procManager.Run(ctx, args, nil, &buf, &buf, ""); err != nil { return errors.Wrapf(err, "failed to make directory, %s", buf.String()) } for k, v := range volMount.Source.Secret { var sb strings.Builder args = getShell() val := v decoded, err := base64.StdEncoding.DecodeString(val) if err != nil { return errors.New("failed to decode Base64 value. please make sure value provided is Base64 encoded") } val = string(decoded) if runtime.GOOS == util.WindowsOS { sb.WriteString("Add-Content -Path ") sb.WriteString(volMount.Name + "/" + k) sb.WriteString(" -Value @\"\r\n") sb.WriteString(val) sb.WriteString("\r\n\"@") } else { sb.WriteString("cat >> ") sb.WriteString(volMount.Name + "/" + k) sb.WriteString(" <<EOL\n") sb.WriteString(val) sb.WriteString("\nEOL") } args = append(args, sb.String()) var buf bytes.Buffer if err := b.procManager.Run(ctx, args, nil, &buf, &buf, ""); err != nil { return errors.Wrapf(err, "failed to write value, %s", buf.String()) } } return nil } // populateSecretVolume mounts all files of type source Secret generated into a volume func (b *Builder) populateSecretVolume(ctx context.Context, volMount *volume.Volume) error { var dataContainerArgs []string var dataSB strings.Builder dataContainerArgs = getShell() if runtime.GOOS == util.WindowsOS { dataSB.WriteString("docker run --rm -v " + b.workspaceDir + ":c:\\source -v ") dataSB.WriteString(volMount.Name + ":c:\\dest -w c:\\source ") dataSB.WriteString(configImageName + " cmd.exe /c copy c:\\source\\" + volMount.Name + " c:\\dest") } else { dataSB.WriteString("docker run --rm -v " + b.workspaceDir + ":/source -v ") dataSB.WriteString(volMount.Name + ":/dest -w /source " + configImageName + " cp ") for k := range volMount.Source.Secret { dataSB.WriteString(volMount.Name + "/" + k) dataSB.WriteString(" ") } dataSB.WriteString("/dest") } dataContainerArgs = append(dataContainerArgs, dataSB.String()) var buf bytes.Buffer if err := b.procManager.Run(ctx, dataContainerArgs, nil, &buf, &buf, ""); err != nil { return errors.Wrapf(err, "failed to populate container, %s", buf.String()) } return nil }