pkg/build/image/image.go

Summary

Maintainability
D
2 days
Test Coverage
F
57%
package image

import (
    "context"
    "fmt"
    "strings"

    "github.com/gookit/color"

    "github.com/werf/logboek"
    "github.com/werf/logboek/pkg/style"
    "github.com/werf/logboek/pkg/types"
    "github.com/werf/werf/v2/pkg/build/stage"
    "github.com/werf/werf/v2/pkg/config"
    "github.com/werf/werf/v2/pkg/container_backend"
    "github.com/werf/werf/v2/pkg/docker_registry"
    "github.com/werf/werf/v2/pkg/dockerfile"
    "github.com/werf/werf/v2/pkg/giterminism_manager"
    "github.com/werf/werf/v2/pkg/image"
    "github.com/werf/werf/v2/pkg/logging"
    "github.com/werf/werf/v2/pkg/storage/manager"
    "github.com/werf/werf/v2/pkg/werf"
)

type BaseImageType string

const (
    ImageFromRegistryAsBaseImage BaseImageType = "ImageFromRegistryAsBaseImage"
    StageAsBaseImage             BaseImageType = "StageAsBaseImage"
    NoBaseImage                  BaseImageType = "NoBaseImage"
)

type CommonImageOptions struct {
    Conveyor           Conveyor
    GiterminismManager giterminism_manager.Interface
    ContainerBackend   container_backend.ContainerBackend
    StorageManager     manager.StorageManagerInterface
    ProjectDir         string
    ProjectName        string
    ContainerWerfDir   string
    TmpDir             string

    ForceTargetPlatformLogging bool
}

type ImageOptions struct {
    CommonImageOptions
    IsArtifact, IsDockerfileImage, IsDockerfileTargetStage bool
    DockerfileImageConfig                                  *config.ImageFromDockerfile

    BaseImageReference        string
    BaseImageName             string
    FetchLatestBaseImage      bool
    DockerfileExpanderFactory dockerfile.ExpanderFactory
}

func NewImage(ctx context.Context, targetPlatform, name string, baseImageType BaseImageType, opts ImageOptions) (*Image, error) {
    switch baseImageType {
    case NoBaseImage, ImageFromRegistryAsBaseImage, StageAsBaseImage:
    default:
        panic(fmt.Sprintf("unknown opts.BaseImageType %q", baseImageType))
    }

    if targetPlatform == "" {
        panic("assertion: targetPlatform cannot be empty")
    }

    i := &Image{
        Name:                    name,
        CommonImageOptions:      opts.CommonImageOptions,
        IsArtifact:              opts.IsArtifact,
        IsDockerfileImage:       opts.IsDockerfileImage,
        IsDockerfileTargetStage: opts.IsDockerfileTargetStage,
        DockerfileImageConfig:   opts.DockerfileImageConfig,
        TargetPlatform:          targetPlatform,

        baseImageType:             baseImageType,
        baseImageReference:        opts.BaseImageReference,
        baseImageName:             opts.BaseImageName,
        dockerfileExpanderFactory: opts.DockerfileExpanderFactory,
    }

    if opts.FetchLatestBaseImage {
        if err := i.setupBaseImageRepoDigest(ctx, i.baseImageReference); err != nil {
            return nil, fmt.Errorf("error fetching base image id from registry: %w", err)
        }
    }

    return i, nil
}

type Image struct {
    CommonImageOptions

    IsArtifact              bool
    IsDockerfileImage       bool
    IsDockerfileTargetStage bool
    Name                    string
    DockerfileImageConfig   *config.ImageFromDockerfile
    TargetPlatform          string

    stages            []stage.Interface
    lastNonEmptyStage stage.Interface
    contentDigest     string
    rebuilt           bool

    baseImageType             BaseImageType
    baseImageReference        string
    baseImageName             string
    dockerfileExpanderFactory dockerfile.ExpanderFactory

    // NOTICE: baseImageRepoId is a legacy field, better use Digest instead everywhere
    baseImageRepoId     string
    baseImageRepoDigest string

    baseStageImage   *stage.StageImage
    stageAsBaseImage stage.Interface
}

func (i *Image) LogName() string {
    return logging.ImageLogName(i.Name, i.IsArtifact)
}

