diff --git a/cli/cmd/docker/builder.go b/cli/cmd/docker/builder.go index 6c51d302a5..73d819c02b 100644 --- a/cli/cmd/docker/builder.go +++ b/cli/cmd/docker/builder.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "os" "os/exec" "path/filepath" @@ -37,7 +38,9 @@ type BuildOpts struct { LogFile *os.File } -// BuildLocal +// BuildLocal builds the image via docker +// If the DOCKER_BUILDKIT environment variable is set, builds will switch to +// using the docker binary directly (with buildkit enabled) func (a *Agent) BuildLocal(ctx context.Context, opts *BuildOpts) (err error) { if os.Getenv("DOCKER_BUILDKIT") == "1" { return buildLocalWithBuildkit(ctx, *opts) @@ -178,6 +181,7 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl } func buildLocalWithBuildkit(ctx context.Context, opts BuildOpts) error { + fmt.Println("Triggering build via buildkit") if _, err := exec.LookPath("docker"); err != nil { return fmt.Errorf("unable to find docker binary in PATH for buildkit build: %w", err) } @@ -203,12 +207,35 @@ func buildLocalWithBuildkit(ctx context.Context, opts BuildOpts) error { extraDockerArgs = parsedFields } + cacheFrom := fmt.Sprintf("%s:%s", opts.ImageRepo, opts.CurrentTag) + cacheTo := "" + if ok, _ := isRunningInGithubActions(); ok && os.Getenv("BUILDKIT_CACHE_EXPORTER") == "gha" { + fmt.Println("Github Actions environment detected, switching to the GitHub Actions cache exporter") + cacheFrom = "type=gha" + cacheTo = "type=gha" + + // CacheMode is set separately to avoid cases where builds may timeout for + // dockerfiles with many layers. + // See https://github.com/moby/buildkit/issues/2276 for details. + cacheMode := os.Getenv("BUILDKIT_CACHE_MODE") + if cacheMode == "min" || cacheMode == "max" { + fmt.Printf("Setting GHA cache mode to %s\n", cacheMode) + cacheTo = fmt.Sprintf("type=gha,mode=%s", cacheMode) + } else if cacheMode != "" { + return errors.New("error while parsing buildkit environment variables: BUILDKIT_CACHE_MODE set to invalid value, valid values: min, max") + } + } + commandArgs := []string{ "build", "-f", dockerfileName, "--tag", fmt.Sprintf("%s:%s", opts.ImageRepo, opts.Tag), - "--cache-from", fmt.Sprintf("%s:%s", opts.ImageRepo, opts.CurrentTag), + "--cache-from", cacheFrom, } + if cacheTo != "" { + commandArgs = append(commandArgs, "--cache-to", cacheTo) + } + for key, val := range opts.Env { commandArgs = append(commandArgs, "--build-arg", fmt.Sprintf("%s=%s", key, val)) } @@ -313,3 +340,33 @@ func sliceContainsString(haystack []string, needle string) bool { return false } + +// isRunningInGithubActions detects if the environment is a github actions +// runner environment by validating certain environment variables and then +// making a call to the Github api to verify the run itself. +func isRunningInGithubActions() (bool, error) { + for _, key := range []string{"CI", "GITHUB_RUN_ID", "GITHUB_TOKEN", "GITHUB_REPOSITORY"} { + if key == "" { + return false, nil + } + } + + url := fmt.Sprintf("https://api.github.com/repos/%s/actions/runs/%s", os.Getenv("GITHUB_REPOSITORY"), os.Getenv("GITHUB_RUN_ID")) + + req, err := http.NewRequest("GET", url, nil) + if err == nil { + return false, err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("GITHUB_TOKEN"))) + + client := http.Client{ + Timeout: 5 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() //nolint:errcheck + + return resp.StatusCode == http.StatusOK, nil +}