Skip to content

Commit

Permalink
feat(staged-dockerfile): make and extract context archive only once
Browse files Browse the repository at this point in the history
Signed-off-by: Ilya Lesikov <ilya@lesikov.com>
  • Loading branch information
ilya-lesikov committed Oct 17, 2022
1 parent 87eb312 commit 9d72ad1
Show file tree
Hide file tree
Showing 48 changed files with 379 additions and 280 deletions.
4 changes: 2 additions & 2 deletions pkg/build/base_phase.go
Expand Up @@ -19,8 +19,8 @@ func (phase *BasePhase) AfterImages(_ context.Context) error {
return nil
}

func (phase *BasePhase) BeforeImageStages(_ context.Context, _ *image.Image) error {
return nil
func (phase *BasePhase) BeforeImageStages(ctx context.Context, img *image.Image) (deferFn func(), err error) {
return nil, nil
}

func (phase *BasePhase) OnImageStage(_ context.Context, _ *image.Image, _ stage.Interface) error {
Expand Down
25 changes: 21 additions & 4 deletions pkg/build/build_phase.go
Expand Up @@ -84,6 +84,8 @@ type BuildPhase struct {
ShouldAddManagedImageRecord bool

ImagesReport *ImagesReport

buildContextArchive container_backend.BuildContextArchiver
}

const (
Expand Down Expand Up @@ -220,12 +222,27 @@ func (phase *BuildPhase) ImageProcessingShouldBeStopped(_ context.Context, _ *im
return false
}

func (phase *BuildPhase) BeforeImageStages(_ context.Context, img *image.Image) error {
func (phase *BuildPhase) BeforeImageStages(ctx context.Context, img *image.Image) (deferFn func(), err error) {
phase.StagesIterator = NewStagesIterator(phase.Conveyor)

img.SetupBaseImage()

return nil
if img.UsesBuildContext() {
phase.buildContextArchive = image.NewBuildContextArchive(phase.Conveyor.giterminismManager, img.TmpDir)
if err := phase.buildContextArchive.Create(ctx, container_backend.BuildContextArchiveCreateOptions{
DockerfileRelToContextPath: img.DockerfileImageConfig.Dockerfile,
ContextGitSubDir: img.DockerfileImageConfig.Context,
ContextAddFiles: img.DockerfileImageConfig.ContextAddFiles,
}); err != nil {
return nil, fmt.Errorf("unable to create build context archive: %w", err)
}

deferFn = func() {
phase.buildContextArchive.CleanupExtractedDir(ctx)
}
}

return deferFn, nil
}

func (phase *BuildPhase) AfterImageStages(ctx context.Context, img *image.Image) error {
Expand Down Expand Up @@ -561,7 +578,7 @@ func (phase *BuildPhase) fetchBaseImageForStage(ctx context.Context, img *image.

func (phase *BuildPhase) calculateStage(ctx context.Context, img *image.Image, stg stage.Interface) (bool, func(), error) {
// FIXME(stapel-to-buildah): store StageImage-s everywhere in stage and build pkgs
stageDependencies, err := stg.GetDependencies(ctx, phase.Conveyor, phase.Conveyor.ContainerBackend, phase.StagesIterator.GetPrevImage(img, stg), phase.StagesIterator.GetPrevBuiltImage(img, stg))
stageDependencies, err := stg.GetDependencies(ctx, phase.Conveyor, phase.Conveyor.ContainerBackend, phase.StagesIterator.GetPrevImage(img, stg), phase.StagesIterator.GetPrevBuiltImage(img, stg), phase.buildContextArchive)
if err != nil {
return false, nil, err
}
Expand Down Expand Up @@ -681,7 +698,7 @@ func (phase *BuildPhase) prepareStageInstructions(ctx context.Context, img *imag
stageImage.Builder.DockerfileStageBuilder().AppendPostInstruction(backend_instruction.NewLabel(*dockerfile_instruction.NewLabel(serviceLabels)))
}

err := stg.PrepareImage(ctx, phase.Conveyor, phase.Conveyor.ContainerBackend, phase.StagesIterator.GetPrevBuiltImage(img, stg), stageImage)
err := stg.PrepareImage(ctx, phase.Conveyor, phase.Conveyor.ContainerBackend, phase.StagesIterator.GetPrevBuiltImage(img, stg), stageImage, phase.buildContextArchive)
if err != nil {
return fmt.Errorf("error preparing stage %s: %w", stg.Name(), err)
}
Expand Down
6 changes: 5 additions & 1 deletion pkg/build/conveyor.go
Expand Up @@ -569,7 +569,11 @@ func (c *Conveyor) doImage(ctx context.Context, img *image.Image, phases []Phase
for _, phase := range phases {
logProcess := logboek.Context(ctx).Debug().LogProcess("Phase %s -- BeforeImageStages()", phase.Name())
logProcess.Start()
if err := phase.BeforeImageStages(ctx, img); err != nil {
deferFn, err := phase.BeforeImageStages(ctx, img)
if deferFn != nil {
defer deferFn()
}
if err != nil {
logProcess.Fail()
return fmt.Errorf("phase %s before image %s stages handler failed: %w", phase.Name(), img.GetLogName(), err)
}
Expand Down
163 changes: 163 additions & 0 deletions pkg/build/image/build_context_archive.go
@@ -0,0 +1,163 @@
package image

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

"github.com/docker/docker/builder/dockerignore"

"github.com/werf/logboek"
"github.com/werf/werf/pkg/container_backend"
"github.com/werf/werf/pkg/context_manager"
"github.com/werf/werf/pkg/git_repo"
"github.com/werf/werf/pkg/giterminism_manager"
"github.com/werf/werf/pkg/path_matcher"
"github.com/werf/werf/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)

pathMatcher := path_matcher.NewPathMatcher(path_matcher.PathMatcherOptions{BasePath: contextPathRelativeToGitWorkTree})
if dockerIgnorePathMatcher, err := createDockerIgnorePathMatcher(ctx, a.giterminismMgr, opts.ContextGitSubDir, opts.DockerfileRelToContextPath); err != nil {
return fmt.Errorf("unable to create dockerignore path matcher: %w", err)
} else if dockerIgnorePathMatcher != nil {
pathMatcher = path_matcher.NewMultiPathMatcher(pathMatcher, dockerIgnorePathMatcher)
}

archive, err := a.giterminismMgr.LocalGitRepo().GetOrCreateArchive(ctx, git_repo.ArchiveOptions{
PathScope: contextPathRelativeToGitWorkTree,
PathMatcher: pathMatcher,
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.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)
}
}

// Might return nil.
func createDockerIgnorePathMatcher(ctx context.Context, giterminismMgr giterminism_manager.Interface, contextGitSubDir, dockerfileRelToContextPath string) (path_matcher.PathMatcher, error) {
dockerfileRelToGitPath := filepath.Join(contextGitSubDir, dockerfileRelToContextPath)

var dockerIgnorePatterns []string
for _, dockerIgnoreRelToContextPath := range []string{
dockerfileRelToContextPath + ".dockerignore",
".dockerignore",
} {
dockerIgnoreRelToGitPath := filepath.Join(contextGitSubDir, dockerIgnoreRelToContextPath)
if exist, err := giterminismMgr.FileReader().IsDockerignoreExistAnywhere(ctx, dockerIgnoreRelToGitPath); err != nil {
return nil, err
} else if !exist {
continue
}

dockerIgnore, err := giterminismMgr.FileReader().ReadDockerignore(ctx, dockerIgnoreRelToGitPath)
if err != nil {
return nil, err
}

r := bytes.NewReader(dockerIgnore)
dockerIgnorePatterns, err = dockerignore.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("unable to read %q file: %w", dockerIgnoreRelToContextPath, err)
}

break
}

if dockerIgnorePatterns == nil {
return nil, nil
}

dockerIgnorePathMatcher := path_matcher.NewPathMatcher(path_matcher.PathMatcherOptions{
BasePath: filepath.Join(giterminismMgr.RelativeToGitProjectDir(), contextGitSubDir),
DockerignorePatterns: dockerIgnorePatterns,
})

if !dockerIgnorePathMatcher.IsPathMatched(dockerfileRelToGitPath) {
logboek.Context(ctx).Warn().LogLn("WARNING: There is no way to ignore the Dockerfile due to docker limitation when building an image for a compressed context that reads from STDIN.")
logboek.Context(ctx).Warn().LogF("WARNING: To hide this message, remove the Dockerfile ignore rule or add an exception rule.\n")

exceptionRule := "!" + dockerfileRelToContextPath
dockerIgnorePatterns = append(dockerIgnorePatterns, exceptionRule)
dockerIgnorePathMatcher = path_matcher.NewPathMatcher(path_matcher.PathMatcherOptions{
BasePath: filepath.Join(giterminismMgr.RelativeToGitProjectDir(), contextGitSubDir),
DockerignorePatterns: dockerIgnorePatterns,
})
}

return dockerIgnorePathMatcher, nil
}
10 changes: 10 additions & 0 deletions pkg/build/image/image.go
Expand Up @@ -171,6 +171,16 @@ 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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/build/phase.go
Expand Up @@ -11,7 +11,7 @@ type Phase interface {
Name() string
BeforeImages(ctx context.Context) error
AfterImages(ctx context.Context) error
BeforeImageStages(ctx context.Context, img *image.Image) error
BeforeImageStages(ctx context.Context, img *image.Image) (deferFn func(), err error)
OnImageStage(ctx context.Context, img *image.Image, stg stage.Interface) error
AfterImageStages(ctx context.Context, img *image.Image) error
ImageProcessingShouldBeStopped(ctx context.Context, img *image.Image) bool
Expand Down
18 changes: 11 additions & 7 deletions pkg/build/stage/base.go
Expand Up @@ -134,7 +134,7 @@ func (s *BaseStage) FetchDependencies(_ context.Context, _ Conveyor, _ container
return nil
}

func (s *BaseStage) GetDependencies(_ context.Context, _ Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage) (string, error) {
func (s *BaseStage) GetDependencies(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
panic("method must be implemented!")
}

Expand Down Expand Up @@ -246,28 +246,28 @@ func (s *BaseStage) SelectSuitableStage(_ context.Context, c Conveyor, stages []
return s.selectStageByOldestCreationTimestamp(stages)
}

func (s *BaseStage) PrepareImage(ctx context.Context, c Conveyor, cr container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage) error {
func (s *BaseStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) error {
/*
* NOTE: BaseStage.PrepareImage does not called in From.PrepareImage.
* NOTE: Take into account when adding new base PrepareImage steps.
*/

addLabels := map[string]string{imagePkg.WerfProjectRepoCommitLabel: c.GiterminismManager().HeadCommit()}
if c.UseLegacyStapelBuilder(cr) {
if c.UseLegacyStapelBuilder(cb) {
stageImage.Builder.LegacyStapelStageBuilder().Container().ServiceCommitChangeOptions().AddLabel(addLabels)
} else {
stageImage.Builder.StapelStageBuilder().AddLabels(addLabels)
}

serviceMounts := s.getServiceMounts(prevBuiltImage)
s.addServiceMountsLabels(serviceMounts, c, cr, stageImage)
if err := s.addServiceMountsVolumes(serviceMounts, c, cr, stageImage, false); err != nil {
s.addServiceMountsLabels(serviceMounts, c, cb, stageImage)
if err := s.addServiceMountsVolumes(serviceMounts, c, cb, stageImage, false); err != nil {
return fmt.Errorf("error adding mounts volumes: %w", err)
}

customMounts := s.getCustomMounts(prevBuiltImage)
s.addCustomMountLabels(customMounts, c, cr, stageImage)
if err := s.addCustomMountVolumes(customMounts, c, cr, stageImage, false); err != nil {
s.addCustomMountLabels(customMounts, c, cb, stageImage)
if err := s.addCustomMountVolumes(customMounts, c, cb, stageImage, false); err != nil {
return fmt.Errorf("error adding mounts volumes: %w", err)
}

Expand Down Expand Up @@ -502,6 +502,10 @@ func (s *BaseStage) GetGitMappings() []*GitMapping {
return s.gitMappings
}

func (s *BaseStage) UsesBuildContext() bool {
return false
}

func mergeMounts(a, b map[string][]string) map[string][]string {
res := map[string][]string{}

Expand Down
8 changes: 4 additions & 4 deletions pkg/build/stage/before_install.go
Expand Up @@ -27,16 +27,16 @@ type BeforeInstallStage struct {
*UserStage
}

func (s *BeforeInstallStage) GetDependencies(ctx context.Context, _ Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage) (string, error) {
func (s *BeforeInstallStage) GetDependencies(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
return s.builder.BeforeInstallChecksum(ctx), nil
}

func (s *BeforeInstallStage) PrepareImage(ctx context.Context, c Conveyor, cr container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage) error {
if err := s.BaseStage.PrepareImage(ctx, c, cr, prevBuiltImage, stageImage); err != nil {
func (s *BeforeInstallStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) error {
if err := s.BaseStage.PrepareImage(ctx, c, cb, prevBuiltImage, stageImage, nil); err != nil {
return err
}

if err := s.builder.BeforeInstall(ctx, cr, stageImage.Builder, c.UseLegacyStapelBuilder(cr)); err != nil {
if err := s.builder.BeforeInstall(ctx, cb, stageImage.Builder, c.UseLegacyStapelBuilder(cb)); err != nil {
return err
}

Expand Down
8 changes: 4 additions & 4 deletions pkg/build/stage/before_setup.go
Expand Up @@ -28,7 +28,7 @@ type BeforeSetupStage struct {
*UserWithGitPatchStage
}

func (s *BeforeSetupStage) GetDependencies(ctx context.Context, c Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage) (string, error) {
func (s *BeforeSetupStage) GetDependencies(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
stageDependenciesChecksum, err := s.getStageDependenciesChecksum(ctx, c, BeforeSetup)
if err != nil {
return "", err
Expand All @@ -37,12 +37,12 @@ func (s *BeforeSetupStage) GetDependencies(ctx context.Context, c Conveyor, _ co
return util.Sha256Hash(s.builder.BeforeSetupChecksum(ctx), stageDependenciesChecksum), nil
}

func (s *BeforeSetupStage) PrepareImage(ctx context.Context, c Conveyor, cr container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage) error {
if err := s.UserWithGitPatchStage.PrepareImage(ctx, c, cr, prevBuiltImage, stageImage); err != nil {
func (s *BeforeSetupStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) error {
if err := s.UserWithGitPatchStage.PrepareImage(ctx, c, cb, prevBuiltImage, stageImage, nil); err != nil {
return err
}

if err := s.builder.BeforeSetup(ctx, cr, stageImage.Builder, c.UseLegacyStapelBuilder(cr)); err != nil {
if err := s.builder.BeforeSetup(ctx, cb, stageImage.Builder, c.UseLegacyStapelBuilder(cb)); err != nil {
return err
}

Expand Down
10 changes: 5 additions & 5 deletions pkg/build/stage/dependencies.go
Expand Up @@ -67,7 +67,7 @@ type DependenciesStage struct {
dependencies []*config.Dependency
}

func (s *DependenciesStage) GetDependencies(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, _, _ *StageImage) (string, error) {
func (s *DependenciesStage) GetDependencies(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevImage, prevBuiltImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) (string, error) {
var args []string

for ind, elm := range s.imports {
Expand Down Expand Up @@ -209,11 +209,11 @@ func (s *DependenciesStage) prepareImage(ctx context.Context, c Conveyor, cr con
return nil
}

func (s *DependenciesStage) PrepareImage(ctx context.Context, c Conveyor, cr container_backend.ContainerBackend, prevImage, stageImage *StageImage) error {
if c.UseLegacyStapelBuilder(cr) {
return s.prepareImageWithLegacyStapelBuilder(ctx, c, cr, prevImage, stageImage)
func (s *DependenciesStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, buildContextArchive container_backend.BuildContextArchiver) error {
if c.UseLegacyStapelBuilder(cb) {
return s.prepareImageWithLegacyStapelBuilder(ctx, c, cb, prevBuiltImage, stageImage)
} else {
return s.prepareImage(ctx, c, cr, prevImage, stageImage)
return s.prepareImage(ctx, c, cb, prevBuiltImage, stageImage)
}
}

Expand Down

0 comments on commit 9d72ad1

Please sign in to comment.