diff --git a/cmd/buildah-test/main.go b/cmd/buildah-test/main.go index 4d463b248d..7679bbffbb 100644 --- a/cmd/buildah-test/main.go +++ b/cmd/buildah-test/main.go @@ -47,7 +47,7 @@ echo STOP defer os.RemoveAll("/tmp/build_stage.sh") if err := b.RunCommand(ctx, "mycontainer", []string{"/.werf/build_stage.sh"}, buildah.RunCommandOpts{ - Mounts: []specs.Mount{ + LegacyMounts: []specs.Mount{ { Type: "bind", Source: "/tmp/build_stage.sh", diff --git a/pkg/build/image/dockerfile.go b/pkg/build/image/dockerfile.go index b19f19c32c..7e42c80fb9 100644 --- a/pkg/build/image/dockerfile.go +++ b/pkg/build/image/dockerfile.go @@ -129,6 +129,8 @@ func mapDockerfileToImagesSets(ctx context.Context, cfg *dockerfile.Dockerfile, var stg stage.Interface switch typedInstr := any(instr).(type) { + case *dockerfile.DockerfileStageInstruction[*dockerfile_instruction.Arg]: + // TODO: implement case *dockerfile.DockerfileStageInstruction[*dockerfile_instruction.Add]: stg = stage_instruction.NewAdd(stageName, typedInstr, dockerfileImageConfig.Dependencies, !isFirstStage, baseStageOptions) case *dockerfile.DockerfileStageInstruction[*dockerfile_instruction.Cmd]: diff --git a/pkg/buildah/common.go b/pkg/buildah/common.go index cb99cb1107..97e9bc9b13 100644 --- a/pkg/buildah/common.go +++ b/pkg/buildah/common.go @@ -10,9 +10,11 @@ import ( "runtime" "strings" + "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/opencontainers/runtime-spec/specs-go" "github.com/werf/werf/pkg/buildah/thirdparty" + "github.com/werf/werf/pkg/dockerfile/instruction" "github.com/werf/werf/pkg/util" "github.com/werf/werf/pkg/werf" ) @@ -44,6 +46,7 @@ type BuildFromCommandsOpts struct { type BuildFromDockerfileOpts struct { CommonOpts + ContextDir string BuildArgs map[string]string Target string @@ -58,46 +61,65 @@ type RunMount struct { type RunCommandOpts struct { CommonOpts - WorkingDir string - User string - Args []string - Mounts []specs.Mount + + ContextDir string + PrependShell bool + Shell []string + AddCapabilities []string + DropCapabilities []string + NetworkType instruction.NetworkType + WorkingDir string + User string + Envs []string + Mounts []instructions.Mount + LegacyMounts []specs.Mount // TODO(ilya-lesikov): migrate to Mounts } type RmiOpts struct { CommonOpts + Force bool } type CommitOpts struct { CommonOpts + Image string } type ConfigOpts struct { CommonOpts - Labels []string - Volumes []string - Expose []string - Envs map[string]string - Cmd []string - Entrypoint []string - User string - Workdir string - Healthcheck string - OnBuild string - StopSignal string - Shell []string + + Labels []string + Volumes []string + Expose []string + Envs map[string]string + Cmd []string + CmdPrependShell bool + Entrypoint []string + EntrypointPrependShell bool + User string + Workdir string + Healthcheck *thirdparty.HealthConfig + OnBuild string + StopSignal string + Shell []string + Maintainer string } type CopyOpts struct { CommonOpts - From string + + Chown string + Chmod string } type AddOpts struct { CommonOpts + ContextDir string + Chown string + Chmod string } type ( diff --git a/pkg/buildah/native_linux.go b/pkg/buildah/native_linux.go index 5819501313..b64f7c2713 100644 --- a/pkg/buildah/native_linux.go +++ b/pkg/buildah/native_linux.go @@ -32,12 +32,12 @@ import ( "github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/unshare" "github.com/hashicorp/go-multierror" - "github.com/moby/buildkit/frontend/dockerfile/instructions" - "github.com/moby/buildkit/frontend/dockerfile/parser" + "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "gopkg.in/errgo.v2/fmt/errors" "github.com/werf/werf/pkg/buildah/thirdparty" + "github.com/werf/werf/pkg/dockerfile/instruction" "github.com/werf/werf/pkg/util" ) @@ -46,6 +46,8 @@ const ( PullPushRetryDelay = 2 * time.Second ) +var DefaultShell = []string{"/bin/sh", "-c"} + func NativeProcessStartupHook() bool { if reexec.Init() { return true @@ -285,33 +287,59 @@ func (b *NativeBuildah) Umount(ctx context.Context, container string, opts Umoun } func (b *NativeBuildah) RunCommand(ctx context.Context, container string, command []string, opts RunCommandOpts) error { + builder, err := b.openContainerBuilder(ctx, container) + if err != nil { + return fmt.Errorf("unable to open container %q builder: %w", container, err) + } + + nsOpts, netPolicy := generateNamespaceOptionsAndNetworkPolicy(opts.NetworkType) + + var stdout, stderr io.Writer + stderrBuf := &bytes.Buffer{} + if opts.LogWriter != nil { + stdout = opts.LogWriter + stderr = io.MultiWriter(opts.LogWriter, stderrBuf) + } else { + stderr = stderrBuf + } + + if opts.PrependShell { + if len(opts.Shell) > 0 { + command = append(opts.Shell, command...) + } else if len(builder.Shell()) > 0 { + command = append(builder.Shell(), command...) + } else { + command = append(DefaultShell, command...) + } + } + runOpts := buildah.RunOptions{ + Env: opts.Envs, + ContextDir: opts.ContextDir, + AddCapabilities: opts.AddCapabilities, + DropCapabilities: opts.DropCapabilities, + Stdout: stdout, + Stderr: stderr, + NamespaceOptions: nsOpts, + ConfigureNetwork: netPolicy, Isolation: define.Isolation(b.Isolation), - Args: opts.Args, - Mounts: opts.Mounts, - ConfigureNetwork: define.NetworkEnabled, SystemContext: &b.DefaultSystemContext, WorkingDir: opts.WorkingDir, User: opts.User, Entrypoint: []string{}, Cmd: []string{}, - } - - stderr := &bytes.Buffer{} - if opts.LogWriter != nil { - runOpts.Stdout = opts.LogWriter - runOpts.Stderr = io.MultiWriter(opts.LogWriter, stderr) - } else { - runOpts.Stderr = stderr - } - builder, err := b.openContainerBuilder(ctx, container) - if err != nil { - return fmt.Errorf("unable to open container %q builder: %w", container, err) + // TODO(ilya-lesikov): implement mounts + Secrets: nil, + SSHSources: nil, + Mounts: opts.LegacyMounts, + RunMounts: nil, + StageMountPoints: nil, + ExternalImageMounts: nil, } if err := builder.Run(command, runOpts); err != nil { - return fmt.Errorf("RunCommand failed:\n%s\n%w", stderr.String(), err) + return fmt.Errorf("RunCommand failed:\n%s\n%w", stderrBuf.String(), err) } return nil @@ -453,6 +481,10 @@ func (b *NativeBuildah) Config(ctx context.Context, container string, opts Confi } } + if opts.Maintainer != "" { + builder.SetMaintainer(opts.Maintainer) + } + for name, value := range opts.Envs { builder.SetEnv(name, value) } @@ -470,11 +502,37 @@ func (b *NativeBuildah) Config(ctx context.Context, container string, opts Confi } if len(opts.Cmd) > 0 { - builder.SetCmd(opts.Cmd) + var cmd []string + if opts.CmdPrependShell { + if builder.Shell() != nil { + cmd = builder.Shell() + } else { + cmd = DefaultShell + } + + cmd = append(cmd, opts.Cmd...) + } else { + cmd = opts.Cmd + } + + builder.SetCmd(cmd) } if len(opts.Entrypoint) > 0 { - builder.SetEntrypoint(opts.Entrypoint) + var entrypoint []string + if opts.EntrypointPrependShell { + if builder.Shell() != nil { + entrypoint = builder.Shell() + } else { + entrypoint = DefaultShell + } + + entrypoint = append(entrypoint, opts.Entrypoint...) + } else { + entrypoint = opts.Entrypoint + } + + builder.SetEntrypoint(entrypoint) } if opts.User != "" { @@ -485,12 +543,8 @@ func (b *NativeBuildah) Config(ctx context.Context, container string, opts Confi builder.SetWorkDir(opts.Workdir) } - if opts.Healthcheck != "" { - if healthcheck, err := newHealthConfigFromString(builder, opts.Healthcheck); err != nil { - return fmt.Errorf("error creating HEALTHCHECK: %w", err) - } else if healthcheck != nil { - builder.SetHealthcheck(healthcheck) - } + if opts.Healthcheck != nil { + builder.SetHealthcheck((*docker.HealthConfig)(opts.Healthcheck)) } if opts.StopSignal != "" { @@ -510,10 +564,21 @@ func (b *NativeBuildah) Copy(ctx context.Context, container, contextDir string, return fmt.Errorf("error getting builder: %w", err) } + var absSrc []string + for _, s := range src { + absSrc = append(absSrc, filepath.Join(contextDir, s)) + } + if err := builder.Add(dst, false, buildah.AddAndCopyOptions{ + Chown: opts.Chown, + Chmod: opts.Chmod, PreserveOwnership: false, ContextDir: contextDir, - }, src...); err != nil { + // TODO(ilya-lesikov): ignore file? + Excludes: nil, + // TODO(ilya-lesikov): ignore file? + IgnoreFile: "", + }, absSrc...); err != nil { return fmt.Errorf("error copying files to %q: %w", dst, err) } @@ -526,10 +591,30 @@ func (b *NativeBuildah) Add(ctx context.Context, container string, src []string, return fmt.Errorf("error getting builder: %w", err) } + var expandedSrc []string + for _, s := range src { + if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { + expandedSrc = append(expandedSrc, s) + continue + } + + if opts.ContextDir == "" { + return fmt.Errorf("context dir is required for adding local files") + } + + expandedSrc = append(expandedSrc, filepath.Join(opts.ContextDir, s)) + } + if err := builder.Add(dst, true, buildah.AddAndCopyOptions{ + Chmod: opts.Chmod, + Chown: opts.Chown, PreserveOwnership: false, ContextDir: opts.ContextDir, - }, src...); err != nil { + // TODO(ilya-lesikov): ignore file? + Excludes: nil, + // TODO(ilya-lesikov): ignore file? + IgnoreFile: "", + }, expandedSrc...); err != nil { return fmt.Errorf("error adding files to %q: %w", dst, err) } @@ -694,38 +779,6 @@ func NewNativeStoreOptions(rootlessUID int, driver StorageDriver) (*thirdparty.S }, nil } -// Can return nil pointer to HealthConfig -func newHealthConfigFromString(builder *buildah.Builder, healthcheck string) (*docker.HealthConfig, error) { - if healthcheck == "" { - return nil, nil - } - - dockerfile, err := parser.Parse(bytes.NewBufferString(fmt.Sprintf("HEALTHCHECK %s", healthcheck))) - if err != nil { - return nil, fmt.Errorf("unable to parse healthcheck instruction: %w", err) - } - - var healthCheckNode *parser.Node - for _, n := range dockerfile.AST.Children { - if strings.ToLower(n.Value) == "healthcheck" { - healthCheckNode = n - } - } - if healthCheckNode == nil { - return nil, fmt.Errorf("no valid healthcheck instruction found, got %q", healthcheck) - } - - cmd, err := instructions.ParseCommand(healthCheckNode) - if err != nil { - return nil, fmt.Errorf("cannot parse healthcheck instruction: %w", err) - } - - healthcheckcmd := cmd.(*instructions.HealthCheckCommand) - healthconfig := (*docker.HealthConfig)(healthcheckcmd.Health) - - return healthconfig, nil -} - func currentRlimits() (map[int]*syscall.Rlimit, error) { result := map[int]*syscall.Rlimit{ syscall.RLIMIT_CORE: {}, @@ -769,3 +822,212 @@ func rlimitsToBuildahUlimits(rlimits map[int]*syscall.Rlimit) []string { rlimitToBuildahUlimitFn(syscall.RLIMIT_STACK, "stack"), } } + +func generateNamespaceOptionsAndNetworkPolicy(network instruction.NetworkType) (define.NamespaceOptions, define.NetworkConfigurationPolicy) { + var netPolicy define.NetworkConfigurationPolicy + nsOpts := define.NamespaceOptions{} + + switch network { + case instruction.NetworkDefault, "": + netPolicy = define.NetworkDefault + nsOpts.AddOrReplace(define.NamespaceOption{ + Name: string(specs.NetworkNamespace), + }) + case instruction.NetworkHost: + netPolicy = define.NetworkEnabled + nsOpts.AddOrReplace(define.NamespaceOption{ + Name: string(specs.NetworkNamespace), + Host: true, + }) + case instruction.NetworkNone: + netPolicy = define.NetworkDisabled + nsOpts.AddOrReplace(define.NamespaceOption{ + Name: string(specs.NetworkNamespace), + }) + default: + panic(fmt.Sprintf("unexpected network type: %v", network)) + } + + return nsOpts, netPolicy +} + +// TODO(ilya-lesikov): implement mounts +// func generateMounts(mounts []*instructions.Mount, contextDir string) (map[string]*specs.Mount, error) { +// result := map[string]*specs.Mount{} +// +// for _, mount := range mounts { +// switch mount.Type { +// case instructions.MountTypeBind: +// if m, err := generateBindMount(mount, contextDir); err != nil { +// return nil, fmt.Errorf("error generating bind mount: %w", err) +// } else { +// result[mount.Target] = m +// } +// case instructions.MountTypeCache: +// result[mount.Target] = &specs.Mount{ +// Type: parse.TypeCache, +// Source: mount.Source, +// Destination: mount.Target, +// Options: generateMountOptions(mount), +// } +// case instructions.MountTypeTmpfs: +// case instructions.MountTypeSecret: +// case instructions.MountTypeSSH: +// } +// } +// } +// +// func generateBindMount(mount *instructions.Mount, contextDir string) (*specs.Mount, string, error) { +// var mountedImage string +// var source string +// if mount.From != "" && contextDir != "" { +// // FIXME(ilya-lesikov): here also be container mounts and new contextDir: +// // /home/user1/go/pkg/mod/github.com/containers/buildah@v1.26.1/internal/parse/parse.go:117 +// source = filepath.Join(contextDir, filepath.Clean(string(filepath.Separator)+mount.Source)) +// mountedImage = "" // TODO +// } else { +// if err := parse.ValidateVolumeHostDir(mount.Source); err != nil { +// return nil, "", fmt.Errorf("invalid bind mount source %q: %w", mount.Source, err) +// } +// source = mount.Source +// } +// +// if err := parse.ValidateVolumeCtrDir(mount.Target); err != nil { +// return nil, "", fmt.Errorf("invalid bind mount target %q: %w", mount.Target, err) +// } +// +// options := []string{"rbind"} +// +// if mount.ReadOnly { +// options = append(options, "ro") +// } else { +// options = append(options, "rw") +// } +// +// var err error +// options, err = parse.ValidateVolumeOpts(options) +// if err != nil { +// return nil, "", fmt.Errorf("invalid bind mount options %v: %w", options, err) +// } +// +// return &specs.Mount{ +// Type: parse.TypeBind, +// Source: source, +// Destination: mount.Target, +// Options: options, +// }, mountedImage, nil +// } +// +// func generateCacheMount(mount *instructions.Mount) (*specs.Mount, error) { +// if err := parse.ValidateVolumeCtrDir(mount.Target); err != nil { +// return nil, fmt.Errorf("invalid cache mount target %q: %w", mount.Target, err) +// } +// +// options := []string{"bind", "shared"} +// +// if mount.ReadOnly { +// options = append(options, "ro") +// } else { +// options = append(options, "rw") +// } +// +// // var mode uint64 +// // if mount.Mode != nil { +// // mode = *mount.Mode +// // } else { +// // mode = 0o755 +// // } +// // +// // var uid uint64 +// // if mount.UID != nil { +// // uid = *mount.UID +// // } else { +// // uid = 0 +// // } +// // +// // var gid uint64 +// // if mount.GID != nil { +// // gid = *mount.GID +// // } else { +// // gid = 0 +// // } +// +// // TODO(ilya-lesikov): create cache dir/image/stage, example: +// // /home/user1/go/pkg/mod/github.com/containers/buildah@v1.26.1/internal/parse/parse.go:286 +// +// if mount.CacheSharing == "locked" { +// // TODO(ilya-lesikov): Implement cache access locking +// // /home/user1/go/pkg/mod/github.com/containers/buildah@v1.26.1/internal/parse/parse.go:335 +// } +// +// var err error +// options, err = parse.ValidateVolumeOpts(options) +// if err != nil { +// return nil, fmt.Errorf("invalid cache mount options %v: %w", options, err) +// } +// +// return &specs.Mount{ +// Type: parse.TypeBind, +// Source: mount.Source, +// Destination: mount.Target, +// Options: options, +// }, nil +// } +// +// func generateTmpFsMount(mount *instructions.Mount) (*specs.Mount, error) { +// if err := parse.ValidateVolumeCtrDir(mount.Target); err != nil { +// return nil, fmt.Errorf("invalid tmpfs mount target %q: %w", mount.Target, err) +// } +// +// options := []string{"bind", "shared"} +// +// if mount.ReadOnly { +// options = append(options, "ro") +// } else { +// options = append(options, "rw") +// } +// +// if mount.Mode != nil { +// options = append(options, fmt.Sprintf("mode=%o", *mount.Mode)) +// } +// +// var err error +// options, err = parse.ValidateVolumeOpts(options) +// if err != nil { +// return nil, fmt.Errorf("invalid tmpfs mount options %v: %w", options, err) +// } +// +// return &specs.Mount{ +// Type: parse.TypeTmpfs, +// Source: parse.TypeTmpfs, +// Destination: mount.Target, +// Options: options, +// }, nil +// } +// +// func getSecretMount(mount *instructions.Mount) (*specs.Mount, error) { +// if err := parse.ValidateVolumeCtrDir(mount.Target); err != nil { +// return nil, fmt.Errorf("invalid secret mount target %q: %w", mount.Target, err) +// } +// +// options := []string{"bind", "shared"} +// +// if mount.ReadOnly { +// options = append(options, "ro") +// } else { +// options = append(options, "rw") +// } +// +// var err error +// options, err = parse.ValidateVolumeOpts(options) +// if err != nil { +// return nil, fmt.Errorf("invalid secret mount options %v: %w", options, err) +// } +// +// return &specs.Mount{ +// Type: parse.TypeSecret, +// Source: mount.Source, +// Destination: mount.Target, +// Options: options, +// }, nil +// } diff --git a/pkg/buildah/thirdparty/healthcheck_linux.go b/pkg/buildah/thirdparty/healthcheck_linux.go new file mode 100644 index 0000000000..02ef0c9f60 --- /dev/null +++ b/pkg/buildah/thirdparty/healthcheck_linux.go @@ -0,0 +1,8 @@ +//go:build linux +// +build linux + +package thirdparty + +import "github.com/containers/buildah/docker" + +type HealthConfig docker.HealthConfig diff --git a/pkg/buildah/thirdparty/healthcheck_others.go b/pkg/buildah/thirdparty/healthcheck_others.go new file mode 100644 index 0000000000..0ec3413265 --- /dev/null +++ b/pkg/buildah/thirdparty/healthcheck_others.go @@ -0,0 +1,15 @@ +//go:build !linux +// +build !linux + +package thirdparty + +import "time" + +// Copied from github.com/containers/buildah@v1.26.1/docker/types.go:52 +type HealthConfig struct { + Test []string `json:",omitempty"` + Interval time.Duration `json:",omitempty"` + Timeout time.Duration `json:",omitempty"` + StartPeriod time.Duration `json:",omitempty"` + Retries int `json:",omitempty"` +} diff --git a/pkg/config/raw_image_from_dockerfile.go b/pkg/config/raw_image_from_dockerfile.go index ebce9985d6..95143dcae4 100644 --- a/pkg/config/raw_image_from_dockerfile.go +++ b/pkg/config/raw_image_from_dockerfile.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/werf/werf/pkg/giterminism_manager" + "github.com/werf/werf/pkg/util" ) type rawImageFromDockerfile struct { @@ -126,7 +127,7 @@ func (c *rawImageFromDockerfile) toImageFromDockerfileDirective(giterminismManag image.Dependencies = append(image.Dependencies, dependencyDirective) } - image.Staged = c.Staged + image.Staged = c.Staged || util.GetBoolEnvironmentDefaultFalse("WERF_FORCE_STAGED_DOCKERFILE") image.raw = c if err := image.validate(giterminismManager); err != nil { diff --git a/pkg/container_backend/buildah_backend.go b/pkg/container_backend/buildah_backend.go index 3e59c4a5ac..5a98c8772d 100644 --- a/pkg/container_backend/buildah_backend.go +++ b/pkg/container_backend/buildah_backend.go @@ -2,6 +2,7 @@ package container_backend import ( "bufio" + "bytes" "context" "crypto/md5" "errors" @@ -15,11 +16,14 @@ import ( "strings" "github.com/google/uuid" + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/opencontainers/runtime-spec/specs-go" copyrec "github.com/werf/copy-recurse" "github.com/werf/logboek" "github.com/werf/werf/pkg/buildah" + "github.com/werf/werf/pkg/buildah/thirdparty" "github.com/werf/werf/pkg/image" "github.com/werf/werf/pkg/path_matcher" "github.com/werf/werf/pkg/util" @@ -162,10 +166,10 @@ func (runtime *BuildahBackend) applyCommands(ctx context.Context, container *con } if err := runtime.buildah.RunCommand(ctx, container.Name, []string{"sh", destScriptPath}, buildah.RunCommandOpts{ - CommonOpts: runtime.getBuildahCommonOpts(ctx, false), - User: "0:0", - WorkingDir: "/", - Mounts: mounts, + CommonOpts: runtime.getBuildahCommonOpts(ctx, false), + User: "0:0", + WorkingDir: "/", + LegacyMounts: mounts, }); err != nil { return fmt.Errorf("unable to run commands script: %w", err) } @@ -515,6 +519,11 @@ func (runtime *BuildahBackend) BuildStapelStage(ctx context.Context, baseImage s } } + healthcheck, err := newHealthConfigFromString(opts.Healthcheck) + if err != nil { + return "", fmt.Errorf("unable to parse healthcheck %q: %w", opts.Healthcheck, err) + } + logboek.Context(ctx).Debug().LogF("Setting config for build container %q\n", container.Name) if err := runtime.buildah.Config(ctx, container.Name, buildah.ConfigOpts{ CommonOpts: runtime.getBuildahCommonOpts(ctx, true), @@ -526,7 +535,7 @@ func (runtime *BuildahBackend) BuildStapelStage(ctx context.Context, baseImage s Entrypoint: opts.Entrypoint, User: opts.User, Workdir: opts.Workdir, - Healthcheck: opts.Healthcheck, + Healthcheck: healthcheck, }); err != nil { return "", fmt.Errorf("unable to set container %q config: %w", container.Name, err) } @@ -745,7 +754,7 @@ func (runtime *BuildahBackend) RemoveHostDirs(ctx context.Context, mountDir stri return runtime.buildah.RunCommand(ctx, container.Name, append([]string{"rm", "-rf"}, containerDirs...), buildah.RunCommandOpts{ User: "0:0", WorkingDir: "/", - Mounts: []specs.Mount{ + LegacyMounts: []specs.Mount{ { Type: "bind", Source: mountDir, @@ -895,3 +904,35 @@ func getGIDFromGroupName(group, etcGroupPath string) (uint32, error) { return 0, fmt.Errorf("could not find GID for group %q in passwd file %q", group, etcGroupPath) } + +// Can return nil pointer to HealthConfig +func newHealthConfigFromString(healthcheck string) (*thirdparty.HealthConfig, error) { + if healthcheck == "" { + return nil, nil + } + + dockerfile, err := parser.Parse(bytes.NewBufferString(fmt.Sprintf("HEALTHCHECK %s", healthcheck))) + if err != nil { + return nil, fmt.Errorf("unable to parse healthcheck instruction: %w", err) + } + + var healthCheckNode *parser.Node + for _, n := range dockerfile.AST.Children { + if strings.ToLower(n.Value) == "healthcheck" { + healthCheckNode = n + } + } + if healthCheckNode == nil { + return nil, fmt.Errorf("no valid healthcheck instruction found, got %q", healthcheck) + } + + cmd, err := instructions.ParseCommand(healthCheckNode) + if err != nil { + return nil, fmt.Errorf("cannot parse healthcheck instruction: %w", err) + } + + healthcheckcmd := cmd.(*instructions.HealthCheckCommand) + healthconfig := (*thirdparty.HealthConfig)(healthcheckcmd.Health) + + return healthconfig, nil +} diff --git a/pkg/container_backend/instruction/add.go b/pkg/container_backend/instruction/add.go index 1ab8390739..c7ba853b91 100644 --- a/pkg/container_backend/instruction/add.go +++ b/pkg/container_backend/instruction/add.go @@ -22,13 +22,23 @@ func (i *Add) UsesBuildContext() bool { } func (i *Add) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { - buildContextTmpDir, err := buildContextArchive.ExtractOrGetExtractedDir(ctx) - if err != nil { - return fmt.Errorf("unable to extract build context: %w", err) + var contextDir string + if i.UsesBuildContext() { + var err error + contextDir, err = buildContextArchive.ExtractOrGetExtractedDir(ctx) + if err != nil { + return fmt.Errorf("unable to extract build context: %w", err) + } } - if err := drv.Add(ctx, containerName, i.Src, i.Dst, buildah.AddOpts{CommonOpts: drvOpts, ContextDir: buildContextTmpDir}); err != nil { + if err := drv.Add(ctx, containerName, i.Src, i.Dst, buildah.AddOpts{ + CommonOpts: drvOpts, + ContextDir: contextDir, + Chown: i.Chown, + Chmod: i.Chmod, + }); err != nil { return fmt.Errorf("error adding %v to %s for container %s: %w", i.Src, i.Dst, containerName, err) } + return nil } diff --git a/pkg/container_backend/instruction/cmd.go b/pkg/container_backend/instruction/cmd.go index 517f1dc6f1..019a7997e7 100644 --- a/pkg/container_backend/instruction/cmd.go +++ b/pkg/container_backend/instruction/cmd.go @@ -22,8 +22,13 @@ func (i *Cmd) UsesBuildContext() bool { } func (i *Cmd) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { - if err := drv.Config(ctx, containerName, buildah.ConfigOpts{CommonOpts: drvOpts, Cmd: i.Cmd.Cmd}); err != nil { + if err := drv.Config(ctx, containerName, buildah.ConfigOpts{ + CommonOpts: drvOpts, + Cmd: i.Cmd.Cmd, + CmdPrependShell: i.PrependShell, + }); err != nil { return fmt.Errorf("error setting cmd %v for container %s: %w", i.Cmd, containerName, err) } + return nil } diff --git a/pkg/container_backend/instruction/copy.go b/pkg/container_backend/instruction/copy.go index 83e3dc8dee..d49563e3be 100644 --- a/pkg/container_backend/instruction/copy.go +++ b/pkg/container_backend/instruction/copy.go @@ -18,17 +18,36 @@ func NewCopy(i dockerfile_instruction.Copy) *Copy { } func (i *Copy) UsesBuildContext() bool { - return true + return i.From == "" } func (i *Copy) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { - buildContextTmpDir, err := buildContextArchive.ExtractOrGetExtractedDir(ctx) - if err != nil { - return fmt.Errorf("unable to extract build context: %w", err) + var contextDir string + if i.UsesBuildContext() { + var err error + contextDir, err = buildContextArchive.ExtractOrGetExtractedDir(ctx) + if err != nil { + return fmt.Errorf("unable to extract build context: %w", err) + } + } else { + container, err := drv.FromCommand(ctx, "", i.From, buildah.FromCommandOpts{}) + if err != nil { + return fmt.Errorf("unable to create container from image %q: %w", i.From, err) + } + + contextDir, err = drv.Mount(ctx, container, buildah.MountOpts{}) + if err != nil { + return fmt.Errorf("unable to mount container %q: %w", container, err) + } } - if err := drv.Copy(ctx, containerName, buildContextTmpDir, i.Src, i.Dst, buildah.CopyOpts{CommonOpts: drvOpts, From: i.From}); err != nil { + if err := drv.Copy(ctx, containerName, contextDir, i.Src, i.Dst, buildah.CopyOpts{ + CommonOpts: drvOpts, + Chown: i.Chown, + Chmod: i.Chmod, + }); err != nil { return fmt.Errorf("error copying %v to %s for container %s: %w", i.Src, i.Dst, containerName, err) } + return nil } diff --git a/pkg/container_backend/instruction/entrypoint.go b/pkg/container_backend/instruction/entrypoint.go index 13f673fa1e..4e7ab11ecc 100644 --- a/pkg/container_backend/instruction/entrypoint.go +++ b/pkg/container_backend/instruction/entrypoint.go @@ -22,8 +22,13 @@ func (i *Entrypoint) UsesBuildContext() bool { } func (i *Entrypoint) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { - if err := drv.Config(ctx, containerName, buildah.ConfigOpts{CommonOpts: drvOpts, Entrypoint: i.Entrypoint.Entrypoint}); err != nil { + if err := drv.Config(ctx, containerName, buildah.ConfigOpts{ + CommonOpts: drvOpts, + Entrypoint: i.Entrypoint.Entrypoint, + EntrypointPrependShell: i.PrependShell, + }); err != nil { return fmt.Errorf("error setting entrypoint %v for container %s: %w", i.Entrypoint, containerName, err) } + return nil } diff --git a/pkg/container_backend/instruction/healthcheck.go b/pkg/container_backend/instruction/healthcheck.go index 0898dbe0c7..523efc2f3c 100644 --- a/pkg/container_backend/instruction/healthcheck.go +++ b/pkg/container_backend/instruction/healthcheck.go @@ -1,6 +1,12 @@ package instruction import ( + "context" + "fmt" + + "github.com/werf/werf/pkg/buildah" + "github.com/werf/werf/pkg/buildah/thirdparty" + "github.com/werf/werf/pkg/container_backend" dockerfile_instruction "github.com/werf/werf/pkg/dockerfile/instruction" ) @@ -8,4 +14,21 @@ type Healthcheck struct { dockerfile_instruction.Healthcheck } -// TODO +func NewHealthcheck(i dockerfile_instruction.Healthcheck) *Healthcheck { + return &Healthcheck{Healthcheck: i} +} + +func (i *Healthcheck) UsesBuildContext() bool { + return false +} + +func (i *Healthcheck) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { + if err := drv.Config(ctx, containerName, buildah.ConfigOpts{ + CommonOpts: drvOpts, + Healthcheck: (*thirdparty.HealthConfig)(i.Config), + }); err != nil { + return fmt.Errorf("error setting healthcheck for container %s: %w", containerName, err) + } + + return nil +} diff --git a/pkg/container_backend/instruction/maintainer.go b/pkg/container_backend/instruction/maintainer.go new file mode 100644 index 0000000000..feb3d048c5 --- /dev/null +++ b/pkg/container_backend/instruction/maintainer.go @@ -0,0 +1,33 @@ +package instruction + +import ( + "context" + "fmt" + + "github.com/werf/werf/pkg/buildah" + "github.com/werf/werf/pkg/container_backend" + dockerfile_instruction "github.com/werf/werf/pkg/dockerfile/instruction" +) + +type Maintainer struct { + dockerfile_instruction.Maintainer +} + +func NewMaintainer(i dockerfile_instruction.Maintainer) *Maintainer { + return &Maintainer{Maintainer: i} +} + +func (i *Maintainer) UsesBuildContext() bool { + return false +} + +func (i *Maintainer) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { + if err := drv.Config(ctx, containerName, buildah.ConfigOpts{ + CommonOpts: drvOpts, + Maintainer: i.Maintainer.Maintainer, + }); err != nil { + return fmt.Errorf("error setting maintainer for container %s: %w", containerName, err) + } + + return nil +} diff --git a/pkg/container_backend/instruction/run.go b/pkg/container_backend/instruction/run.go index 16ea2f9887..9e5fb00f5f 100644 --- a/pkg/container_backend/instruction/run.go +++ b/pkg/container_backend/instruction/run.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/werf/werf/pkg/buildah" "github.com/werf/werf/pkg/container_backend" dockerfile_instruction "github.com/werf/werf/pkg/dockerfile/instruction" @@ -18,14 +20,39 @@ func NewRun(i dockerfile_instruction.Run) *Run { } func (i *Run) UsesBuildContext() bool { + for _, mount := range i.Mounts { + if mount.Type == instructions.MountTypeBind && mount.From == "" { + return true + } + } + return false } func (i *Run) Apply(ctx context.Context, containerName string, drv buildah.Buildah, drvOpts buildah.CommonOpts, buildContextArchive container_backend.BuildContextArchiver) error { + var contextDir string + if i.UsesBuildContext() { + var err error + contextDir, err = buildContextArchive.ExtractOrGetExtractedDir(ctx) + if err != nil { + return fmt.Errorf("unable to extract build context: %w", err) + } + } + + var addCapabilities []string + if i.Security == dockerfile_instruction.SecurityInsecure { + addCapabilities = []string{"all"} + } + if err := drv.RunCommand(ctx, containerName, i.Command, buildah.RunCommandOpts{ - CommonOpts: drvOpts, + CommonOpts: drvOpts, + ContextDir: contextDir, + PrependShell: i.PrependShell, + AddCapabilities: addCapabilities, + NetworkType: i.Network, }); err != nil { return fmt.Errorf("error running command %v for container %s: %w", i.Command, containerName, err) } + return nil } diff --git a/pkg/dockerfile/frontend/buildkit_dockerfile.go b/pkg/dockerfile/frontend/buildkit_dockerfile.go index f6cd270d52..ca99eaf17b 100644 --- a/pkg/dockerfile/frontend/buildkit_dockerfile.go +++ b/pkg/dockerfile/frontend/buildkit_dockerfile.go @@ -101,8 +101,19 @@ func extractSrcAndDst(sourcesAndDest instructions.SourcesAndDest) ([]string, str if len(sourcesAndDest) < 2 { panic(fmt.Sprintf("unexpected buildkit instruction source and destination: %#v", sourcesAndDest)) } - dst := sourcesAndDest[len(sourcesAndDest)-1] - src := sourcesAndDest[0 : len(sourcesAndDest)-1] + + // TODO: somehow parse.Parse() and instructions.Parse() do not unquote at least sources, + // maybe that will be fixed in later versions of buildkit? Ref: + // /home/user1/go/pkg/mod/github.com/moby/buildkit@v0.8.2/frontend/dockerfile/instructions/parse.go:143 + // /home/user1/go/pkg/mod/github.com/moby/buildkit@v0.8.2/frontend/dockerfile/parser/parser.go:250 + var src []string + for _, s := range sourcesAndDest[0 : len(sourcesAndDest)-1] { + s, _ = strconv.Unquote(s) + src = append(src, s) + } + + dst, _ := strconv.Unquote(sourcesAndDest[len(sourcesAndDest)-1]) + return src, dst } diff --git a/pkg/dockerfile/instruction/healthcheck.go b/pkg/dockerfile/instruction/healthcheck.go index cc765778ac..29e591cde3 100644 --- a/pkg/dockerfile/instruction/healthcheck.go +++ b/pkg/dockerfile/instruction/healthcheck.go @@ -3,6 +3,7 @@ package instruction import "github.com/docker/docker/api/types/container" type Healthcheck struct { + // TODO(ilya-lesikov): isn't this should be a part of a Config.Test? Type HealthcheckType Config *container.HealthConfig } diff --git a/test/e2e/build/common_test.go b/test/e2e/build/common_test.go index 8cb0a73fe9..9bd0a06650 100644 --- a/test/e2e/build/common_test.go +++ b/test/e2e/build/common_test.go @@ -4,22 +4,34 @@ import ( "strings" ) -func setupEnv(withLocalRepo bool, buildahMode string) { - SuiteData.Stubs.SetEnv("WERF_BUILDAH_MODE", buildahMode) +type setupEnvOptions struct { + BuildahMode string + WithLocalRepo bool + WithForceStagedDockerfile bool +} + +func setupEnv(opts setupEnvOptions) { + SuiteData.Stubs.SetEnv("WERF_BUILDAH_MODE", opts.BuildahMode) - if withLocalRepo && buildahMode == "docker" { + if opts.WithLocalRepo && opts.BuildahMode == "docker" { SuiteData.Stubs.SetEnv("WERF_REPO", strings.Join([]string{SuiteData.RegistryLocalAddress, SuiteData.ProjectName}, "/")) - } else if withLocalRepo { + } else if opts.WithLocalRepo { SuiteData.Stubs.SetEnv("WERF_REPO", strings.Join([]string{SuiteData.RegistryInternalAddress, SuiteData.ProjectName}, "/")) } else { SuiteData.Stubs.UnsetEnv("WERF_REPO") } - if withLocalRepo { + if opts.WithLocalRepo { SuiteData.Stubs.SetEnv("WERF_INSECURE_REGISTRY", "1") SuiteData.Stubs.SetEnv("WERF_SKIP_TLS_VERIFY_REGISTRY", "1") } else { SuiteData.Stubs.UnsetEnv("WERF_INSECURE_REGISTRY") SuiteData.Stubs.UnsetEnv("WERF_SKIP_TLS_VERIFY_REGISTRY") } + + if opts.WithForceStagedDockerfile { + SuiteData.Stubs.SetEnv("WERF_FORCE_STAGED_DOCKERFILE", "1") + } else { + SuiteData.Stubs.UnsetEnv("WERF_FORCE_STAGED_DOCKERFILE") + } } diff --git a/test/e2e/build/complex_test.go b/test/e2e/build/complex_test.go index 2194ffa006..c39cefe726 100644 --- a/test/e2e/build/complex_test.go +++ b/test/e2e/build/complex_test.go @@ -9,12 +9,22 @@ import ( "github.com/werf/werf/test/pkg/werf" ) +type complexTestOptions struct { + BuildahMode string + WithLocalRepo bool + WithStagedDockerfileBuilder bool +} + var _ = Describe("Complex build", Label("e2e", "build", "complex"), func() { DescribeTable("should succeed and produce expected image", - func(withLocalRepo bool, buildahMode string) { + func(testOpts complexTestOptions) { By("initializing") - setupEnv(withLocalRepo, buildahMode) - contRuntime, err := contback.NewContainerBackend(buildahMode) + setupEnv(setupEnvOptions{ + BuildahMode: testOpts.BuildahMode, + WithLocalRepo: testOpts.WithLocalRepo, + WithForceStagedDockerfile: testOpts.WithStagedDockerfileBuilder, + }) + contRuntime, err := contback.NewContainerBackend(testOpts.BuildahMode) if err == contback.ErrRuntimeUnavailable { Skip(err.Error()) } else if err != nil { @@ -219,9 +229,39 @@ var _ = Describe("Complex build", Label("e2e", "build", "complex"), func() { ) } }, - Entry("without repo using Docker", false, "docker"), - Entry("with local repo using Docker", true, "docker"), - Entry("with local repo using Native Buildah with rootless isolation", true, "native-rootless"), - Entry("with local repo using Native Buildah with chroot isolation", true, "native-chroot"), + Entry("without repo using Docker", complexTestOptions{ + BuildahMode: "docker", + WithLocalRepo: false, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Docker", complexTestOptions{ + BuildahMode: "docker", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Native Buildah with rootless isolation", complexTestOptions{ + BuildahMode: "native-rootless", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Native Buildah with chroot isolation", complexTestOptions{ + BuildahMode: "native-chroot", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + // TODO(ilya-lesikov): uncomment after Staged Dockerfile builder finished + // // TODO(1.3): after Full Dockerfile Builder removed and Staged Dockerfile Builder enabled by default this test no longer needed + // Entry("with local repo using Native Buildah and Staged Dockerfile builder with rootless isolation", complexTestOptions{ + // BuildahMode: "native-rootless", + // WithLocalRepo: true, + // WithStagedDockerfileBuilder: true, + // }), + // TODO(ilya-lesikov): uncomment after Staged Dockerfile builder finished + // // TODO(1.3): after Full Dockerfile Builder removed and Staged Dockerfile Builder enabled by default this test no longer needed + // Entry("with local repo using Native Buildah and Staged Dockerfile builder with chroot isolation", complexTestOptions{ + // BuildahMode: "native-chroot", + // WithLocalRepo: true, + // WithStagedDockerfileBuilder: true, + // }), ) }) diff --git a/test/e2e/build/simple_test.go b/test/e2e/build/simple_test.go index e0c7398abe..f274ace9bb 100644 --- a/test/e2e/build/simple_test.go +++ b/test/e2e/build/simple_test.go @@ -8,12 +8,22 @@ import ( "github.com/werf/werf/test/pkg/werf" ) +type simpleTestOptions struct { + BuildahMode string + WithLocalRepo bool + WithStagedDockerfileBuilder bool +} + var _ = Describe("Simple build", Label("e2e", "build", "simple"), func() { DescribeTable("should succeed and produce expected image", - func(withLocalRepo bool, buildahMode string) { + func(testOpts simpleTestOptions) { By("initializing") - setupEnv(withLocalRepo, buildahMode) - contRuntime, err := contback.NewContainerBackend(buildahMode) + setupEnv(setupEnvOptions{ + BuildahMode: testOpts.BuildahMode, + WithLocalRepo: testOpts.WithLocalRepo, + WithForceStagedDockerfile: testOpts.WithStagedDockerfileBuilder, + }) + contRuntime, err := contback.NewContainerBackend(testOpts.BuildahMode) if err == contback.ErrRuntimeUnavailable { Skip(err.Error()) } else if err != nil { @@ -61,9 +71,39 @@ var _ = Describe("Simple build", Label("e2e", "build", "simple"), func() { ) } }, - Entry("without repo using Docker", false, "docker"), - Entry("with local repo using Docker", true, "docker"), - Entry("with local repo using Native Buildah with rootless isolation", true, "native-rootless"), - Entry("with local repo using Native Buildah with chroot isolation", true, "native-chroot"), + Entry("without repo using Docker", simpleTestOptions{ + BuildahMode: "docker", + WithLocalRepo: false, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Docker", simpleTestOptions{ + BuildahMode: "docker", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Native Buildah with rootless isolation", simpleTestOptions{ + BuildahMode: "native-rootless", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + Entry("with local repo using Native Buildah with chroot isolation", simpleTestOptions{ + BuildahMode: "native-chroot", + WithLocalRepo: true, + WithStagedDockerfileBuilder: false, + }), + // TODO(ilya-lesikov): uncomment after Staged Dockerfile builder finished + // // TODO(1.3): after Full Dockerfile Builder removed and Staged Dockerfile Builder enabled by default this test no longer needed + // Entry("with local repo using Native Buildah and Staged Dockerfile Builder with rootless isolation", simpleTestOptions{ + // BuildahMode: "native-rootless", + // WithLocalRepo: true, + // WithStagedDockerfileBuilder: true, + // }), + // TODO(ilya-lesikov): uncomment after Staged Dockerfile builder finished + // // TODO(1.3): after Full Dockerfile Builder removed and Staged Dockerfile Builder enabled by default this test no longer needed + // Entry("with local repo using Native Buildah and Staged Dockerfile Builder with chroot isolation", simpleTestOptions{ + // BuildahMode: "native-chroot", + // WithLocalRepo: true, + // WithStagedDockerfileBuilder: true, + // }), ) })