pkg/build/image/build_context_archive.go

Summary

Maintainability
A
1 hr
Test Coverage
F
31%
package image

import (
    "context"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "sort"

    "github.com/containers/buildah/copier"

    "github.com/werf/logboek"
    "github.com/werf/werf/v2/pkg/container_backend"
    "github.com/werf/werf/v2/pkg/context_manager"
    "github.com/werf/werf/v2/pkg/git_repo"
    "github.com/werf/werf/v2/pkg/giterminism_manager"
    "github.com/werf/werf/v2/pkg/path_matcher"
    "github.com/werf/werf/v2/pkg/util"
)

func NewBuildContextArchive(giterminismMgr giterminism_manager.Interface, extractionRootTmpDir string) *BuildContextArchive {
    return &BuildContextArchive{
        giterminismMgr:       giterminismMgr,
        extractionRootTmpDir: extractionRootTmpDir,
    }
}

type BuildContextArchive struct {
    giterminismMgr       giterminism_manager.Interface
    path                 string
    extractionRootTmpDir string
    extractionDir        string
}

func (a *BuildContextArchive) Create(ctx context.Context, opts container_backend.BuildContextArchiveCreateOptions) error {
    contextPathRelativeToGitWorkTree := filepath.Join(a.giterminismMgr.RelativeToGitProjectDir(), opts.ContextGitSubDir)

    dockerIgnorePathMatcher, err := createDockerIgnorePathMatcher(ctx, a.giterminismMgr, opts.ContextGitSubDir, opts.DockerfileRelToContextPath)
    if err != nil {
        return fmt.Errorf("unable to create dockerignore path matcher: %w", err)
    }

    archive, err := a.giterminismMgr.LocalGitRepo().GetOrCreateArchive(ctx, git_repo.ArchiveOptions{
        PathScope: contextPathRelativeToGitWorkTree,
        PathMatcher: path_matcher.NewMultiPathMatcher(path_matcher.NewPathMatcher(
            path_matcher.PathMatcherOptions{BasePath: contextPathRelativeToGitWorkTree}),
            dockerIgnorePathMatcher,
        ),
        Commit: a.giterminismMgr.HeadCommit(),
    })
    if err != nil {
        return fmt.Errorf("unable to get or create archive: %w", err)
    }

    a.path = archive.GetFilePath()

    if len(opts.ContextAddFiles) > 0 {
        if err := logboek.Context(ctx).Debug().LogProcess("Add contextAddFiles to build context archive %s", a.path).DoError(func() error {
            a.path, err = context_manager.AddContextAddFilesToContextArchive(ctx, a.path, a.giterminismMgr.ProjectDir(), opts.ContextGitSubDir, opts.ContextAddFiles)
            return err
        }); err != nil {
            return fmt.Errorf("unable to add contextAddFiles to build context archive %s: %w", a.path, err)
        }
    }

    return nil
}

func (a *BuildContextArchive) Path() string {
    return a.path
}

func (a *BuildContextArchive) ExtractOrGetExtractedDir(ctx context.Context) (string, error) {
    if a.path == "" {
        panic("extract should not be called before create")
    }

    if a.extractionDir != "" {
        return a.extractionDir, nil
    }

    if err := os.MkdirAll(a.extractionRootTmpDir, os.ModePerm); err != nil {
        return "", fmt.Errorf("unable to create extraction root tmp dir %q: %w", a.extractionRootTmpDir, err)
    }

    var err error
    a.extractionDir, err = ioutil.TempDir(a.extractionRootTmpDir, "context")
    if err != nil {
        return "", fmt.Errorf("unable to create context tmp dir: %w", err)
    }

    archiveReader, err := os.Open(a.path)
    if err != nil {
        return "", fmt.Errorf("unable to open context archive %q: %w", a.path, err)
    }
    defer archiveReader.Close()

    if err := util.ExtractTar(archiveReader, a.extractionDir, util.ExtractTarOptions{}); err != nil {
        return "", fmt.Errorf("unable to extract context tar to tmp context dir %q: %w", a.extractionDir, err)
    }

    return a.extractionDir, nil
}

func (a *BuildContextArchive) CleanupExtractedDir(ctx context.Context) {
    if a.extractionDir == "" {
        return
    }

    if err := os.RemoveAll(a.extractionDir); err != nil {
        logboek.Context(ctx).Warn().LogF("WARNING: unable to remove extracted context dir %q: %s", a.extractionDir, err)
    }
}

func (a *BuildContextArchive) CalculateGlobsChecksum(ctx context.Context, globs []string, checkForArchives bool) (string, error) {
    contextDir, err := a.ExtractOrGetExtractedDir(ctx)
    if err != nil {
        return "", fmt.Errorf("unable to get build context dir: %w", err)
    }

    globStats, err := copier.Stat(contextDir, contextDir, copier.StatOptions{CheckForArchives: checkForArchives}, globs)
    if err != nil {
        return "", fmt.Errorf("unable to stat globs: %w", err)
    }
    if len(globStats) == 0 {
        return "", fmt.Errorf("no glob matches for globs: %v", globs)
    }

    var matches []string
    for _, globStat := range globStats {
        if globStat.Error != "" {
            return "", fmt.Errorf("unable to stat glob %q: %s", globStat.Glob, globStat.Error)
        }

        for _, match := range globStat.Globbed {
            matches = append(matches, match)
        }
    }

    pathsChecksum, err := a.CalculatePathsChecksum(ctx, matches)
    if err != nil {
        return "", fmt.Errorf("unable to calculate build context paths checksum: %w", err)
    }

    return pathsChecksum, nil
}

func (a *BuildContextArchive) CalculatePathsChecksum(ctx context.Context, paths []string) (string, error) {
    sort.Strings(paths)
    paths = util.UniqStrings(paths)

    dir, err := a.ExtractOrGetExtractedDir(ctx)
    if err != nil {
        return "", fmt.Errorf("unable to access context directory: %w", err)
    }

    var pathsHashes []string
    for _, path := range paths {
        p := filepath.Join(dir, path)

        hash, err := util.HashContentsAndPathsRecurse(p)
        if err != nil {
            return "", fmt.Errorf("unable to calculate hash: %w", err)
        }

        pathsHashes = append(pathsHashes, hash)
    }

    return util.Sha256Hash(pathsHashes...), nil
}