func (i *Image) ShouldLogPlatform() bool {
    return i.ForceTargetPlatformLogging || i.TargetPlatform != i.ContainerBackend.GetRuntimePlatform()
}

func (i *Image) LogDetailedName() string {
    var targetPlatformForLog string
    if i.ShouldLogPlatform() {
        targetPlatformForLog = i.TargetPlatform
    }
    return logging.ImageLogProcessName(i.Name, i.IsArtifact, targetPlatformForLog)
}

func (i *Image) LogProcessStyle() color.Style {
    return ImageLogProcessStyle(i.IsArtifact)
}

func (i *Image) LogTagStyle() color.Style {
    return ImageLogTagStyle(i.IsArtifact)
}

func ImageLogProcessStyle(isArtifact bool) color.Style {
    return logging.ImageDefaultStyle(isArtifact)
}

func ImageLogTagStyle(isArtifact bool) color.Style {
    return logging.ImageDefaultStyle(isArtifact)
}

func (i *Image) IsFinal() bool {
    if i.IsArtifact {
        return false
    }
    if i.IsDockerfileImage && !i.IsDockerfileTargetStage {
        return false
    }
    return true
}

func (i *Image) SetStages(stages []stage.Interface) {
    i.stages = stages
}

func (i *Image) GetStages() []stage.Interface {
    return i.stages
}

func (i *Image) SetLastNonEmptyStage(stg stage.Interface) {
    i.lastNonEmptyStage = stg
}

func (i *Image) GetLastNonEmptyStage() stage.Interface {
    return i.lastNonEmptyStage
}

func (i *Image) SetContentDigest(digest string) {
    i.contentDigest = digest
}

func (i *Image) GetContentDigest() string {
    return i.contentDigest
}

func (i *Image) GetStage(name stage.StageName) stage.Interface {
    for _, s := range i.stages {
        if s.Name() == name {
            return s
        }
    }

    return nil
}

func (i *Image) GetStageID() string {
    return i.GetLastNonEmptyStage().GetStageImage().Image.GetStageDescription().Info.Tag
}

func (i *Image) UsesBuildContext() bool {
    for _, stg := range i.GetStages() {
        if stg.UsesBuildContext() {
            return true
        }
    }

    return false
}

func (i *Image) GetName() string {
    return i.Name
}

func (i *Image) GetLogName() string {
    return i.LogName()
}

func (i *Image) SetRebuilt(rebuilt bool) {
    i.rebuilt = rebuilt
}

func (i *Image) GetRebuilt() bool {
    return i.rebuilt
}

func (i *Image) ExpandDependencies(ctx context.Context, baseEnv map[string]string) error {
    for _, stg := range i.stages {
        if err := stg.ExpandDependencies(ctx, i.Conveyor, baseEnv); err != nil {
            return fmt.Errorf("unable to expand dependencies for stage %q: %w", stg.Name(), err)
        }
    }
    return nil
}

func isUnsupportedMediaTypeError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "unsupported MediaType")
}

