From 76c98c66e08faa5153c9c62af476885936836e83 Mon Sep 17 00:00:00 2001 From: Ilya Lesikov Date: Tue, 4 Oct 2022 15:55:09 +0300 Subject: [PATCH] feat(buildah): add low level dockerfile stage builder Signed-off-by: Ilya Lesikov --- cmd/buildah-test/main.go | 26 +-- pkg/build/build_phase.go | 6 +- pkg/build/stage/data_test.go | 8 +- pkg/build/stage/dockerfile.go | 10 +- pkg/buildah/common.go | 23 +- pkg/buildah/native_linux.go | 62 ++++-- pkg/container_backend/buildah_backend.go | 198 +++++++++++++++++- .../docker_server_backend.go | 5 + pkg/container_backend/dockerfile_commands.go | 84 ++++++++ pkg/container_backend/interface.go | 5 + .../perf_check_container_backend.go | 9 + .../stage_builder/dockerfile_builder.go | 115 ++++++++++ .../stage_builder/dockerfile_stage_builder.go | 125 ++++------- .../stage_builder/stage_builder.go | 15 +- 14 files changed, 548 insertions(+), 143 deletions(-) create mode 100644 pkg/container_backend/dockerfile_commands.go create mode 100644 pkg/container_backend/stage_builder/dockerfile_builder.go diff --git a/cmd/buildah-test/main.go b/cmd/buildah-test/main.go index 621a56c239..4d463b248d 100644 --- a/cmd/buildah-test/main.go +++ b/cmd/buildah-test/main.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "os" "path/filepath" "strings" @@ -12,7 +11,6 @@ import ( "github.com/opencontainers/runtime-spec/specs-go" "github.com/werf/werf/pkg/buildah" - "github.com/werf/werf/pkg/util" "github.com/werf/werf/pkg/werf" ) @@ -81,32 +79,12 @@ func runDockerfile(ctx context.Context, mode buildah.Mode, dockerfilePath, conte return fmt.Errorf("unable to create buildah client: %w", err) } - dockerfileData, err := os.ReadFile(dockerfilePath) - if err != nil { - return fmt.Errorf("error reading %q: %w", dockerfilePath, err) - } - errCh := make(chan error, 0) buildDoneCh := make(chan string, 0) - var contextTar io.Reader - if contextDir != "" { - contextTar = util.BufferedPipedWriterProcess(func(w io.WriteCloser) { - if err := util.WriteDirAsTar((contextDir), w); err != nil { - errCh <- fmt.Errorf("unable to write dir %q as tar: %w", contextDir, err) - return - } - - if err := w.Close(); err != nil { - errCh <- fmt.Errorf("unable to close buffered piped writer for context dir %q: %w", contextDir, err) - return - } - }) - } - go func() { - imageID, err := b.BuildFromDockerfile(ctx, dockerfileData, buildah.BuildFromDockerfileOpts{ - ContextTar: contextTar, + imageID, err := b.BuildFromDockerfile(ctx, dockerfilePath, buildah.BuildFromDockerfileOpts{ + ContextDir: contextDir, CommonOpts: buildah.CommonOpts{ LogWriter: os.Stdout, }, diff --git a/pkg/build/build_phase.go b/pkg/build/build_phase.go index 3e2ae70775..713b80aa6b 100644 --- a/pkg/build/build_phase.go +++ b/pkg/build/build_phase.go @@ -636,10 +636,10 @@ func (phase *BuildPhase) prepareStageInstructions(ctx context.Context, img *imag for key, value := range serviceLabels { labels = append(labels, fmt.Sprintf("%s=%v", key, value)) } - stageImage.Builder.DockerfileStageBuilder().AppendLabels(labels...) + stageImage.Builder.DockerfileBuilder().AppendLabels(labels...) phase.Conveyor.AppendOnTerminateFunc(func() error { - return stageImage.Builder.DockerfileStageBuilder().Cleanup(ctx) + return stageImage.Builder.DockerfileBuilder().Cleanup(ctx) }) default: @@ -750,7 +750,7 @@ func (phase *BuildPhase) atomicBuildStageImage(ctx context.Context, img *image.I if err := logboek.Context(ctx).Streams().DoErrorWithTag(fmt.Sprintf("%s/%s", img.LogName(), stg.Name()), img.LogTagStyle(), func() error { switch { case stg.Name() == "dockerfile": - return stageImage.Builder.DockerfileStageBuilder().Build(ctx) + return stageImage.Builder.DockerfileBuilder().Build(ctx) case phase.Conveyor.UseLegacyStapelBuilder(phase.Conveyor.ContainerBackend): return stageImage.Builder.LegacyStapelStageBuilder().Build(ctx, phase.ImageBuildOptions) default: diff --git a/pkg/build/stage/data_test.go b/pkg/build/stage/data_test.go index d62f5fa41f..35487f67a4 100644 --- a/pkg/build/stage/data_test.go +++ b/pkg/build/stage/data_test.go @@ -117,16 +117,16 @@ func CheckImageDependenciesAfterPrepare(img *LegacyImageStub, stageBuilder *stag } if dep.TargetBuildArgImageName != "" { - Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileStageBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageName, dep.GetDockerImageName()))).To(BeTrue()) + Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageName, dep.GetDockerImageName()))).To(BeTrue()) } if dep.TargetBuildArgImageRepo != "" { - Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileStageBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageRepo, dep.DockerImageRepo))).To(BeTrue()) + Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageRepo, dep.DockerImageRepo))).To(BeTrue()) } if dep.TargetBuildArgImageTag != "" { - Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileStageBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageTag, dep.DockerImageTag))).To(BeTrue()) + Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageTag, dep.DockerImageTag))).To(BeTrue()) } if dep.TargetBuildArgImageID != "" { - Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileStageBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageID, dep.DockerImageID))).To(BeTrue()) + Expect(util.IsStringsContainValue(stageBuilder.GetDockerfileBuilderImplementation().BuildDockerfileOptions.BuildArgs, fmt.Sprintf("%s=%s", dep.TargetBuildArgImageID, dep.DockerImageID))).To(BeTrue()) } } } diff --git a/pkg/build/stage/dockerfile.go b/pkg/build/stage/dockerfile.go index 58b09432ed..28f95ee301 100644 --- a/pkg/build/stage/dockerfile.go +++ b/pkg/build/stage/dockerfile.go @@ -696,16 +696,16 @@ func (s *DockerfileStage) PrepareImage(ctx context.Context, c Conveyor, cr conta return err } - if err := s.SetupDockerImageBuilder(stageImage.Builder.DockerfileStageBuilder(), c); err != nil { + if err := s.SetupDockerImageBuilder(stageImage.Builder.DockerfileBuilder(), c); err != nil { return err } - stageImage.Builder.DockerfileStageBuilder().SetContextArchivePath(archivePath) + stageImage.Builder.DockerfileBuilder().SetContextArchivePath(archivePath) - stageImage.Builder.DockerfileStageBuilder().AppendLabels(fmt.Sprintf("%s=%s", image.WerfProjectRepoCommitLabel, c.GiterminismManager().HeadCommit())) + stageImage.Builder.DockerfileBuilder().AppendLabels(fmt.Sprintf("%s=%s", image.WerfProjectRepoCommitLabel, c.GiterminismManager().HeadCommit())) if c.GiterminismManager().Dev() { - stageImage.Builder.DockerfileStageBuilder().AppendLabels(fmt.Sprintf("%s=true", image.WerfDevLabel)) + stageImage.Builder.DockerfileBuilder().AppendLabels(fmt.Sprintf("%s=true", image.WerfDevLabel)) } return nil @@ -746,7 +746,7 @@ func (s *DockerfileStage) prepareContextArchive(ctx context.Context, giterminism return archivePath, nil } -func (s *DockerfileStage) SetupDockerImageBuilder(b stage_builder.DockerfileStageBuilderInterface, c Conveyor) error { +func (s *DockerfileStage) SetupDockerImageBuilder(b stage_builder.DockerfileBuilderInterface, c Conveyor) error { b.SetDockerfile(s.dockerfile) b.SetDockerfileCtxRelPath(s.dockerfilePath) diff --git a/pkg/buildah/common.go b/pkg/buildah/common.go index c7c5d13506..cb99cb1107 100644 --- a/pkg/buildah/common.go +++ b/pkg/buildah/common.go @@ -38,9 +38,13 @@ type CommonOpts struct { LogWriter io.Writer } +type BuildFromCommandsOpts struct { + CommonOpts +} + type BuildFromDockerfileOpts struct { CommonOpts - ContextTar io.Reader + ContextDir string BuildArgs map[string]string Target string } @@ -81,6 +85,19 @@ type ConfigOpts struct { User string Workdir string Healthcheck string + OnBuild string + StopSignal string + Shell []string +} + +type CopyOpts struct { + CommonOpts + From string +} + +type AddOpts struct { + CommonOpts + ContextDir string } type ( @@ -96,7 +113,7 @@ type ( type Buildah interface { Tag(ctx context.Context, ref, newRef string, opts TagOpts) error Push(ctx context.Context, ref string, opts PushOpts) error - BuildFromDockerfile(ctx context.Context, dockerfile []byte, opts BuildFromDockerfileOpts) (string, error) + BuildFromDockerfile(ctx context.Context, dockerfile string, opts BuildFromDockerfileOpts) (string, error) RunCommand(ctx context.Context, container string, command []string, opts RunCommandOpts) error FromCommand(ctx context.Context, container, image string, opts FromCommandOpts) (string, error) Pull(ctx context.Context, ref string, opts PullOpts) error @@ -107,6 +124,8 @@ type Buildah interface { Umount(ctx context.Context, container string, opts UmountOpts) error Commit(ctx context.Context, container string, opts CommitOpts) (string, error) Config(ctx context.Context, container string, opts ConfigOpts) error + Copy(ctx context.Context, container, contextDir string, src []string, dst string, opts CopyOpts) error + Add(ctx context.Context, container string, src []string, dst string, opts AddOpts) error } type Mode string diff --git a/pkg/buildah/native_linux.go b/pkg/buildah/native_linux.go index 57068e4838..5819501313 100644 --- a/pkg/buildah/native_linux.go +++ b/pkg/buildah/native_linux.go @@ -37,7 +37,6 @@ import ( "github.com/sirupsen/logrus" "gopkg.in/errgo.v2/fmt/errors" - "github.com/werf/logboek" "github.com/werf/werf/pkg/buildah/thirdparty" "github.com/werf/werf/pkg/util" ) @@ -229,7 +228,7 @@ func (b *NativeBuildah) Push(ctx context.Context, ref string, opts PushOpts) err return nil } -func (b *NativeBuildah) BuildFromDockerfile(ctx context.Context, dockerfile []byte, opts BuildFromDockerfileOpts) (string, error) { +func (b *NativeBuildah) BuildFromDockerfile(ctx context.Context, dockerfile string, opts BuildFromDockerfileOpts) (string, error) { buildOpts := define.BuildOptions{ Isolation: define.Isolation(b.Isolation), Args: opts.BuildArgs, @@ -257,20 +256,11 @@ func (b *NativeBuildah) BuildFromDockerfile(ctx context.Context, dockerfile []by buildOpts.Err = errLog } - sessionTmpDir, contextTmpDir, dockerfileTmpPath, err := b.prepareBuildFromDockerfile(dockerfile, opts.ContextTar) - if err != nil { - return "", fmt.Errorf("error preparing for build from dockerfile: %w", err) - } - defer func() { - if err = os.RemoveAll(sessionTmpDir); err != nil { - logboek.Warn().LogF("unable to remove session tmp dir %q: %s\n", sessionTmpDir, err) - } - }() - buildOpts.ContextDirectory = contextTmpDir + buildOpts.ContextDirectory = opts.ContextDir - imageId, _, err := imagebuildah.BuildDockerfiles(ctx, b.Store, buildOpts, dockerfileTmpPath) + imageId, _, err := imagebuildah.BuildDockerfiles(ctx, b.Store, buildOpts, dockerfile) if err != nil { - return "", fmt.Errorf("unable to build Dockerfile %q:\n%s\n%w", dockerfileTmpPath, errLog.String(), err) + return "", fmt.Errorf("unable to build Dockerfile %q:\n%s\n%w", dockerfile, errLog.String(), err) } return imageId, nil @@ -475,6 +465,10 @@ func (b *NativeBuildah) Config(ctx context.Context, container string, opts Confi builder.SetPort(expose) } + if len(opts.Shell) > 0 { + builder.SetShell(opts.Shell) + } + if len(opts.Cmd) > 0 { builder.SetCmd(opts.Cmd) } @@ -499,9 +493,49 @@ func (b *NativeBuildah) Config(ctx context.Context, container string, opts Confi } } + if opts.StopSignal != "" { + builder.SetStopSignal(opts.StopSignal) + } + + if opts.OnBuild != "" { + builder.SetOnBuild(opts.OnBuild) + } + return builder.Save() } +func (b *NativeBuildah) Copy(ctx context.Context, container, contextDir string, src []string, dst string, opts CopyOpts) error { + builder, err := b.getBuilderFromContainer(ctx, container) + if err != nil { + return fmt.Errorf("error getting builder: %w", err) + } + + if err := builder.Add(dst, false, buildah.AddAndCopyOptions{ + PreserveOwnership: false, + ContextDir: contextDir, + }, src...); err != nil { + return fmt.Errorf("error copying files to %q: %w", dst, err) + } + + return nil +} + +func (b *NativeBuildah) Add(ctx context.Context, container string, src []string, dst string, opts AddOpts) error { + builder, err := b.getBuilderFromContainer(ctx, container) + if err != nil { + return fmt.Errorf("error getting builder: %w", err) + } + + if err := builder.Add(dst, true, buildah.AddAndCopyOptions{ + PreserveOwnership: false, + ContextDir: opts.ContextDir, + }, src...); err != nil { + return fmt.Errorf("error adding files to %q: %w", dst, err) + } + + return nil +} + func (b *NativeBuildah) getImage(ref string) (*libimage.Image, error) { image, _, err := b.Runtime.LookupImage(ref, &libimage.LookupImageOptions{ ManifestList: true, diff --git a/pkg/container_backend/buildah_backend.go b/pkg/container_backend/buildah_backend.go index 7044f14e28..f0164f5f2b 100644 --- a/pkg/container_backend/buildah_backend.go +++ b/pkg/container_backend/buildah_backend.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "os" "path/filepath" "sort" @@ -416,6 +417,158 @@ func (runtime *BuildahBackend) applyDependenciesImports(ctx context.Context, con return nil } +func (runtime *BuildahBackend) BuildDockerfileStage(ctx context.Context, image ImageInterface, contextTar io.ReadCloser, opts BuildDockerfileStageOptions, commands ...any) (string, error) { + var container *containerDesc + if c, err := runtime.createContainers(ctx, []string{image.Name()}); err != nil { + return "", err + } else { + container = c[0] + } + defer func() { + if err := runtime.removeContainers(ctx, []*containerDesc{container}); err != nil { + logboek.Context(ctx).Error().LogF("ERROR: unable to remove temporary build container: %s\n", err) + } + }() + // TODO: cleanup orphan build containers in werf-host-cleanup procedure + + logboek.Context(ctx).Debug().LogF("Mounting build container %s\n", container.Name) + if err := runtime.mountContainers(ctx, []*containerDesc{container}); err != nil { + return "", fmt.Errorf("unable to mount build container %s: %w", container.Name, err) + } + defer func() { + logboek.Context(ctx).Debug().LogF("Unmounting build container %s\n", container.Name) + if err := runtime.unmountContainers(ctx, []*containerDesc{container}); err != nil { + logboek.Context(ctx).Error().LogF("ERROR: unable to unmount containers: %s\n", err) + } + }() + + contextTmpDir, err := runtime.ExtractContext(contextTar) + if err != nil { + return "", fmt.Errorf("error extracting context: %w", err) + } + defer func() { + if err := os.RemoveAll(contextTmpDir); err != nil { + logboek.Context(ctx).Error().LogF("ERROR: unable to remove temporary context dir %s: %s\n", contextTmpDir, err) + } + }() + + logboek.Context(ctx).Debug().LogF("Executing commands for build container %s\n", container.Name) + for _, command := range commands { + switch cmd := command.(type) { + case CommandLabel: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Labels: cmd.LabelsAsList(), + }); err != nil { + return "", fmt.Errorf("error setting labels %v for container %s: %w", cmd.LabelsAsList(), container.Name, err) + } + case CommandExpose: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Expose: cmd.Ports, + }); err != nil { + return "", fmt.Errorf("error setting exposed ports %v for container %s: %w", cmd.Ports, container.Name, err) + } + case CommandVolume: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Volumes: cmd.Volumes, + }); err != nil { + return "", fmt.Errorf("error setting volumes %v for container %s: %w", cmd.Volumes, container.Name, err) + } + case CommandOnBuild: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + OnBuild: cmd.Instruction, + }); err != nil { + return "", fmt.Errorf("error setting onbuild %v for container %s: %w", cmd.Instruction, container.Name, err) + } + case CommandStopSignal: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + StopSignal: cmd.Signal, + }); err != nil { + return "", fmt.Errorf("error setting stop signal %v for container %s: %w", cmd.Signal, container.Name, err) + } + case CommandShell: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Shell: cmd.Shell, + }); err != nil { + return "", fmt.Errorf("error setting shell %v for container %s: %w", cmd.Shell, container.Name, err) + } + case CommandEnv: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Envs: cmd.Envs, + }); err != nil { + return "", fmt.Errorf("error setting envs %v for container %s: %w", cmd.Envs, container.Name, err) + } + case CommandRun: + if err := runtime.buildah.RunCommand(ctx, container.Name, cmd.Command, buildah.RunCommandOpts{ + // FIXME(ilya-lesikov): should we suppress or not? + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + }); err != nil { + return "", fmt.Errorf("error running command %v for container %s: %w", cmd.Command, container.Name, err) + } + case CommandCopy: + if err := runtime.buildah.Copy(ctx, container.Name, contextTmpDir, cmd.Src, cmd.Dst, buildah.CopyOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + From: cmd.From, + }); err != nil { + return "", fmt.Errorf("error copying %v to %s for container %s: %w", cmd.Src, cmd.Dst, container.Name, err) + } + case CommandAdd: + if err := runtime.buildah.Add(ctx, container.Name, cmd.Src, cmd.Dst, buildah.AddOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + ContextDir: contextTmpDir, + }); err != nil { + return "", fmt.Errorf("error adding %v to %s for container %s: %w", cmd.Src, cmd.Dst, container.Name, err) + } + case CommandUser: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + User: cmd.User, + }); err != nil { + return "", fmt.Errorf("error setting user %s for container %s: %w", cmd.User, container.Name, err) + } + case CommandWorkdir: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Workdir: cmd.Workdir, + }); err != nil { + return "", fmt.Errorf("error setting workdir %s for container %s: %w", cmd.Workdir, container.Name, err) + } + case CommandEntrypoint: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Entrypoint: cmd.Entrypoint, + }); err != nil { + return "", fmt.Errorf("error setting entrypoint %v for container %s: %w", cmd.Entrypoint, container.Name, err) + } + case CommandCmd: + if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + Cmd: cmd.Cmd, + }); err != nil { + return "", fmt.Errorf("error setting cmd %v for container %s: %w", cmd.Cmd, container.Name, err) + } + default: + return "", fmt.Errorf("invalid command type: %T", command) + } + } + + logboek.Context(ctx).Debug().LogF("Committing build container %s\n", container.Name) + imageID, err := runtime.buildah.Commit(ctx, container.Name, buildah.CommitOpts{ + CommonOpts: runtime.getBuildahCommonOpts(ctx, true), + }) + if err != nil { + return "", fmt.Errorf("error committing container %s: %w", container.Name, err) + } + + return imageID, nil +} + func (runtime *BuildahBackend) BuildStapelStage(ctx context.Context, opts BuildStapelStageOptions) (string, error) { var container *containerDesc if c, err := runtime.createContainers(ctx, []string{opts.BaseImage}); err != nil { @@ -551,7 +704,7 @@ func (runtime *BuildahBackend) Push(ctx context.Context, ref string, opts PushOp }) } -func (runtime *BuildahBackend) BuildDockerfile(ctx context.Context, dockerfile []byte, opts BuildDockerfileOpts) (string, error) { +func (runtime *BuildahBackend) BuildDockerfile(ctx context.Context, dockerfileContent []byte, opts BuildDockerfileOpts) (string, error) { buildArgs := make(map[string]string) for _, argStr := range opts.BuildArgs { argParts := strings.SplitN(argStr, "=", 2) @@ -561,11 +714,35 @@ func (runtime *BuildahBackend) BuildDockerfile(ctx context.Context, dockerfile [ buildArgs[argParts[0]] = argParts[1] } - return runtime.buildah.BuildFromDockerfile(ctx, dockerfile, buildah.BuildFromDockerfileOpts{ + contextTmpDir, err := runtime.ExtractContext(opts.ContextTar) + if err != nil { + return "", fmt.Errorf("error extracting context: %w", err) + } + defer func() { + if err := os.RemoveAll(contextTmpDir); err != nil { + logboek.Context(ctx).Error().LogF("ERROR: unable to remove temporary context dir %s: %s\n", contextTmpDir, err) + } + }() + + dockerfile, err := ioutil.TempFile(runtime.TmpDir, "*.Dockerfile") + if err != nil { + return "", fmt.Errorf("error creating temporary dockerfile: %w", err) + } + + if _, err := dockerfile.Write(dockerfileContent); err != nil { + return "", fmt.Errorf("error writing temporary dockerfile: %w", err) + } + defer func() { + if err := os.Remove(dockerfile.Name()); err != nil { + logboek.Context(ctx).Error().LogF("ERROR: unable to remove temporary dockerfile %s: %s\n", dockerfile.Name(), err) + } + }() + + return runtime.buildah.BuildFromDockerfile(ctx, dockerfile.Name(), buildah.BuildFromDockerfileOpts{ CommonOpts: buildah.CommonOpts{ LogWriter: logboek.Context(ctx).OutStream(), }, - ContextTar: opts.ContextTar, + ContextDir: contextTmpDir, BuildArgs: buildArgs, Target: opts.Target, }) @@ -685,6 +862,21 @@ func (runtime *BuildahBackend) RemoveHostDirs(ctx context.Context, mountDir stri }) } +func (runtime *BuildahBackend) ExtractContext(contextTar io.Reader) (string, error) { + contextTmpDir, err := ioutil.TempDir(runtime.TmpDir, "context") + if err != nil { + return "", fmt.Errorf("unable to create context tmp dir: %w", err) + } + + if contextTar != nil { + if err := util.ExtractTar(contextTar, contextTmpDir, util.ExtractTarOptions{}); err != nil { + return "", fmt.Errorf("unable to extract context tar to tmp context dir: %w", err) + } + } + + return contextTmpDir, nil +} + func parseVolume(volume string) (string, string, error) { volumeParts := strings.SplitN(volume, ":", 2) if len(volumeParts) != 2 { diff --git a/pkg/container_backend/docker_server_backend.go b/pkg/container_backend/docker_server_backend.go index aace6f9771..f582ce0021 100644 --- a/pkg/container_backend/docker_server_backend.go +++ b/pkg/container_backend/docker_server_backend.go @@ -3,6 +3,7 @@ package container_backend import ( "context" "fmt" + "io" "strings" "github.com/docker/docker/api/types" @@ -79,6 +80,10 @@ func (runtime *DockerServerBackend) BuildDockerfile(ctx context.Context, _ []byt return tempID, docker.CliBuild_LiveOutputWithCustomIn(ctx, opts.ContextTar, cliArgs...) } +func (runtime *DockerServerBackend) BuildDockerfileStage(ctx context.Context, image ImageInterface, contextTar io.ReadCloser, opts BuildDockerfileStageOptions, commands ...any) (string, error) { + panic("not implemented") +} + // ShouldCleanupDockerfileImage for docker-server backend we should cleanup image built from dockerfrom tagged with tempID // which is implementation detail of the BuildDockerfile. func (runtime *DockerServerBackend) ShouldCleanupDockerfileImage() bool { diff --git a/pkg/container_backend/dockerfile_commands.go b/pkg/container_backend/dockerfile_commands.go new file mode 100644 index 0000000000..09f71aaf4b --- /dev/null +++ b/pkg/container_backend/dockerfile_commands.go @@ -0,0 +1,84 @@ +package container_backend + +import "fmt" + +type CommandEnv struct { + Envs map[string]string +} + +type CommandCopy struct { + From string + Src []string + Dst string +} + +type CommandAdd struct { + Src []string + Dst string +} + +type CommandRun struct { + Command []string +} + +type CommandEntrypoint struct { + Entrypoint []string +} + +type CommandCmd struct { + Cmd []string +} + +type CommandUser struct { + User string +} + +type CommandWorkdir struct { + Workdir string +} + +type CommandExpose struct { + Ports []string +} + +type CommandVolume struct { + Volumes []string +} + +type CommandOnBuild struct { + Instruction string +} + +type CommandStopSignal struct { + Signal string +} + +type CommandShell struct { + Shell []string +} + +type CommandHealthcheck struct { + Type HealthcheckType + Command string +} + +type HealthcheckType string + +var ( + HealthcheckTypeNone HealthcheckType = "NONE" + HealthcheckTypeCmd HealthcheckType = "CMD" + HealthcheckTypeCmdShell HealthcheckType = "CMD-SHELL" +) + +type CommandLabel struct { + Labels map[string]string +} + +func (c *CommandLabel) LabelsAsList() []string { + var labels []string + for k, v := range c.Labels { + labels = append(labels, fmt.Sprintf("%s=%s", k, v)) + } + + return labels +} diff --git a/pkg/container_backend/interface.go b/pkg/container_backend/interface.go index 4a44e6807b..701ee88261 100644 --- a/pkg/container_backend/interface.go +++ b/pkg/container_backend/interface.go @@ -43,6 +43,10 @@ type BuildDockerfileOpts struct { Tags []string } +type BuildDockerfileStageOptions struct { + CommonOpts +} + type ContainerBackend interface { Tag(ctx context.Context, ref, newRef string, opts TagOpts) error Push(ctx context.Context, ref string, opts PushOpts) error @@ -51,6 +55,7 @@ type ContainerBackend interface { GetImageInfo(ctx context.Context, ref string, opts GetImageInfoOpts) (*image.Info, error) BuildDockerfile(ctx context.Context, dockerfile []byte, opts BuildDockerfileOpts) (string, error) + BuildDockerfileStage(ctx context.Context, image ImageInterface, contextTar io.ReadCloser, opts BuildDockerfileStageOptions, commands ...any) (string, error) BuildStapelStage(ctx context.Context, opts BuildStapelStageOptions) (string, error) CalculateDependencyImportChecksum(ctx context.Context, dependencyImport DependencyImportSpec) (string, error) diff --git a/pkg/container_backend/perf_check_container_backend.go b/pkg/container_backend/perf_check_container_backend.go index 1f00639190..976f726426 100644 --- a/pkg/container_backend/perf_check_container_backend.go +++ b/pkg/container_backend/perf_check_container_backend.go @@ -2,6 +2,7 @@ package container_backend import ( "context" + "io" "github.com/werf/logboek" "github.com/werf/werf/pkg/image" @@ -71,6 +72,14 @@ func (runtime *PerfCheckContainerBackend) BuildDockerfile(ctx context.Context, d return } +func (runtime *PerfCheckContainerBackend) BuildDockerfileStage(ctx context.Context, image ImageInterface, contextTar io.ReadCloser, opts BuildDockerfileStageOptions, commands ...any) (resID string, resErr error) { + logboek.Context(ctx).Default().LogProcess("ContainerBackend.BuildDockerfile"). + Do(func() { + resID, resErr = runtime.ContainerBackend.BuildDockerfileStage(ctx, image, contextTar, opts, commands) + }) + return +} + func (runtime *PerfCheckContainerBackend) BuildStapelStage(ctx context.Context, opts BuildStapelStageOptions) (resID string, resErr error) { logboek.Context(ctx).Default().LogProcess("ContainerBackend.BuildDockerfile"). Do(func() { diff --git a/pkg/container_backend/stage_builder/dockerfile_builder.go b/pkg/container_backend/stage_builder/dockerfile_builder.go new file mode 100644 index 0000000000..d59d163d5f --- /dev/null +++ b/pkg/container_backend/stage_builder/dockerfile_builder.go @@ -0,0 +1,115 @@ +package stage_builder + +import ( + "context" + "fmt" + "os" + + "github.com/werf/logboek" + "github.com/werf/werf/pkg/container_backend" +) + +type DockerfileBuilderInterface interface { + Build(ctx context.Context) error + Cleanup(ctx context.Context) error + SetDockerfile(dockerfile []byte) + SetDockerfileCtxRelPath(dockerfileCtxRelPath string) + SetTarget(target string) + AppendBuildArgs(args ...string) + AppendAddHost(addHost ...string) + SetNetwork(network string) + SetSSH(ssh string) + AppendLabels(labels ...string) + SetContextArchivePath(contextArchivePath string) +} + +type DockerfileBuilder struct { + ContainerBackend container_backend.ContainerBackend + Dockerfile []byte + BuildDockerfileOptions container_backend.BuildDockerfileOpts + ContextArchivePath string + + Image container_backend.ImageInterface +} + +func NewDockerfileBuilder(containerBackend container_backend.ContainerBackend, image container_backend.ImageInterface) *DockerfileBuilder { + return &DockerfileBuilder{ContainerBackend: containerBackend, Image: image} +} + +func (b *DockerfileBuilder) Build(ctx context.Context) error { + // filePathToStdin != "" ?? + + if container_backend.Debug() { + fmt.Printf("[DOCKER BUILD] context archive path: %s\n", b.ContextArchivePath) + } + + contextReader, err := os.Open(b.ContextArchivePath) + if err != nil { + return fmt.Errorf("unable to open context archive %q: %w", b.ContextArchivePath, err) + } + defer contextReader.Close() + + opts := b.BuildDockerfileOptions + opts.ContextTar = contextReader + + if container_backend.Debug() { + fmt.Printf("ContextArchivePath=%q\n", b.ContextArchivePath) + fmt.Printf("BiuldDockerfileOptions: %#v\n", opts) + } + + builtID, err := b.ContainerBackend.BuildDockerfile(ctx, b.Dockerfile, opts) + if err != nil { + return fmt.Errorf("error building dockerfile with %s: %w", b.ContainerBackend.String(), err) + } + b.Image.SetBuiltID(builtID) + + return nil +} + +func (b *DockerfileBuilder) Cleanup(ctx context.Context) error { + if !b.ContainerBackend.ShouldCleanupDockerfileImage() { + return nil + } + + logboek.Context(ctx).Info().LogF("Cleanup built dockerfile image %q\n", b.Image.BuiltID()) + if err := b.ContainerBackend.Rmi(ctx, b.Image.BuiltID(), container_backend.RmiOpts{}); err != nil { + return fmt.Errorf("unable to remove built dockerfile image %q: %w", b.Image.BuiltID(), err) + } + return nil +} + +func (b *DockerfileBuilder) SetDockerfile(dockerfile []byte) { + b.Dockerfile = dockerfile +} + +func (b *DockerfileBuilder) SetDockerfileCtxRelPath(dockerfileCtxRelPath string) { + b.BuildDockerfileOptions.DockerfileCtxRelPath = dockerfileCtxRelPath +} + +func (b *DockerfileBuilder) SetTarget(target string) { + b.BuildDockerfileOptions.Target = target +} + +func (b *DockerfileBuilder) AppendBuildArgs(args ...string) { + b.BuildDockerfileOptions.BuildArgs = append(b.BuildDockerfileOptions.BuildArgs, args...) +} + +func (b *DockerfileBuilder) AppendAddHost(addHost ...string) { + b.BuildDockerfileOptions.AddHost = append(b.BuildDockerfileOptions.AddHost, addHost...) +} + +func (b *DockerfileBuilder) SetNetwork(network string) { + b.BuildDockerfileOptions.Network = network +} + +func (b *DockerfileBuilder) SetSSH(ssh string) { + b.BuildDockerfileOptions.SSH = ssh +} + +func (b *DockerfileBuilder) AppendLabels(labels ...string) { + b.BuildDockerfileOptions.Labels = append(b.BuildDockerfileOptions.Labels, labels...) +} + +func (b *DockerfileBuilder) SetContextArchivePath(contextArchivePath string) { + b.ContextArchivePath = contextArchivePath +} diff --git a/pkg/container_backend/stage_builder/dockerfile_stage_builder.go b/pkg/container_backend/stage_builder/dockerfile_stage_builder.go index 3655f4ee9e..6e5c66b40f 100644 --- a/pkg/container_backend/stage_builder/dockerfile_stage_builder.go +++ b/pkg/container_backend/stage_builder/dockerfile_stage_builder.go @@ -3,113 +3,64 @@ package stage_builder import ( "context" "fmt" - "os" + "io" - "github.com/werf/logboek" "github.com/werf/werf/pkg/container_backend" ) type DockerfileStageBuilderInterface interface { - Build(ctx context.Context) error - Cleanup(ctx context.Context) error - SetDockerfile(dockerfile []byte) - SetDockerfileCtxRelPath(dockerfileCtxRelPath string) - SetTarget(target string) - AppendBuildArgs(args ...string) - AppendAddHost(addHost ...string) - SetNetwork(network string) - SetSSH(ssh string) - AppendLabels(labels ...string) - SetContextArchivePath(contextArchivePath string) + AppendPreCommands(commands ...any) DockerfileStageBuilderInterface + AppendMainCommands(commands ...any) DockerfileStageBuilderInterface + AppendPostCommands(commands ...any) DockerfileStageBuilderInterface + Build(ctx context.Context, contextTar io.ReadCloser) error } type DockerfileStageBuilder struct { - ContainerBackend container_backend.ContainerBackend - Dockerfile []byte - BuildDockerfileOptions container_backend.BuildDockerfileOpts - ContextArchivePath string + preCommands []any + mainCommands []any + postCommands []any - Image container_backend.ImageInterface + fromImage container_backend.ImageInterface + resultImage container_backend.ImageInterface + containerBackend container_backend.ContainerBackend } -func NewDockerfileStageBuilder(containerBackend container_backend.ContainerBackend, image container_backend.ImageInterface) *DockerfileStageBuilder { - return &DockerfileStageBuilder{ContainerBackend: containerBackend, Image: image} -} - -func (b *DockerfileStageBuilder) Build(ctx context.Context) error { - // filePathToStdin != "" ?? - - if container_backend.Debug() { - fmt.Printf("[DOCKER BUILD] context archive path: %s\n", b.ContextArchivePath) - } - - contextReader, err := os.Open(b.ContextArchivePath) - if err != nil { - return fmt.Errorf("unable to open context archive %q: %w", b.ContextArchivePath, err) - } - defer contextReader.Close() - - opts := b.BuildDockerfileOptions - opts.ContextTar = contextReader - - if container_backend.Debug() { - fmt.Printf("ContextArchivePath=%q\n", b.ContextArchivePath) - fmt.Printf("BiuldDockerfileOptions: %#v\n", opts) +func NewDockerfileStageBuilder(containerBackend container_backend.ContainerBackend, fromImage, resultImage container_backend.ImageInterface) *DockerfileStageBuilder { + return &DockerfileStageBuilder{ + containerBackend: containerBackend, + fromImage: fromImage, + resultImage: resultImage, } - - builtID, err := b.ContainerBackend.BuildDockerfile(ctx, b.Dockerfile, opts) - if err != nil { - return fmt.Errorf("error building dockerfile with %s: %w", b.ContainerBackend.String(), err) - } - b.Image.SetBuiltID(builtID) - - return nil } -func (b *DockerfileStageBuilder) Cleanup(ctx context.Context) error { - if !b.ContainerBackend.ShouldCleanupDockerfileImage() { - return nil - } - - logboek.Context(ctx).Info().LogF("Cleanup built dockerfile image %q\n", b.Image.BuiltID()) - if err := b.ContainerBackend.Rmi(ctx, b.Image.BuiltID(), container_backend.RmiOpts{}); err != nil { - return fmt.Errorf("unable to remove built dockerfile image %q: %w", b.Image.BuiltID(), err) - } - return nil -} - -func (b *DockerfileStageBuilder) SetDockerfile(dockerfile []byte) { - b.Dockerfile = dockerfile -} - -func (b *DockerfileStageBuilder) SetDockerfileCtxRelPath(dockerfileCtxRelPath string) { - b.BuildDockerfileOptions.DockerfileCtxRelPath = dockerfileCtxRelPath -} - -func (b *DockerfileStageBuilder) SetTarget(target string) { - b.BuildDockerfileOptions.Target = target +func (b *DockerfileStageBuilder) AppendPreCommands(commands ...any) DockerfileStageBuilderInterface { + b.preCommands = append(b.preCommands, commands...) + return b } -func (b *DockerfileStageBuilder) AppendBuildArgs(args ...string) { - b.BuildDockerfileOptions.BuildArgs = append(b.BuildDockerfileOptions.BuildArgs, args...) +func (b *DockerfileStageBuilder) AppendMainCommands(commands ...any) DockerfileStageBuilderInterface { + b.mainCommands = append(b.mainCommands, commands...) + return b } -func (b *DockerfileStageBuilder) AppendAddHost(addHost ...string) { - b.BuildDockerfileOptions.AddHost = append(b.BuildDockerfileOptions.AddHost, addHost...) +func (b *DockerfileStageBuilder) AppendPostCommands(commands ...any) DockerfileStageBuilderInterface { + b.postCommands = append(b.postCommands, commands...) + return b } -func (b *DockerfileStageBuilder) SetNetwork(network string) { - b.BuildDockerfileOptions.Network = network -} - -func (b *DockerfileStageBuilder) SetSSH(ssh string) { - b.BuildDockerfileOptions.SSH = ssh -} +func (b *DockerfileStageBuilder) Build(ctx context.Context, contextTar io.ReadCloser) error { + commands := append(append(b.preCommands, b.mainCommands...), b.postCommands) + if len(commands) == 0 { + b.resultImage.SetName(b.fromImage.Name()) + b.resultImage.SetBuiltID(b.fromImage.BuiltID()) + return nil + } -func (b *DockerfileStageBuilder) AppendLabels(labels ...string) { - b.BuildDockerfileOptions.Labels = append(b.BuildDockerfileOptions.Labels, labels...) -} + if builtID, err := b.containerBackend.BuildDockerfileStage(ctx, b.resultImage, contextTar, container_backend.BuildDockerfileStageOptions{}, commands...); err != nil { + return fmt.Errorf("error building dockerfile stage: %w", err) + } else { + b.resultImage.SetBuiltID(builtID) + } -func (b *DockerfileStageBuilder) SetContextArchivePath(contextArchivePath string) { - b.ContextArchivePath = contextArchivePath + return nil } diff --git a/pkg/container_backend/stage_builder/stage_builder.go b/pkg/container_backend/stage_builder/stage_builder.go index 0ad21fbc0b..3fc830c203 100644 --- a/pkg/container_backend/stage_builder/stage_builder.go +++ b/pkg/container_backend/stage_builder/stage_builder.go @@ -4,6 +4,7 @@ import "github.com/werf/werf/pkg/container_backend" type StageBuilderInterface interface { StapelStageBuilder() StapelStageBuilderInterface + DockerfileBuilder() DockerfileBuilderInterface DockerfileStageBuilder() DockerfileStageBuilderInterface LegacyStapelStageBuilder() LegacyStapelStageBuilderInterface } @@ -13,10 +14,15 @@ type StageBuilder struct { FromImage container_backend.ImageInterface Image container_backend.LegacyImageInterface // TODO: use ImageInterface + dockerfileBuilder *DockerfileBuilder dockerfileStageBuilder *DockerfileStageBuilder stapelStageBuilder *StapelStageBuilder } +func (stageBuilder *StageBuilder) GetDockerfileBuilderImplementation() *DockerfileBuilder { + return stageBuilder.dockerfileBuilder +} + func (stageBuilder *StageBuilder) GetDockerfileStageBuilderImplementation() *DockerfileStageBuilder { return stageBuilder.dockerfileStageBuilder } @@ -44,9 +50,16 @@ func (stageBuilder *StageBuilder) LegacyStapelStageBuilder() LegacyStapelStageBu return NewLegacyStapelStageBuilder(stageBuilder.ContainerBackend, stageBuilder.Image) } +func (stageBuilder *StageBuilder) DockerfileBuilder() DockerfileBuilderInterface { + if stageBuilder.dockerfileBuilder == nil { + stageBuilder.dockerfileBuilder = NewDockerfileBuilder(stageBuilder.ContainerBackend, stageBuilder.Image) + } + return stageBuilder.dockerfileBuilder +} + func (stageBuilder *StageBuilder) DockerfileStageBuilder() DockerfileStageBuilderInterface { if stageBuilder.dockerfileStageBuilder == nil { - stageBuilder.dockerfileStageBuilder = NewDockerfileStageBuilder(stageBuilder.ContainerBackend, stageBuilder.Image) + stageBuilder.dockerfileStageBuilder = NewDockerfileStageBuilder(stageBuilder.ContainerBackend, stageBuilder.FromImage, stageBuilder.Image) } return stageBuilder.dockerfileStageBuilder }