blob: 181dd895e6bd9bdddc2998ae08b615ff076b15d0 [file] [log] [blame]
// Package docker is for running Dockerfiles.
package docker
import (
"bufio"
"context"
"fmt"
"os/exec"
"regexp"
"sync"
"go.skia.org/infra/task_driver/go/td"
)
var (
// dockerStepPrefix is a regex that matches Step lines in Docker output.
dockerStepPrefix = regexp.MustCompile(`^Step \d+\/\d+ : `)
// dockerCmd is the name of the executable to run Docker. A variable so we
// can change it at test time.
dockerCmd = "docker"
)
// Build a Dockerfile.
//
// There must be a Dockerfile in the 'directory' and the resulting output is
// tagged with 'tag'.
func Build(ctx context.Context, directory string, tag string) error {
ctx = td.StartStep(ctx, td.Props(fmt.Sprintf("docker build -t %s %s", tag, directory)))
defer td.EndStep(ctx)
// Runs "docker build -t <some tag name> ." in 'directory' and streams the
// output.
// Parse the output of the build.
// Parse lines that start with "Step N/M : ACTION value"
//
// Examples:
// Step 1/7 : FROM debian:testing-slim
// ---> e205e0c9e7f5
// Step 2/7 : RUN apt-get update && apt-get upgrade -y && apt-get install -y git python curl
// ---> Using cache
// ---> 5b8240d40b63
// OR
// Step 2/7 : RUN apt-get update && apt-get upgrade -y && apt-get install -y git python curl
// ---> Running in 9402d36e7474
// Step 3/7 : RUN mkdir -p --mode=0777 /workspace/__cache
// Step 5/7 : ENV CIPD_CACHE_DIR /workspace/__cache
// Step 6/7 : USER skia
cmd := exec.CommandContext(ctx, dockerCmd, "build", "-t", tag, directory)
cmd.Dir = directory
cmd.Env = append(cmd.Env, td.GetEnv(ctx)...)
stdOut, err := cmd.StdoutPipe()
if err != nil {
return td.FailStep(ctx, err)
}
if err := cmd.Start(); err != nil {
return td.FailStep(ctx, err)
}
logStream := td.NewLogStream(ctx, "docker", td.Info)
scanner := bufio.NewScanner(stdOut)
// Spin up a Go routine to parse the step output of the Docker build and
// funnel that into the right logs.
// wg let's us wait for the Go routine to finish.
var wg sync.WaitGroup
wg.Add(1)
// logStreamError records any errors that occur writing to the logStream.
var logStreamError error = nil
go func() {
var subStepContext context.Context = nil
for scanner.Scan() {
line := scanner.Text()
// If this matches the regex then StartStep, EndStep the last step,
// and create a new associated log for the new step.
if dockerStepPrefix.MatchString(line) {
if subStepContext != nil {
td.EndStep(subStepContext)
}
subStepContext = td.StartStep(ctx, td.Props(line))
logStream = td.NewLogStream(subStepContext, line, td.Info)
} else {
// Otherwise just write the log line to the current logStream.
if _, err := logStream.Write([]byte(line)); err != nil {
logStreamError = err
}
}
}
// Now that we've processed all output, End the current step.
if subStepContext != nil {
td.EndStep(subStepContext)
}
wg.Done()
}()
// Wait for command to finish.
if err := cmd.Wait(); err != nil {
// Wait for log processing Go routine to finish.
wg.Wait()
return td.FailStep(ctx, err)
}
// Wait for log processing Go routine to finish.
wg.Wait()
if logStreamError != nil {
return td.FailStep(ctx, logStreamError)
}
return nil
}