func (i *Image) SetupBaseImage(ctx context.Context, storageManager manager.StorageManagerInterface, storageOpts manager.StorageOptions) error {
    logboek.Context(ctx).Debug().LogF(" -- SetupBaseImage for %q\n", i.Name)

    switch i.baseImageType {
    case StageAsBaseImage:
        i.stageAsBaseImage = i.Conveyor.GetImage(i.TargetPlatform, i.baseImageName).GetLastNonEmptyStage()
        i.baseImageReference = i.stageAsBaseImage.GetStageImage().Image.Name()
        i.baseStageImage = i.stageAsBaseImage.GetStageImage()

    case ImageFromRegistryAsBaseImage:
        if i.IsDockerfileImage && i.dockerfileExpanderFactory != nil {
            dependenciesArgs := stage.ResolveDependenciesArgs(i.TargetPlatform, i.DockerfileImageConfig.Dependencies, i.Conveyor)
            ref, err := i.dockerfileExpanderFactory.GetExpander(dockerfile.ExpandOptions{SkipUnsetEnv: false}).ProcessWordWithMap(i.baseImageReference, dependenciesArgs)
            if err != nil {
                return fmt.Errorf("unable to expand dockerfile base image reference %q: %w", i.baseImageReference, err)
            }
            i.baseImageReference = ref
        }

        i.baseStageImage = i.Conveyor.GetOrCreateStageImage(i.baseImageReference, nil, nil, i)

        if i.IsDockerfileImage && i.DockerfileImageConfig.Staged {
            if werf.GetStagedDockerfileVersion() == werf.StagedDockerfileV1 {
                var info *image.Info

                if i.baseImageReference != "scratch" {
                    var err error
                    info, err = storageManager.GetImageInfo(ctx, i.baseImageReference, storageOpts)
                    if isUnsupportedMediaTypeError(err) {
                        if err := logboek.Context(ctx).Default().LogProcess("Pulling base image %s", i.baseStageImage.Image.Name()).
                            Options(func(options types.LogProcessOptionsInterface) {
                                options.Style(style.Highlight())
                            }).
                            DoError(func() error {
                                return i.ContainerBackend.PullImageFromRegistry(ctx, i.baseStageImage.Image)
                            }); err != nil {
                            return err
                        }

                        info, err = storageManager.GetImageInfo(ctx, i.baseImageReference, storageOpts)
                        if err != nil {
                            return fmt.Errorf("unable to get base image %q manifest: %w", i.baseImageReference, err)
                        }
                    } else if err != nil {
                        return fmt.Errorf("unable to get base image %q manifest: %w", i.baseImageReference, err)
                    }
                } else {
                    info = &image.Info{
                        Name: i.baseImageReference,
                        Env:  nil,
                    }
                }

                i.baseStageImage.Image.SetStageDescription(&image.StageDescription{
                    StageID: nil, // this is not a stage actually, TODO
                    Info:    info,
                })
            }
        }
    case NoBaseImage:

    default:
        panic(fmt.Sprintf("unknown base image type %q", i.baseImageType))
    }

    if i.IsDockerfileImage && i.DockerfileImageConfig.Staged {
        if werf.GetStagedDockerfileVersion() == werf.StagedDockerfileV1 {
            switch i.baseImageType {
            case StageAsBaseImage, ImageFromRegistryAsBaseImage:
                if err := i.ExpandDependencies(ctx, EnvToMap(i.baseStageImage.Image.GetStageDescription().Info.Env)); err != nil {
                    return err
                }
            }
        }
    }

    return nil
}

// TODO(staged-dockerfile): this is only for compatibility with stapel-builder logic, and this should be unified with new staged-dockerfile logic
func (i *Image) GetBaseStageImage() *stage.StageImage {
    return i.baseStageImage
}

func (i *Image) GetBaseImageReference() string {
    return i.baseImageReference
}

func (i *Image) GetBaseImageRepoDigest() string {
    return i.baseImageRepoDigest
}

