executors/docker/internal/prebuilt/prebuilt.go (146 lines of code) (raw):

package prebuilt import ( "context" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "runtime" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-runner/helpers/container/helperimage" "gitlab.com/gitlab-org/gitlab-runner/helpers/docker" "gitlab.com/gitlab-org/gitlab-runner/helpers/homedir" ) const ( prebuiltExportImageExtension = ".tar.xz" prebuiltDockerArchiveImageExtension = ".docker.tar.zst" ) var PrebuiltImagesPaths []string func init() { runner, err := os.Executable() if err != nil { logrus.Errorln( "Docker executor: unable to detect gitlab-runner folder, "+ "prebuilt image helpers will be loaded from remote registry.", err, ) } runnerFolder := filepath.Dir(runner) PrebuiltImagesPaths = []string{ // When gitlab-runner is running from repository root filepath.Join(runnerFolder, "out/helper-images"), // When gitlab-runner is running from `out/binaries` filepath.Join(runnerFolder, "../helper-images"), // Add working directory path, used when running from temp directory, such as with `go run` filepath.Join(homedir.New().GetWDOrEmpty(), "out/helper-images"), } if runtime.GOOS == "linux" { // This section covers the Linux packaged app scenario, with the binary in /usr/bin. // The helper images are located in /usr/lib/gitlab-runner/helper-images, // as part of the packaging done in the create_package function in ci/package PrebuiltImagesPaths = append( PrebuiltImagesPaths, filepath.Join(runnerFolder, "../lib/gitlab-runner/helper-images"), ) } } func Get(ctx context.Context, client docker.Client, info helperimage.Info) (*types.ImageInspect, error) { if err := load(ctx, client, info); err != nil { return nil, err } image, _, err := client.ImageInspectWithRaw(ctx, info.String()) if err == nil { return &image, nil } return nil, err } func load(ctx context.Context, client docker.Client, info helperimage.Info) error { imagePaths := []string{ info.Prebuilt + prebuiltDockerArchiveImageExtension, info.Prebuilt + prebuiltExportImageExtension, } // future proof using amd64 in the future over x86_64 if strings.Contains(info.Prebuilt, "x86_64") { name := strings.ReplaceAll(info.Prebuilt, "x86_64", "amd64") imagePaths = append( imagePaths, name+prebuiltDockerArchiveImageExtension, name+prebuiltExportImageExtension, ) } var errs []error for _, imageDir := range PrebuiltImagesPaths { for _, imagePath := range imagePaths { importPath := filepath.Join(imageDir, imagePath) if strings.HasSuffix(imagePath, prebuiltDockerArchiveImageExtension) { if err := imageLoad(ctx, client, importPath, info.Name, info.Tag); err != nil { errs = append(errs, fmt.Errorf("loading %v: %w", imagePath, err)) continue } return nil } if err := imageImport(ctx, client, importPath, info.Name, info.Tag); err != nil { errs = append(errs, fmt.Errorf("importing %v: %w", imagePath, err)) continue } return nil } } return errors.Join(errs...) } func imageLoad(ctx context.Context, client docker.Client, path, ref, tag string) error { file, err := os.Open(path) if err != nil { return err } defer func() { _ = file.Close() }() resp, err := client.ImageLoad(ctx, file, true) if err != nil { return fmt.Errorf("failed to load image: %w", err) } defer resp.Body.Close() defer func() { _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1024)) }() // image load makes it unnecessarily difficult to get the image ref var event struct { Stream string `json:"stream"` } decoder := json.NewDecoder(resp.Body) var imageID string for decoder.More() { if err := decoder.Decode(&event); err != nil { return fmt.Errorf("decoding image id: %w", err) } switch { case strings.Contains(event.Stream, "Loaded image:"): imageID = strings.TrimSpace(strings.TrimPrefix(event.Stream, "Loaded image:")) case strings.Contains(event.Stream, "Loaded image ID:"): imageID = strings.TrimSpace(strings.TrimPrefix(event.Stream, "Loaded image ID:")) } if imageID != "" { break } } if imageID == "" { return fmt.Errorf("could not find image ID for loaded prebuilt image") } if err := client.ImageTag(ctx, imageID, ref+":"+tag); err != nil { return fmt.Errorf("tagging %v to %v:%v", imageID, ref, tag) } return nil } func imageImport(ctx context.Context, client docker.Client, path, ref, tag string) error { file, err := os.Open(path) if err != nil { return err } defer func() { _ = file.Close() }() source := image.ImportSource{ Source: file, SourceName: "-", } options := image.ImportOptions{ Tag: tag, // NOTE: The ENTRYPOINT metadata is not preserved on export, so we need to reapply this metadata on import. // See https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/2058#note_388341301 Changes: []string{`ENTRYPOINT ["/usr/bin/dumb-init", "/entrypoint"]`}, } if err = client.ImageImportBlocking(ctx, source, ref, options); err != nil { return fmt.Errorf("failed to import image: %w", err) } return nil }