| // Package docker is for running Dockerfiles. |
| package docker |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strings" |
| "time" |
| |
| "cloud.google.com/go/pubsub" |
| "github.com/google/uuid" |
| "golang.org/x/oauth2" |
| |
| "go.skia.org/infra/go/auth" |
| docker_pubsub "go.skia.org/infra/go/docker/build/pubsub" |
| sk_exec "go.skia.org/infra/go/exec" |
| "go.skia.org/infra/task_driver/go/lib/log_parser" |
| "go.skia.org/infra/task_driver/go/lib/os_steps" |
| "go.skia.org/infra/task_driver/go/td" |
| ) |
| |
| var ( |
| AUTH_SCOPES = []string{auth.ScopeUserinfoEmail, auth.ScopeFullControl} |
| |
| REPOSITORY_HOST = "gcr.io" |
| |
| // dockerStepRegex is a regex that matches Step lines in Docker output. |
| dockerStepRegex = 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" |
| |
| // imageSha256Regex is used to parse an image sha256 sum from log |
| // output. |
| imageSha256Regex = regexp.MustCompile(`sha256:[a-f0-9]{64}`) |
| ) |
| |
| type Docker struct { |
| configDir string |
| stop chan struct{} |
| } |
| |
| func New(ctx context.Context, ts oauth2.TokenSource) (*Docker, error) { |
| configDir, err := os_steps.TempDir(ctx, "", "") |
| if err != nil { |
| td.Fatal(ctx, err) |
| } |
| stop := make(chan struct{}) |
| ready := make(chan error) |
| go func() { |
| for { |
| now := time.Now() |
| tok, err := ts.Token() |
| if err == nil { |
| err = Login(ctx, tok.AccessToken, REPOSITORY_HOST, configDir) |
| } |
| if ready != nil { |
| ready <- err |
| ready = nil |
| } |
| if err != nil { |
| return |
| } |
| t := time.NewTimer(tok.Expiry.Sub(now)) |
| select { |
| case <-stop: |
| stop <- struct{}{} |
| return |
| case <-t.C: |
| } |
| |
| } |
| }() |
| rv := &Docker{ |
| configDir: configDir, |
| stop: stop, |
| } |
| err = <-ready |
| if err != nil { |
| return nil, err |
| } |
| return rv, nil |
| } |
| |
| func (d *Docker) Cleanup(ctx context.Context) error { |
| return td.Do(ctx, td.Props("Docker Cleanup").Infra(), func(ctx context.Context) error { |
| d.stop <- struct{}{} |
| <-d.stop |
| return os_steps.RemoveAll(ctx, d.configDir) |
| }) |
| } |
| |
| // Pull a Docker image. |
| func (d *Docker) Pull(ctx context.Context, imageWithTag string) error { |
| return Pull(ctx, imageWithTag, d.configDir) |
| } |
| |
| // Push a Docker file. |
| func (d *Docker) Push(ctx context.Context, tag string) (string, error) { |
| return Push(ctx, tag, d.configDir) |
| } |
| |
| // Tag the given Docker image. |
| func (d *Docker) Tag(ctx context.Context, imageID, tag string) error { |
| return Tag(ctx, imageID, tag, d.configDir) |
| } |
| |
| // Run does a "docker run". |
| // |
| // volumes should be in the form of "ARG1:ARG2" where ARG1 is the local directory and ARG2 will be the directory in the image. |
| // Note the above does a --rm i.e. it automatically removes the container when it exits. |
| func (d *Docker) Run(ctx context.Context, image string, cmd, volumes, env []string) error { |
| return Run(ctx, image, d.configDir, cmd, volumes, env) |
| } |
| |
| // Run "docker build" with the given args. |
| func (d *Docker) Build(ctx context.Context, args ...string) error { |
| return Build(ctx, append([]string{"--config", d.configDir, "build"}, args...)...) |
| } |
| |
| // Extract the given src from the given image to the given host dest. |
| func (d *Docker) Extract(ctx context.Context, image, src, dest string) error { |
| return td.Do(ctx, td.Props(fmt.Sprintf("Extract %s %s:%s", image, src, dest)), func(ctx context.Context) (rv error) { |
| // Create a container from the image with a dummy command. |
| containerName := fmt.Sprintf("tmp-%s", uuid.New().String()) |
| cmd := []string{dockerCmd, "--config", d.configDir, "create", "--name", containerName, image, "dummy-cmd"} |
| if _, err := sk_exec.RunCwd(ctx, ".", cmd...); err != nil { |
| return err |
| } |
| |
| // Make sure we remove the container once we're done with it. |
| defer func() { |
| _, err := sk_exec.RunCwd(ctx, ".", dockerCmd, "rm", "-v", containerName) |
| if err != nil { |
| rv = err |
| } |
| }() |
| |
| // Perform the copy. |
| _, err := sk_exec.RunCwd(ctx, ".", dockerCmd, "cp", "-L", fmt.Sprintf("%s:%s", containerName, src), dest) |
| return err |
| }) |
| } |
| |
| // Login to docker to be able to run authenticated commands (Eg: docker.Push). |
| func Login(ctx context.Context, accessToken, hostname, configDir string) error { |
| loginCmd := &sk_exec.Command{ |
| Name: dockerCmd, |
| Args: []string{"--config", configDir, "login", "-u", "oauth2accesstoken", "--password-stdin", hostname}, |
| Stdin: strings.NewReader(accessToken), |
| LogStdout: true, |
| LogStderr: true, |
| } |
| _, err := sk_exec.RunCommand(ctx, loginCmd) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // Pull a Docker image. |
| func Pull(ctx context.Context, imageWithTag, configDir string) error { |
| pullCmd := fmt.Sprintf("%s --config %s pull %s", dockerCmd, configDir, imageWithTag) |
| _, err := sk_exec.RunSimple(ctx, pullCmd) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // Push a Docker image. |
| func Push(ctx context.Context, tag, configDir string) (string, error) { |
| out, err := sk_exec.RunCwd(ctx, ".", dockerCmd, "--config", configDir, "push", tag) |
| if err != nil { |
| return "", err |
| } |
| m := imageSha256Regex.FindStringSubmatch(out) |
| if len(m) == 1 { |
| return m[0], nil |
| } |
| return "", nil |
| } |
| |
| // Tag the given Docker image. |
| func Tag(ctx context.Context, imageID, tag, configDir string) error { |
| _, err := sk_exec.RunCwd(ctx, ".", "docker", "--config", configDir, "tag", imageID, tag) |
| return err |
| } |
| |
| // Run does a "docker run". |
| // |
| // volumes should be in the form of "ARG1:ARG2" where ARG1 is the local directory and ARG2 will be the directory in the image. |
| // Note the above does a --rm i.e. it automatically removes the container when it exits. |
| func Run(ctx context.Context, image, configDir string, cmd, volumes, env []string) error { |
| runArgs := []string{"--config", configDir, "run"} |
| for _, v := range volumes { |
| runArgs = append(runArgs, "--volume", v) |
| } |
| for _, e := range env { |
| runArgs = append(runArgs, "--env", e) |
| } |
| runArgs = append(runArgs, image) |
| runArgs = append(runArgs, cmd...) |
| runCmd := &sk_exec.Command{ |
| Name: dockerCmd, |
| Args: runArgs, |
| LogStdout: true, |
| LogStderr: true, |
| } |
| _, err := sk_exec.RunCommand(ctx, runCmd) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // Build a Dockerfile. |
| // |
| // There must be a Dockerfile in the 'directory' and the resulting output is |
| // tagged with 'tag'. |
| func BuildHelper(ctx context.Context, directory, tag, configDir string, buildArgs map[string]string) error { |
| cmdArgs := []string{"--config", configDir, "build", "--pull", "-t", tag, directory} |
| if buildArgs != nil { |
| for k, v := range buildArgs { |
| cmdArgs = append(cmdArgs, "--build-arg", fmt.Sprintf("%s=%s", k, v)) |
| } |
| } |
| return Build(ctx, cmdArgs...) |
| } |
| |
| // Build runs "docker build <args>" in 'directory' and streams the |
| // output. The log output is parsed into sub-steps for each line starting 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 |
| func Build(ctx context.Context, args ...string) error { |
| return log_parser.RunRegexp(ctx, dockerStepRegex, ".", append([]string{dockerCmd}, args...)) |
| } |
| |
| // BuildPushImageFromInfraImage is a utility function that pulls the infra image, runs the specified |
| // buildCmd on the infra image, builds the specified image+tag, pushes it. After pushing it sends |
| // a pubsub msg signaling completion. |
| func BuildPushImageFromInfraImage(ctx context.Context, appName, image, tag, repo, configDir, workDir, infraImageTag string, topic *pubsub.Topic, cmd, volumes, env []string, buildArgs map[string]string) error { |
| err := td.Do(ctx, td.Props(fmt.Sprintf("Build & Push %s Image", appName)).Infra(), func(ctx context.Context) error { |
| |
| // Make sure we have the specified infra image. |
| infraImageWithTag := fmt.Sprintf("gcr.io/skia-public/infra:%s", infraImageTag) |
| if err := Pull(ctx, infraImageWithTag, configDir); err != nil { |
| return err |
| } |
| // Create the image locally using infraImageWithTag. |
| if err := Run(ctx, infraImageWithTag, configDir, cmd, volumes, env); err != nil { |
| return err |
| } |
| // Build the image using docker. |
| imageWithTag := fmt.Sprintf("%s:%s", image, tag) |
| if err := BuildHelper(ctx, workDir, imageWithTag, configDir, buildArgs); err != nil { |
| return err |
| } |
| // Push the docker image. |
| if _, err := Push(ctx, imageWithTag, configDir); err != nil { |
| return err |
| } |
| // Send pubsub msg. |
| return PublishToTopic(ctx, image, tag, repo, topic) |
| }) |
| return err |
| } |
| |
| // PublishToTopic publishes a message to the pubsub topic which is subscribed to by |
| // https://github.com/google/skia-buildbot/blob/cd593cf6c534ba7a1bd2d88a488d37840663230d/docker_pushes_watcher/go/docker_pushes_watcher/main.go#L335 |
| // The tag will be used to determine if the image should be updated. |
| func PublishToTopic(ctx context.Context, image, tag, repo string, topic *pubsub.Topic) error { |
| return td.Do(ctx, td.Props(fmt.Sprintf("Publish pubsub msg to %s", docker_pubsub.TOPIC)).Infra(), func(ctx context.Context) error { |
| // Publish to the pubsub topic. |
| b, err := json.Marshal(&docker_pubsub.BuildInfo{ |
| ImageName: image, |
| Tag: tag, |
| Repo: repo, |
| }) |
| if err != nil { |
| return err |
| } |
| msg := &pubsub.Message{ |
| Data: b, |
| } |
| res := topic.Publish(ctx, msg) |
| if _, err := res.Get(ctx); err != nil { |
| return err |
| } |
| return nil |
| }) |
| } |