func (i *Image) FetchBaseImage(ctx context.Context) error {
    logboek.Context(ctx).Debug().LogF(" -- FetchBaseImage for %q\n", i.Name)

    switch i.baseImageType {
    case ImageFromRegistryAsBaseImage:
        if i.baseStageImage.Image.Name() == "scratch" {
            return nil
        }

        // TODO: Refactor, move manifest fetching into SetupBaseImage, only pull image in FetchBaseImage method

        if info, err := i.ContainerBackend.GetImageInfo(ctx, i.baseStageImage.Image.Name(), container_backend.GetImageInfoOpts{}); err != nil {
            return fmt.Errorf("unable to inspect local image %s: %w", i.baseStageImage.Image.Name(), err)
        } else if info != nil {
            logboek.Context(ctx).Debug().LogF("GetImageInfo of %q -> %#v\n", i.baseStageImage.Image.Name(), info)

            // TODO: do not use container_backend.LegacyStageImage for base image
            i.baseStageImage.Image.SetStageDescription(&image.StageDescription{
                StageID: nil, // this is not a stage actually, TODO
                Info:    info,
            })

            err = i.setupBaseImageRepoDigest(ctx, i.baseStageImage.Image.Name())
            if (i.baseImageRepoDigest != "" && i.baseImageRepoDigest == info.RepoDigest) || (err != nil && !isUnsupportedMediaTypeError(err)) {
                if err != nil {
                    logboek.Context(ctx).Warn().LogF("WARNING: cannot get base image id (%s): %s\n", i.baseStageImage.Image.Name(), err)
                    logboek.Context(ctx).Warn().LogF("WARNING: using existing image %s without pull\n", i.baseStageImage.Image.Name())
                    logboek.Context(ctx).Warn().LogOptionalLn()
                } else {
                    logboek.Context(ctx).Info().LogF("No pull needed for base image %s of image %q: image by digest %s is up to date\n", i.baseImageReference, i.Name, i.baseImageRepoDigest)
                }
                // No image pull
                return nil
            }
        }

        if err := logboek.Context(ctx).Default().LogProcess("Pulling base image %s", i.baseStageImage.Image.Name()).
            Options(func(options types.LogProcessOptionsInterface) {
                options.Style(style.Highlight())
            }).
            DoError(func() error {
                return i.ContainerBackend.PullImageFromRegistry(ctx, i.baseStageImage.Image)
            }); err != nil {
            return err
        }

        info, err := i.ContainerBackend.GetImageInfo(ctx, i.baseStageImage.Image.Name(), container_backend.GetImageInfoOpts{})
        if err != nil {
            return fmt.Errorf("unable to inspect local image %s: %w", i.baseStageImage.Image.Name(), err)
        }

        if info == nil {
            return fmt.Errorf("unable to inspect local image %s after successful pull: image is not exists", i.baseStageImage.Image.Name())
        }

        i.baseStageImage.Image.SetStageDescription(&image.StageDescription{
            StageID: nil, // this is not a stage actually, TODO
            Info:    info,
        })

        return nil
    case StageAsBaseImage:
        return i.StorageManager.FetchStage(ctx, i.ContainerBackend, i.stageAsBaseImage)

    case NoBaseImage:
        return nil

    default:
        panic(fmt.Sprintf("unknown base image type %q", i.baseImageType))
    }
}

func packRepoIDAndDigest(repoID, digest string) string {
    return fmt.Sprintf("%s/%s", repoID, digest)
}

func unpackRepoIDAndDigest(packed string) (string, string) {
    parts := strings.SplitN(packed, "/", 2)
    return parts[0], parts[1]
}

func (i *Image) setupBaseImageRepoDigest(ctx context.Context, reference string) error {
    i.Conveyor.GetServiceRWMutex("baseImagesRepoIdsCache" + reference).Lock()
    defer i.Conveyor.GetServiceRWMutex("baseImagesRepoIdsCache" + reference).Unlock()

    switch {
    case i.baseImageRepoId != "":
        return nil
    case i.Conveyor.IsBaseImagesRepoIdsCacheExist(reference):
        i.baseImageRepoId, i.baseImageRepoDigest = unpackRepoIDAndDigest(i.Conveyor.GetBaseImagesRepoIdsCache(reference))
        return nil
    case i.Conveyor.IsBaseImagesRepoErrCacheExist(reference):
        return i.Conveyor.GetBaseImagesRepoErrCache(reference)
    }

    var fetchedBaseRepoImage *image.Info
    processMsg := fmt.Sprintf("Trying to get from base image id from registry (%s)", reference)
    if err := logboek.Context(ctx).Info().LogProcessInline(processMsg).DoError(func() error {
        var fetchImageIdErr error
        fetchedBaseRepoImage, fetchImageIdErr = docker_registry.API().GetRepoImage(ctx, reference)
        if fetchImageIdErr != nil {
            i.Conveyor.SetBaseImagesRepoErrCache(reference, fetchImageIdErr)
            return fmt.Errorf("can not get base image id from registry (%s): %w", reference, fetchImageIdErr)
        }

        return nil
    }); err != nil {
        return err
    }

    i.baseImageRepoId = fetchedBaseRepoImage.ID
    i.baseImageRepoDigest = fetchedBaseRepoImage.RepoDigest
    i.Conveyor.SetBaseImagesRepoIdsCache(reference, packRepoIDAndDigest(i.baseImageRepoId, i.baseImageRepoDigest))

    return nil
}

func EnvToMap(env []string) map[string]string {
    res := make(map[string]string)
    for _, kv := range env {
        k, v := parseKeyValue(kv)
        res[k] = v
    }
    return res
}

func parseKeyValue(env string) (string, string) {
    parts := strings.SplitN(env, "=", 2)
    v := ""
    if len(parts) > 1 {
        v = parts[1]
    }

    return parts[0], v
}