Path: blob/main/components/image-builder-bob/pkg/builder/builder.go
2499 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package builder56import (7"bytes"8"context"9"fmt"10"io/ioutil"11"os"12"os/exec"13"path/filepath"14"runtime"15"syscall"16"time"1718"github.com/gitpod-io/gitpod/common-go/log"1920"github.com/docker/cli/cli/config/configfile"21"github.com/docker/cli/cli/config/types"22"github.com/google/go-containerregistry/pkg/crane"23"github.com/google/go-containerregistry/pkg/logs"24"github.com/moby/buildkit/client"25"golang.org/x/xerrors"26)2728const (29buildkitdSocketPath = "unix:///run/buildkit/buildkitd.sock"30// maxConnectionAttempts is the number of attempts to try to connect to the buildkit daemon.31// Uses exponential backoff to retry. 8 attempts is a bit over 4 minutes.32maxConnectionAttempts = 833initialConnectionTimeout = 2 * time.Second34)3536// Builder builds images using buildkit37type Builder struct {38Config *Config39}4041// Build runs the actual image build42func (b *Builder) Build() error {43var (44cl *client.Client45teardown func() error = func() error { return nil }46err error47)48if b.Config.ExternalBuildkitd != "" {49log.WithField("socketPath", b.Config.ExternalBuildkitd).Info("using external buildkit daemon")50cl, err = connectToBuildkitd(b.Config.ExternalBuildkitd)5152if err != nil {53log.Warn("cannot connect to node-local buildkitd - falling back to pod-local one")54cl, teardown, err = StartBuildkit(buildkitdSocketPath)55}56} else {57cl, teardown, err = StartBuildkit(buildkitdSocketPath)58}59if err != nil {60return err61}62defer teardown()6364ctx, cancel := context.WithCancel(context.Background())65defer cancel()6667err = b.buildBaseLayer(ctx, cl)68if err != nil {69return err70}71err = b.buildWorkspaceImage(ctx)72if err != nil {73return err74}7576return nil77}7879func (b *Builder) buildBaseLayer(ctx context.Context, cl *client.Client) error {80if !b.Config.BuildBase {81return nil82}8384log.Info("building base image")85return buildImage(ctx, b.Config.ContextDir, b.Config.Dockerfile, b.Config.WorkspaceLayerAuth, b.Config.BaseRef)86}8788func (b *Builder) buildWorkspaceImage(ctx context.Context) (err error) {89log.Info("building workspace image")9091logs.Progress.SetOutput(os.Stderr)9293return crane.Copy(b.Config.BaseRef, b.Config.TargetRef, crane.Insecure, crane.WithJobs(runtime.GOMAXPROCS(0)))94}9596func buildImage(ctx context.Context, contextDir, dockerfile, authLayer, target string) (err error) {97log.Info("waiting for build context")98waitctx, cancel := context.WithTimeout(ctx, 30*time.Minute)99defer cancel()100101err = waitForBuildContext(waitctx)102if err != nil {103return err104}105106dockerConfig := "/tmp/config.json"107defer os.Remove(dockerConfig)108109if authLayer != "" {110configFile := configfile.ConfigFile{111AuthConfigs: make(map[string]types.AuthConfig),112}113114err := configFile.LoadFromReader(bytes.NewReader([]byte(fmt.Sprintf(`{"auths": %v }`, authLayer))))115if err != nil {116return xerrors.Errorf("unexpected error reading registry authentication: %w", err)117}118119f, _ := os.OpenFile(dockerConfig, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)120defer f.Close()121122err = configFile.SaveToWriter(f)123if err != nil {124return xerrors.Errorf("unexpected error writing registry authentication: %w", err)125}126}127128contextdir := contextDir129if contextdir == "" {130contextdir = "."131}132133buildctlArgs := []string{134// "--debug",135"build",136"--progress=plain",137"--output=type=image,name=" + target + ",push=true,oci-mediatypes=true",138//"--export-cache=type=inline",139"--local=context=" + contextdir,140//"--export-cache=type=registry,ref=" + target + "-cache",141//"--import-cache=type=registry,ref=" + target + "-cache",142"--frontend=dockerfile.v0",143"--local=dockerfile=" + filepath.Dir(dockerfile),144"--opt=filename=" + filepath.Base(dockerfile),145}146147buildctlCmd := exec.Command("buildctl", buildctlArgs...)148149buildctlCmd.Stderr = os.Stderr150buildctlCmd.Stdout = os.Stdout151152env := os.Environ()153env = append(env, "DOCKER_CONFIG=/tmp")154// set log max size to 4MB from 2MB default (to prevent log clipping for large builds)155env = append(env, "BUILDKIT_STEP_LOG_MAX_SIZE=4194304")156buildctlCmd.Env = env157158if err := buildctlCmd.Start(); err != nil {159return err160}161162err = buildctlCmd.Wait()163if err != nil {164return err165}166167return nil168}169170func waitForBuildContext(ctx context.Context) error {171done := make(chan struct{})172173go func() {174for {175if ctx.Err() != nil {176return177}178179if _, err := os.Stat("/workspace/.gitpod/ready"); err != nil {180continue181}182183close(done)184return185}186}()187188select {189case <-ctx.Done():190return ctx.Err()191case <-done:192return nil193}194}195196// StartBuildkit starts a local buildkit daemon197func StartBuildkit(socketPath string) (cl *client.Client, teardown func() error, err error) {198stderr, err := ioutil.TempFile(os.TempDir(), "buildkitd_stderr")199if err != nil {200return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)201}202stdout, err := ioutil.TempFile(os.TempDir(), "buildkitd_stdout")203if err != nil {204return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)205}206207cmd := exec.Command("buildkitd",208"--debug",209"--addr="+socketPath,210"--oci-worker-net=host",211"--root=/workspace/buildkit",212)213cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 0, Gid: 0}}214cmd.Stderr = stderr215cmd.Stdout = stdout216err = cmd.Start()217if err != nil {218return nil, nil, xerrors.Errorf("cannot start buildkitd: %w", err)219}220log.WithField("stderr", stderr.Name()).WithField("stdout", stdout.Name()).Debug("buildkitd started")221222defer func() {223if err == nil {224return225}226227if cmd.Process != nil {228_ = cmd.Process.Kill()229}230231serr, _ := ioutil.ReadFile(stderr.Name())232sout, _ := ioutil.ReadFile(stdout.Name())233234log.WithField("buildkitd-stderr", string(serr)).WithField("buildkitd-stdout", string(sout)).Error("buildkitd failure")235}()236237teardown = func() error {238stdout.Close()239stderr.Close()240return cmd.Process.Kill()241}242243cl, err = connectToBuildkitd(socketPath)244if err != nil {245return246}247248return249}250251func connectToBuildkitd(socketPath string) (cl *client.Client, err error) {252backoff := 1 * time.Second253for i := 0; i < maxConnectionAttempts; i++ {254ctx, cancel := context.WithTimeout(context.Background(), initialConnectionTimeout)255256log.WithField("attempt", i).Debug("attempting to connect to buildkitd")257cl, err = client.New(ctx, socketPath, client.WithFailFast())258if err != nil {259cancel()260if i == maxConnectionAttempts-1 {261log.WithField("attempt", i).WithError(err).Warn("cannot connect to buildkitd")262break263}264265time.Sleep(backoff)266backoff = 2 * backoff267continue268}269270_, err = cl.ListWorkers(ctx)271if err != nil {272cancel()273if i == maxConnectionAttempts-1 {274log.WithField("attempt", i).WithError(err).Error("cannot connect to buildkitd")275break276}277278time.Sleep(backoff)279backoff = 2 * backoff280continue281}282283cancel()284return285}286287return nil, xerrors.Errorf("cannot connect to buildkitd")288}289290291