From a4c36c7d26f823fd9619c7736248dcf4f8180c29 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sat, 26 Apr 2025 19:09:42 +0200 Subject: [PATCH 01/22] :tada: Build images with buildah Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 96 +++++++++++++++++++++++++++++ pkg/kbld/config/config.go | 1 + pkg/kbld/config/config_buildah.go | 18 ++++++ pkg/kbld/image/built.go | 15 ++++- pkg/kbld/image/factory.go | 4 +- 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 pkg/kbld/builder/buildah/buildah.go create mode 100644 pkg/kbld/config/config_buildah.go diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go new file mode 100644 index 00000000..865e7039 --- /dev/null +++ b/pkg/kbld/builder/buildah/buildah.go @@ -0,0 +1,96 @@ +package buildah + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + + ctlconf "carvel.dev/kbld/pkg/kbld/config" + ctllog "carvel.dev/kbld/pkg/kbld/logger" +) + +type Buildah struct { + logger ctllog.Logger +} + +func New(logger ctllog.Logger) Buildah { + return Buildah{logger} +} + +func ensureDirectory(directory string) error { + stat, err := os.Stat(directory) + if err != nil { + return fmt.Errorf("Checking if path '%s' is a directory: %s", directory, err) + } + + // Provide explicit directory check error message because otherwise docker CLI + // outputs confusing msg 'error: fork/exec /usr/local/bin/docker: not a directory' + if !stat.IsDir() { + return fmt.Errorf("Expected path '%s' to be a directory, but was not", directory) + } + + return nil +} + +func Launch(directory string, command string, cmdArgs []string, prefixedLogger *ctllog.PrefixWriter) error { + var stdoutBuf, stderrBuf bytes.Buffer + cmd := exec.Command("buildah", cmdArgs...) + cmd.Dir = directory + cmd.Stdout = io.MultiWriter(&stdoutBuf, prefixedLogger) + cmd.Stderr = io.MultiWriter(&stderrBuf, prefixedLogger) + + err := cmd.Run() + if err != nil { + prefixedLogger.Write([]byte(fmt.Sprintf("error: %s\n", err))) + return err + } + return nil +} + +func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { + + err := ensureDirectory(directory) + if err != nil { + return "", err + } + + tagRef := imgDst.NewImage + + prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ") + prefixedLogger.Write([]byte(fmt.Sprintf("Start building using buildah\n"))) + + cmdArgs := []string{"build", "--tag", tagRef} + + if opts.Pull { + cmdArgs = append(cmdArgs, "--pull") + } + if opts.File != nil { + cmdArgs = append(cmdArgs, "--file="+*opts.File) + } + if opts.Target != nil { + cmdArgs = append(cmdArgs, "--target="+*opts.Target) + } + // Use current directory as context + // cmdArgs = append(cmdArgs, "./") + + build_err := Launch(directory, "buildah", cmdArgs, prefixedLogger) + if build_err != nil { + return "", build_err + } + push_err := b.PushImage(image, tagRef) + if push_err != nil { + return "", nil + } + return tagRef, nil +} + +func (b Buildah) PushImage(image, tagRef string) error { + prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") + push_err := Launch("", "buildah", []string{"push", tagRef}, prefixedLogger) + if push_err != nil { + return push_err + } + return nil +} diff --git a/pkg/kbld/config/config.go b/pkg/kbld/config/config.go index 3e4a4afd..634a5a47 100644 --- a/pkg/kbld/config/config.go +++ b/pkg/kbld/config/config.go @@ -61,6 +61,7 @@ type Source struct { KubectlBuildkit *SourceKubectlBuildkitOpts Ko *SourceKoOpts Bazel *SourceBazelOpts + Buildah *SourceBuildahOpts } type ImageOverride struct { diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go new file mode 100644 index 00000000..b80b8cf3 --- /dev/null +++ b/pkg/kbld/config/config_buildah.go @@ -0,0 +1,18 @@ +package config + +// Options for builds using Containerfiles +// +// see https://github.com/containers/common/blob/main/docs/Containerfile.5.md +type ContainerFileOpts struct { + // Always pull images + Pull bool + // File containing instructions + // Docker will use "Dockerfile" as default + // Buildah can detect "Containerfile" or "Dockerfile" as default + File *string + Target *string +} + +type SourceBuildahOpts struct { + ContainerFileOpts +} diff --git a/pkg/kbld/image/built.go b/pkg/kbld/image/built.go index c4dc3fb8..e5e7bd24 100644 --- a/pkg/kbld/image/built.go +++ b/pkg/kbld/image/built.go @@ -4,9 +4,11 @@ package image import ( + "fmt" "path/filepath" ctlbbz "carvel.dev/kbld/pkg/kbld/builder/bazel" + ctlbah "carvel.dev/kbld/pkg/kbld/builder/buildah" ctlbdk "carvel.dev/kbld/pkg/kbld/builder/docker" ctlbko "carvel.dev/kbld/pkg/kbld/builder/ko" ctlbkb "carvel.dev/kbld/pkg/kbld/builder/kubectlbuildkit" @@ -25,13 +27,14 @@ type BuiltImage struct { kubectlBuildkit ctlbkb.KubectlBuildkit ko ctlbko.Ko bazel ctlbbz.Bazel + buildah ctlbah.Buildah } func NewBuiltImage(url string, buildSource ctlconf.Source, imgDst *ctlconf.ImageDestination, docker ctlbdk.Docker, dockerBuildx ctlbdk.Buildx, pack ctlbpk.Pack, - kubectlBuildkit ctlbkb.KubectlBuildkit, ko ctlbko.Ko, bazel ctlbbz.Bazel) BuiltImage { + kubectlBuildkit ctlbkb.KubectlBuildkit, ko ctlbko.Ko, bazel ctlbbz.Bazel, buildah ctlbah.Buildah) BuiltImage { - return BuiltImage{url, buildSource, imgDst, docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel} + return BuiltImage{url, buildSource, imgDst, docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel, buildah} } func (i BuiltImage) URL() (string, []ctlconf.Origin, error) { @@ -84,6 +87,14 @@ func (i BuiltImage) URL() (string, []ctlconf.Origin, error) { urlRepo, i.buildSource.Path, i.imgDst, *i.buildSource.Docker.Buildx) return url, origins, err + case i.buildSource.Buildah != nil: + if i.imgDst == nil { + return "", nil, fmt.Errorf("Image destination is mandatory for buildah.") + } + + tag, err := i.buildah.BuildAndPushImage(urlRepo, i.buildSource.Path, *i.imgDst, *i.buildSource.Buildah) + return tag, origins, err + // Fall back on Docker by default default: if i.buildSource.Docker == nil { diff --git a/pkg/kbld/image/factory.go b/pkg/kbld/image/factory.go index 8a708948..3d441696 100644 --- a/pkg/kbld/image/factory.go +++ b/pkg/kbld/image/factory.go @@ -7,6 +7,7 @@ import ( "fmt" ctlbbz "carvel.dev/kbld/pkg/kbld/builder/bazel" + ctlbah "carvel.dev/kbld/pkg/kbld/builder/buildah" ctlbdk "carvel.dev/kbld/pkg/kbld/builder/docker" ctlbko "carvel.dev/kbld/pkg/kbld/builder/ko" ctlbkb "carvel.dev/kbld/pkg/kbld/builder/kubectlbuildkit" @@ -72,9 +73,10 @@ func (f Factory) New(url string) Image { kubectlBuildkit := ctlbkb.NewKubectlBuildkit(f.logger) ko := ctlbko.NewKo(f.logger) bazel := ctlbbz.NewBazel(docker, f.logger) + buildah := ctlbah.New(f.logger) var builtImg Image = NewBuiltImage(url, srcConf, imgDstConf, - docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel) + docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel, buildah) if imgDstConf != nil { builtImg = NewTaggedImage(builtImg, *imgDstConf, f.registry) From 308dcbae0ba968e862e49478ab3761e44191cd2e Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sat, 26 Apr 2025 21:30:19 +0200 Subject: [PATCH 02/22] buildah : raw options Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 3 +++ pkg/kbld/config/config_buildah.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 865e7039..46702485 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -72,6 +72,9 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image if opts.Target != nil { cmdArgs = append(cmdArgs, "--target="+*opts.Target) } + if opts.RawOptions != nil { + cmdArgs = append(cmdArgs, *opts.RawOptions...) + } // Use current directory as context // cmdArgs = append(cmdArgs, "./") diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index b80b8cf3..3b0eeb5a 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -15,4 +15,6 @@ type ContainerFileOpts struct { type SourceBuildahOpts struct { ContainerFileOpts + // More options + RawOptions *[]string `json:"rawOptions"` } From 93710e5faec612ed0ecae57461ab334a3c743e6b Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sat, 26 Apr 2025 21:50:23 +0200 Subject: [PATCH 03/22] buildah : platforms Build for multiple platforms. Use a manifest in all situations because buildah can not create a manifest when a tag already exists. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 8 ++++++-- pkg/kbld/config/config_buildah.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 46702485..7a761548 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -61,7 +61,7 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ") prefixedLogger.Write([]byte(fmt.Sprintf("Start building using buildah\n"))) - cmdArgs := []string{"build", "--tag", tagRef} + cmdArgs := []string{"build", "--manifest=" + tagRef} if opts.Pull { cmdArgs = append(cmdArgs, "--pull") @@ -72,6 +72,10 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image if opts.Target != nil { cmdArgs = append(cmdArgs, "--target="+*opts.Target) } + if len(opts.Platforms) > 0 { + cmdArgs = append(cmdArgs, "--platform="+strings.Join(opts.Platforms, ",")) + } + if opts.RawOptions != nil { cmdArgs = append(cmdArgs, *opts.RawOptions...) } @@ -91,7 +95,7 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image func (b Buildah) PushImage(image, tagRef string) error { prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") - push_err := Launch("", "buildah", []string{"push", tagRef}, prefixedLogger) + push_err := Launch("", "buildah", []string{"manifest", "push", "--all", tagRef}, prefixedLogger) if push_err != nil { return push_err } diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index 3b0eeb5a..5d0f7a59 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -11,6 +11,8 @@ type ContainerFileOpts struct { // Buildah can detect "Containerfile" or "Dockerfile" as default File *string Target *string + // + Platforms []string } type SourceBuildahOpts struct { From fde3ab7bbe479d5fb3e0ded4466f4cb594052a60 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sat, 26 Apr 2025 21:52:00 +0200 Subject: [PATCH 04/22] buildah : options build-arg Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 3 +++ pkg/kbld/config/config_buildah.go | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 7a761548..77405c6d 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -69,6 +69,9 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image if opts.File != nil { cmdArgs = append(cmdArgs, "--file="+*opts.File) } + for arg, value := range opts.BuildArgs { + cmdArgs = append(cmdArgs, "--build-arg="+arg+"="+value) + } if opts.Target != nil { cmdArgs = append(cmdArgs, "--target="+*opts.Target) } diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index 5d0f7a59..13ebf59c 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -9,7 +9,10 @@ type ContainerFileOpts struct { // File containing instructions // Docker will use "Dockerfile" as default // Buildah can detect "Containerfile" or "Dockerfile" as default - File *string + File *string + // Option "--build-arg=K=V" + BuildArgs map[string]string `json:"buildArgs"` + // Target *string // Platforms []string From 37746f10a85e166fd5577a08f4ca6652874099b1 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sun, 27 Apr 2025 14:50:56 +0200 Subject: [PATCH 05/22] buildah : publish with all tags Manifests for buildah and podman have no digest. Buildah pushs the manifest to all tags and TaggedImage must ignore the incomplete URL. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 77 ++++++++++++++++++----------- pkg/kbld/image/built.go | 7 +-- pkg/kbld/image/tagged.go | 5 +- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 77405c6d..70648fdd 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -34,34 +34,17 @@ func ensureDirectory(directory string) error { return nil } -func Launch(directory string, command string, cmdArgs []string, prefixedLogger *ctllog.PrefixWriter) error { - var stdoutBuf, stderrBuf bytes.Buffer - cmd := exec.Command("buildah", cmdArgs...) - cmd.Dir = directory - cmd.Stdout = io.MultiWriter(&stdoutBuf, prefixedLogger) - cmd.Stderr = io.MultiWriter(&stderrBuf, prefixedLogger) - - err := cmd.Run() - if err != nil { - prefixedLogger.Write([]byte(fmt.Sprintf("error: %s\n", err))) - return err - } - return nil -} - -func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { +func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { err := ensureDirectory(directory) if err != nil { return "", err } - tagRef := imgDst.NewImage - prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ") prefixedLogger.Write([]byte(fmt.Sprintf("Start building using buildah\n"))) - cmdArgs := []string{"build", "--manifest=" + tagRef} + cmdArgs := []string{"build", "--manifest=" + image} if opts.Pull { cmdArgs = append(cmdArgs, "--pull") @@ -85,22 +68,58 @@ func (b Buildah) BuildAndPushImage(image, directory string, imgDst ctlconf.Image // Use current directory as context // cmdArgs = append(cmdArgs, "./") - build_err := Launch(directory, "buildah", cmdArgs, prefixedLogger) - if build_err != nil { - return "", build_err + { + cmd := exec.Command("buildah", cmdArgs...) + cmd.Dir = directory + cmd.Stdout = prefixedLogger + + err := cmd.Run() + if err != nil { + prefixedLogger.Write([]byte(fmt.Sprintf("error: %s\n", err))) + return "", err + } } - push_err := b.PushImage(image, tagRef) + remoteRef, push_err := b.PushImage(image, imgDst) if push_err != nil { - return "", nil + return "", push_err } - return tagRef, nil + prefixedLogger.WriteStr("Image build : " + remoteRef) + return remoteRef, nil } -func (b Buildah) PushImage(image, tagRef string) error { - prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") - push_err := Launch("", "buildah", []string{"manifest", "push", "--all", tagRef}, prefixedLogger) +func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) error { + pushCommand := exec.Command("buildah", "manifest", "push", "--all", src, "docker://"+dest) + pushCommand.Stdout = log + push_err := pushCommand.Run() if push_err != nil { return push_err } return nil -} +} //// BuildahPush + +// Push built image to a remote registry +// Return one of the remote image address +func (b Buildah) PushImage(image string, imgDst *ctlconf.ImageDestination) (string, error) { + prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") + if imgDst == nil { + push_err := BuildahPush(image, image, prefixedLogger) + if push_err != nil { + return "", push_err + } + return image, nil + } else if len(imgDst.Tags) > 0 { + for _, tag := range imgDst.Tags { + push_err := BuildahPush(image, imgDst.NewImage+":"+tag, prefixedLogger) + if push_err != nil { + return "", push_err + } + } + return imgDst.NewImage + ":" + imgDst.Tags[0], nil + } else { + push_err := BuildahPush(image, imgDst.NewImage+":kbld", prefixedLogger) + if push_err != nil { + return "", push_err + } + return imgDst.NewImage + ":kbld", nil + } +} //// PushImage diff --git a/pkg/kbld/image/built.go b/pkg/kbld/image/built.go index e5e7bd24..b6936d8a 100644 --- a/pkg/kbld/image/built.go +++ b/pkg/kbld/image/built.go @@ -4,7 +4,6 @@ package image import ( - "fmt" "path/filepath" ctlbbz "carvel.dev/kbld/pkg/kbld/builder/bazel" @@ -88,11 +87,7 @@ func (i BuiltImage) URL() (string, []ctlconf.Origin, error) { return url, origins, err case i.buildSource.Buildah != nil: - if i.imgDst == nil { - return "", nil, fmt.Errorf("Image destination is mandatory for buildah.") - } - - tag, err := i.buildah.BuildAndPushImage(urlRepo, i.buildSource.Path, *i.imgDst, *i.buildSource.Buildah) + tag, err := i.buildah.BuildAndPushImage(urlRepo, i.buildSource.Path, i.imgDst, *i.buildSource.Buildah) return tag, origins, err // Fall back on Docker by default diff --git a/pkg/kbld/image/tagged.go b/pkg/kbld/image/tagged.go index 560a87b8..ce91b2cd 100644 --- a/pkg/kbld/image/tagged.go +++ b/pkg/kbld/image/tagged.go @@ -4,6 +4,8 @@ package image import ( + "strings" + ctlconf "carvel.dev/kbld/pkg/kbld/config" ctlreg "carvel.dev/kbld/pkg/kbld/registry" regname "github.com/google/go-containerregistry/pkg/name" @@ -26,7 +28,8 @@ func (i TaggedImage) URL() (string, []ctlconf.Origin, error) { return "", nil, err } - if len(i.imgDst.Tags) > 0 { + if len(i.imgDst.Tags) > 0 && strings.Contains(url, "@") { + // Configure new tags only if a digest is known dstRef, err := regname.NewDigest(url, regname.WeakValidation) if err != nil { return "", nil, err From 89cee0b3895af9c78509a5dc046bff30317112b9 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Tue, 29 Apr 2025 20:53:32 +0200 Subject: [PATCH 06/22] buildah : get the manifest digest with --digestfile The manifest digest is written to a temporary file with the option --digestfile. Push only to a random tag and let kbld create the other tags. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 65 ++++++++++++++++++----------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 70648fdd..e15c2222 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -1,12 +1,12 @@ package buildah import ( - "bytes" "fmt" - "io" "os" "os/exec" + "strings" + ctlb "carvel.dev/kbld/pkg/kbld/builder" ctlconf "carvel.dev/kbld/pkg/kbld/config" ctllog "carvel.dev/kbld/pkg/kbld/logger" ) @@ -87,39 +87,56 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco return remoteRef, nil } -func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) error { - pushCommand := exec.Command("buildah", "manifest", "push", "--all", src, "docker://"+dest) +// Push the buildah manifest and return the digest +func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, error) { + digest_file, digest_err := os.CreateTemp("", "buildah-") + if digest_err != nil { + return "", fmt.Errorf("cannot create digest file: %w", digest_err) + } + defer func() { + if err := digest_file.Close(); err != nil { + fmt.Printf("ERROR: Closing temp file %q: %v", digest_file.Name(), err) + } + if err := os.Remove(digest_file.Name()); err != nil { + fmt.Printf("ERROR: Removing temp file %q: %v", digest_file.Name(), err) + } + }() + + // !!! with --digestfile, buildah will not return an error if an authentication is required. + pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digest_file.Name(), src, "docker://"+dest) pushCommand.Stdout = log push_err := pushCommand.Run() if push_err != nil { - return push_err + return "", fmt.Errorf("error pushing to %q (check if you are authenticated) : %w", dest, push_err) } - return nil + + digest := make([]byte, 64+7) + digest_len, read_err := digest_file.Read(digest) + if read_err != nil { + return "", fmt.Errorf("cannot read digest in file %q (check if you are authenticated) : %w", digest_file.Name(), read_err) + } + return string(digest[0:digest_len]), nil } //// BuildahPush // Push built image to a remote registry -// Return one of the remote image address +// Return the image URL with digest func (b Buildah) PushImage(image string, imgDst *ctlconf.ImageDestination) (string, error) { prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") + var remoteImg string if imgDst == nil { - push_err := BuildahPush(image, image, prefixedLogger) - if push_err != nil { - return "", push_err - } - return image, nil - } else if len(imgDst.Tags) > 0 { - for _, tag := range imgDst.Tags { - push_err := BuildahPush(image, imgDst.NewImage+":"+tag, prefixedLogger) - if push_err != nil { - return "", push_err - } - } - return imgDst.NewImage + ":" + imgDst.Tags[0], nil + remoteImg = image } else { - push_err := BuildahPush(image, imgDst.NewImage+":kbld", prefixedLogger) - if push_err != nil { - return "", push_err + tb := ctlb.TagBuilder{} + randSuffix, err := tb.RandomStr50() + if err != nil { + return "", fmt.Errorf("generating image dst suffix: %s", err) } - return imgDst.NewImage + ":kbld", nil + remoteImg = imgDst.NewImage + ":kbld-" + randSuffix + } + + digest, push_err := BuildahPush(image, remoteImg, prefixedLogger) + if push_err != nil { + return "", push_err } + return remoteImg + "@" + digest, nil } //// PushImage From 50e71df041357d1ccf238770438182cfd0d4d7dc Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Tue, 29 Apr 2025 20:55:37 +0200 Subject: [PATCH 07/22] log : buildah commands Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index e15c2222..757de6d6 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -68,6 +68,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco // Use current directory as context // cmdArgs = append(cmdArgs, "./") + prefixedLogger.WriteStr("=> buildah " + strings.Join(cmdArgs, " ")) { cmd := exec.Command("buildah", cmdArgs...) cmd.Dir = directory @@ -103,6 +104,7 @@ func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, err }() // !!! with --digestfile, buildah will not return an error if an authentication is required. + log.WriteStr("=> buildah manifest push --all --digestfile=" + digest_file.Name() + " " + src + " docker://" + dest) pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digest_file.Name(), src, "docker://"+dest) pushCommand.Stdout = log push_err := pushCommand.Run() From 74c14b28e0ca54e780b9cc10d08a29956f43a2bc Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Fri, 2 May 2025 11:23:22 +0200 Subject: [PATCH 08/22] :recycle: Generate args in SourceBuildahOpts Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 16 +--------------- pkg/kbld/config/config_buildah.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 757de6d6..b1eca564 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -46,25 +46,11 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco cmdArgs := []string{"build", "--manifest=" + image} - if opts.Pull { - cmdArgs = append(cmdArgs, "--pull") - } if opts.File != nil { cmdArgs = append(cmdArgs, "--file="+*opts.File) } - for arg, value := range opts.BuildArgs { - cmdArgs = append(cmdArgs, "--build-arg="+arg+"="+value) - } - if opts.Target != nil { - cmdArgs = append(cmdArgs, "--target="+*opts.Target) - } - if len(opts.Platforms) > 0 { - cmdArgs = append(cmdArgs, "--platform="+strings.Join(opts.Platforms, ",")) - } + cmdArgs = append(cmdArgs, opts.Args()...) - if opts.RawOptions != nil { - cmdArgs = append(cmdArgs, *opts.RawOptions...) - } // Use current directory as context // cmdArgs = append(cmdArgs, "./") diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index 13ebf59c..d610022b 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -1,5 +1,7 @@ package config +import "strings" + // Options for builds using Containerfiles // // see https://github.com/containers/common/blob/main/docs/Containerfile.5.md @@ -23,3 +25,25 @@ type SourceBuildahOpts struct { // More options RawOptions *[]string `json:"rawOptions"` } + +func (opts SourceBuildahOpts) Args() []string { + args := []string{} + + if opts.Pull { + args = append(args, "--pull") + } + for arg, value := range opts.BuildArgs { + args = append(args, "--build-arg="+arg+"="+value) + } + if opts.Target != nil { + args = append(args, "--target="+*opts.Target) + } + if len(opts.Platforms) > 0 { + args = append(args, "--platform="+strings.Join(opts.Platforms, ",")) + } + + if opts.RawOptions != nil { + args = append(args, *opts.RawOptions...) + } + return args +} // SourceBuildahOpts.Args From b3408d6448f8c7c786eb8a79ce7d026ac01cc6d9 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Fri, 2 May 2025 11:25:48 +0200 Subject: [PATCH 09/22] lint : variable names Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index b1eca564..c258819f 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -66,9 +66,9 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco return "", err } } - remoteRef, push_err := b.PushImage(image, imgDst) - if push_err != nil { - return "", push_err + remoteRef, pushErr := b.PushImage(image, imgDst) + if pushErr != nil { + return "", pushErr } prefixedLogger.WriteStr("Image build : " + remoteRef) return remoteRef, nil @@ -76,34 +76,34 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco // Push the buildah manifest and return the digest func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, error) { - digest_file, digest_err := os.CreateTemp("", "buildah-") - if digest_err != nil { - return "", fmt.Errorf("cannot create digest file: %w", digest_err) + digestFile, digestErr := os.CreateTemp("", "buildah-") + if digestErr != nil { + return "", fmt.Errorf("cannot create digest file: %w", digestErr) } defer func() { - if err := digest_file.Close(); err != nil { - fmt.Printf("ERROR: Closing temp file %q: %v", digest_file.Name(), err) + if err := digestFile.Close(); err != nil { + fmt.Printf("ERROR: Closing temp file %q: %v", digestFile.Name(), err) } - if err := os.Remove(digest_file.Name()); err != nil { - fmt.Printf("ERROR: Removing temp file %q: %v", digest_file.Name(), err) + if err := os.Remove(digestFile.Name()); err != nil { + fmt.Printf("ERROR: Removing temp file %q: %v", digestFile.Name(), err) } }() // !!! with --digestfile, buildah will not return an error if an authentication is required. - log.WriteStr("=> buildah manifest push --all --digestfile=" + digest_file.Name() + " " + src + " docker://" + dest) - pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digest_file.Name(), src, "docker://"+dest) + log.WriteStr("=> buildah manifest push --all --digestfile=" + digestFile.Name() + " " + src + " docker://" + dest) + pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digestFile.Name(), src, "docker://"+dest) pushCommand.Stdout = log - push_err := pushCommand.Run() - if push_err != nil { - return "", fmt.Errorf("error pushing to %q (check if you are authenticated) : %w", dest, push_err) + pushErr := pushCommand.Run() + if pushErr != nil { + return "", fmt.Errorf("error pushing to %q (check if you are authenticated) : %w", dest, pushErr) } digest := make([]byte, 64+7) - digest_len, read_err := digest_file.Read(digest) - if read_err != nil { - return "", fmt.Errorf("cannot read digest in file %q (check if you are authenticated) : %w", digest_file.Name(), read_err) + digestLen, readErr := digestFile.Read(digest) + if readErr != nil { + return "", fmt.Errorf("cannot read digest in file %q (check if you are authenticated) : %w", digestFile.Name(), readErr) } - return string(digest[0:digest_len]), nil + return string(digest[0:digestLen]), nil } //// BuildahPush // Push built image to a remote registry @@ -122,9 +122,9 @@ func (b Buildah) PushImage(image string, imgDst *ctlconf.ImageDestination) (stri remoteImg = imgDst.NewImage + ":kbld-" + randSuffix } - digest, push_err := BuildahPush(image, remoteImg, prefixedLogger) - if push_err != nil { - return "", push_err + digest, pushErr := BuildahPush(image, remoteImg, prefixedLogger) + if pushErr != nil { + return "", pushErr } return remoteImg + "@" + digest, nil } //// PushImage From 5b578f1b7f0221a7c1457cbe6abed636bbc80aab Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Fri, 2 May 2025 11:28:03 +0200 Subject: [PATCH 10/22] lint : comments Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index c258819f..02ef46a1 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -104,7 +104,7 @@ func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, err return "", fmt.Errorf("cannot read digest in file %q (check if you are authenticated) : %w", digestFile.Name(), readErr) } return string(digest[0:digestLen]), nil -} //// BuildahPush +} // BuildahPush // Push built image to a remote registry // Return the image URL with digest @@ -127,4 +127,4 @@ func (b Buildah) PushImage(image string, imgDst *ctlconf.ImageDestination) (stri return "", pushErr } return remoteImg + "@" + digest, nil -} //// PushImage +} // PushImage From bdc5ff580e0f134746a8330d8a655810b2c7268c Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sun, 11 May 2025 21:29:24 +0200 Subject: [PATCH 11/22] log : buildah show errors with the Logger Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 02ef46a1..27fc6698 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -59,6 +59,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco cmd := exec.Command("buildah", cmdArgs...) cmd.Dir = directory cmd.Stdout = prefixedLogger + cmd.Stderr = prefixedLogger err := cmd.Run() if err != nil { @@ -93,6 +94,7 @@ func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, err log.WriteStr("=> buildah manifest push --all --digestfile=" + digestFile.Name() + " " + src + " docker://" + dest) pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digestFile.Name(), src, "docker://"+dest) pushCommand.Stdout = log + pushCommand.Stderr = log pushErr := pushCommand.Run() if pushErr != nil { return "", fmt.Errorf("error pushing to %q (check if you are authenticated) : %w", dest, pushErr) From dfe5ff9acafa5107ef3365c30b86963fb1af58ce Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Sun, 11 May 2025 22:10:36 +0200 Subject: [PATCH 12/22] buildah : local and remote image name from configuration id and destination Use the destination name for local storage if available. The destination name use a special random tag. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 62 ++++++++++++++++------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 27fc6698..087e22ad 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -34,7 +34,33 @@ func ensureDirectory(directory string) error { return nil } +// Generate a name to send the image to the server +func remoteImageName(imgDst ctlconf.ImageDestination) string { + if len(imgDst.Tags) == 0 { + tb := ctlb.TagBuilder{} + randSuffix, err := tb.RandomStr50() + if err != nil { + return imgDst.NewImage + ":kbld" + } + return imgDst.NewImage + ":kbld-" + randSuffix + } else { + return imgDst.NewImage + ":" + imgDst.Tags[0] + } +} + +// Generate a name to store the image in local +func localImageName(configImageName string, imgDest *ctlconf.ImageDestination) string { + if imgDest == nil { + return configImageName + } else { + return remoteImageName(*imgDest) + } +} + func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { + if imgDst == nil { + return "", fmt.Errorf("a destination is required to store the built image") + } err := ensureDirectory(directory) if err != nil { @@ -44,7 +70,8 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ") prefixedLogger.Write([]byte(fmt.Sprintf("Start building using buildah\n"))) - cmdArgs := []string{"build", "--manifest=" + image} + localName := localImageName(image, imgDst) + cmdArgs := []string{"build", "--manifest=" + localName} if opts.File != nil { cmdArgs = append(cmdArgs, "--file="+*opts.File) @@ -67,12 +94,16 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco return "", err } } - remoteRef, pushErr := b.PushImage(image, imgDst) + + pushLogger := b.logger.NewPrefixedWriter(image + " push | ") + remoteName := localName + digest, pushErr := BuildahPush(localName, remoteName, pushLogger) if pushErr != nil { return "", pushErr } - prefixedLogger.WriteStr("Image build : " + remoteRef) - return remoteRef, nil + remoteName = remoteName + "@" + digest + prefixedLogger.WriteStr("Image build : " + remoteName) + return remoteName, nil } // Push the buildah manifest and return the digest @@ -107,26 +138,3 @@ func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, err } return string(digest[0:digestLen]), nil } // BuildahPush - -// Push built image to a remote registry -// Return the image URL with digest -func (b Buildah) PushImage(image string, imgDst *ctlconf.ImageDestination) (string, error) { - prefixedLogger := b.logger.NewPrefixedWriter(image + " push | ") - var remoteImg string - if imgDst == nil { - remoteImg = image - } else { - tb := ctlb.TagBuilder{} - randSuffix, err := tb.RandomStr50() - if err != nil { - return "", fmt.Errorf("generating image dst suffix: %s", err) - } - remoteImg = imgDst.NewImage + ":kbld-" + randSuffix - } - - digest, pushErr := BuildahPush(image, remoteImg, prefixedLogger) - if pushErr != nil { - return "", pushErr - } - return remoteImg + "@" + digest, nil -} // PushImage From 035ac3022f64e2ff98dab30e16a84cae1f0a9766 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Wed, 25 Jun 2025 17:52:45 +0200 Subject: [PATCH 13/22] buildah : use tag 'latest' by default Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 087e22ad..affe54a6 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -6,7 +6,6 @@ import ( "os/exec" "strings" - ctlb "carvel.dev/kbld/pkg/kbld/builder" ctlconf "carvel.dev/kbld/pkg/kbld/config" ctllog "carvel.dev/kbld/pkg/kbld/logger" ) @@ -37,12 +36,7 @@ func ensureDirectory(directory string) error { // Generate a name to send the image to the server func remoteImageName(imgDst ctlconf.ImageDestination) string { if len(imgDst.Tags) == 0 { - tb := ctlb.TagBuilder{} - randSuffix, err := tb.RandomStr50() - if err != nil { - return imgDst.NewImage + ":kbld" - } - return imgDst.NewImage + ":kbld-" + randSuffix + return imgDst.NewImage + ":latest" } else { return imgDst.NewImage + ":" + imgDst.Tags[0] } From 6c1cdc05da8a48b8dedbbe2cb68b4ee264a0dbb6 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Wed, 25 Jun 2025 17:53:59 +0200 Subject: [PATCH 14/22] test : buildah build and push Signed-off-by: Benjamin Le Rohellec --- test/e2e/build_buildah_test.go | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/e2e/build_buildah_test.go diff --git a/test/e2e/build_buildah_test.go b/test/e2e/build_buildah_test.go new file mode 100644 index 00000000..efa42dec --- /dev/null +++ b/test/e2e/build_buildah_test.go @@ -0,0 +1,59 @@ +package e2e + +import ( + "regexp" + "strings" + "testing" +) + +func TestBuildahBuildAndPush(t *testing.T) { + env := BuildEnv(t) + kbld := Kbld{t, env.KbldBinaryPath, Logger{}} + + input := env.WithRegistries(` +kind: Object +spec: +- image: docker.io/*username*/kbld-e2e-tests-build +- image: docker.io/*username*/kbld-e2e-tests-build2 +--- +apiVersion: kbld.k14s.io/v1alpha1 +kind: Sources +sources: +- image: docker.io/*username*/kbld-e2e-tests-build + path: assets/simple-app + buildah: + pull: true +- image: docker.io/*username*/kbld-e2e-tests-build2 + path: assets/simple-app + buildah: + # try out multi platform build + platforms: ["linux/amd64","linux/arm64"] + +--- +apiVersion: kbld.k14s.io/v1alpha1 +kind: ImageDestinations +destinations: +- image: docker.io/*username*/kbld-e2e-tests-build +- image: docker.io/*username*/kbld-e2e-tests-build2 + tags: + - test +`) + + out, _ := kbld.RunWithOpts([]string{"-f", "-", "--images-annotation=false"}, RunOpts{ + StdinReader: strings.NewReader(input), + }) + + out = strings.Replace(out, regexp.MustCompile("sha256:[a-z0-9]{64}").FindString(out), "SHA256-REPLACED1", -1) + out = strings.Replace(out, regexp.MustCompile("sha256:[a-z0-9]{64}").FindString(out), "SHA256-REPLACED2", -1) + + expectedOut := env.WithRegistries(`--- +kind: Object +spec: +- image: index.docker.io/*username*/kbld-e2e-tests-build:latest@SHA256-REPLACED1 +- image: index.docker.io/*username*/kbld-e2e-tests-build2:test@SHA256-REPLACED2 +`) + + if out != expectedOut { + t.Fatalf("Expected >>>%s<<< to match >>>%s<<<", out, expectedOut) + } +} From 88a921650320b9b2b3a261ac64c3aaf15aab4a37 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Wed, 21 Jan 2026 20:15:42 +0100 Subject: [PATCH 15/22] buildah : direct pipe for stderr Generation logs are printed in direct using stderr. Flushes mid-sentence are not well rendered through logger. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index affe54a6..00196c63 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -80,7 +80,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco cmd := exec.Command("buildah", cmdArgs...) cmd.Dir = directory cmd.Stdout = prefixedLogger - cmd.Stderr = prefixedLogger + cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { From ac12cc83402f7956d29f3a078b1ab6b95a9af7a1 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Wed, 21 Jan 2026 20:17:32 +0100 Subject: [PATCH 16/22] =?UTF-8?q?buildah=C2=A0:=20use=20a=20new=20manifest?= =?UTF-8?q?=20for=20each=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the same manifest name accumulates images. The local manifest needs a random name. Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 00196c63..327f7fda 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -6,6 +6,7 @@ import ( "os/exec" "strings" + ctlb "carvel.dev/kbld/pkg/kbld/builder" ctlconf "carvel.dev/kbld/pkg/kbld/config" ctllog "carvel.dev/kbld/pkg/kbld/logger" ) @@ -34,7 +35,10 @@ func ensureDirectory(directory string) error { } // Generate a name to send the image to the server -func remoteImageName(imgDst ctlconf.ImageDestination) string { +func remoteImageName(configImageName string, imgDst *ctlconf.ImageDestination) string { + if imgDst == nil { + return configImageName + } if len(imgDst.Tags) == 0 { return imgDst.NewImage + ":latest" } else { @@ -43,12 +47,17 @@ func remoteImageName(imgDst ctlconf.ImageDestination) string { } // Generate a name to store the image in local +// The local name is always new and random. The manifest is new each time and do not accumulate images. func localImageName(configImageName string, imgDest *ctlconf.ImageDestination) string { - if imgDest == nil { - return configImageName - } else { - return remoteImageName(*imgDest) + if imgDest != nil { + configImageName = imgDest.NewImage + } + tb := ctlb.TagBuilder{} + randSuffix, err := tb.RandomStr50() + if err != nil { + return configImageName + ":kbld" } + return configImageName + ":kbld-" + randSuffix } func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { @@ -90,7 +99,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco } pushLogger := b.logger.NewPrefixedWriter(image + " push | ") - remoteName := localName + remoteName := remoteImageName(image, imgDst) digest, pushErr := BuildahPush(localName, remoteName, pushLogger) if pushErr != nil { return "", pushErr From e1f435eb14f430a3cc66866208b59177a933341f Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Wed, 8 Apr 2026 18:50:52 +0200 Subject: [PATCH 17/22] License Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 3 +++ pkg/kbld/config/config_buildah.go | 3 +++ test/e2e/build_buildah_test.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 327f7fda..4a1ead57 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -1,3 +1,6 @@ +// Copyright 2026 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package buildah import ( diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index d610022b..10f877b5 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -1,3 +1,6 @@ +// Copyright 2026 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package config import "strings" diff --git a/test/e2e/build_buildah_test.go b/test/e2e/build_buildah_test.go index efa42dec..b6a6ffd4 100644 --- a/test/e2e/build_buildah_test.go +++ b/test/e2e/build_buildah_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package e2e import ( From 9cf350af473ad8d68b89b91d04529f061122dcab Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Thu, 9 Apr 2026 19:47:10 +0200 Subject: [PATCH 18/22] :pencil: Comments and documentations Fix the linter checks. Signed-off-by: Benjamin Le Rohellec --- .golangci.yml | 2 ++ pkg/kbld/builder/buildah/buildah.go | 34 +++++++++++++++++++---------- pkg/kbld/config/config_buildah.go | 4 +++- test/e2e/build_buildah_test.go | 2 ++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9e734ba9..cf3096a9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,8 @@ linters-settings: rules: - name: dot-imports disabled: true + - name: add-constant + disabled: true issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 4a1ead57..f87a1a8f 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -1,9 +1,16 @@ // Copyright 2026 The Carvel Authors. // SPDX-License-Identifier: Apache-2.0 +// Package buildah use Buildah to build container images +// +// Buildah will consume a directory as context and a Dockerfile/Containerfile as instructions. +// To support multiples architectures at once, buildah create and push manifests. +// +// https://github.com/containers/buildah package buildah import ( + "errors" "fmt" "os" "os/exec" @@ -14,10 +21,12 @@ import ( ctllog "carvel.dev/kbld/pkg/kbld/logger" ) +// Buildah is the builder class using the buildah tool type Buildah struct { logger ctllog.Logger } +// New creates a new Buildah builder func New(logger ctllog.Logger) Buildah { return Buildah{logger} } @@ -28,8 +37,7 @@ func ensureDirectory(directory string) error { return fmt.Errorf("Checking if path '%s' is a directory: %s", directory, err) } - // Provide explicit directory check error message because otherwise docker CLI - // outputs confusing msg 'error: fork/exec /usr/local/bin/docker: not a directory' + // Buildah requires a directory as context if !stat.IsDir() { return fmt.Errorf("Expected path '%s' to be a directory, but was not", directory) } @@ -38,15 +46,15 @@ func ensureDirectory(directory string) error { } // Generate a name to send the image to the server +// This name is not random to avoid cluttering the server with an endless stream of persistent tags func remoteImageName(configImageName string, imgDst *ctlconf.ImageDestination) string { if imgDst == nil { return configImageName } if len(imgDst.Tags) == 0 { return imgDst.NewImage + ":latest" - } else { - return imgDst.NewImage + ":" + imgDst.Tags[0] } + return imgDst.NewImage + ":" + imgDst.Tags[0] } // Generate a name to store the image in local @@ -63,18 +71,20 @@ func localImageName(configImageName string, imgDest *ctlconf.ImageDestination) s return configImageName + ":kbld-" + randSuffix } +// BuildAndPushImage builds an image using a directory and some options, send the result to a remote server and return the tag with hash. func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { + const noName = "" if imgDst == nil { - return "", fmt.Errorf("a destination is required to store the built image") + return noName, errors.New("a destination is required to store the built image") } err := ensureDirectory(directory) if err != nil { - return "", err + return noName, err } prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ") - prefixedLogger.Write([]byte(fmt.Sprintf("Start building using buildah\n"))) + prefixedLogger.Write([]byte("Start building using buildah\n")) localName := localImageName(image, imgDst) cmdArgs := []string{"build", "--manifest=" + localName} @@ -97,23 +107,23 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco err := cmd.Run() if err != nil { prefixedLogger.Write([]byte(fmt.Sprintf("error: %s\n", err))) - return "", err + return noName, err } } pushLogger := b.logger.NewPrefixedWriter(image + " push | ") remoteName := remoteImageName(image, imgDst) - digest, pushErr := BuildahPush(localName, remoteName, pushLogger) + digest, pushErr := Push(localName, remoteName, pushLogger) if pushErr != nil { - return "", pushErr + return noName, pushErr } remoteName = remoteName + "@" + digest prefixedLogger.WriteStr("Image build : " + remoteName) return remoteName, nil } -// Push the buildah manifest and return the digest -func BuildahPush(src string, dest string, log *ctllog.PrefixWriter) (string, error) { +// Push sends the buildah manifest to a remote server and return the digest +func Push(src string, dest string, log *ctllog.PrefixWriter) (string, error) { digestFile, digestErr := os.CreateTemp("", "buildah-") if digestErr != nil { return "", fmt.Errorf("cannot create digest file: %w", digestErr) diff --git a/pkg/kbld/config/config_buildah.go b/pkg/kbld/config/config_buildah.go index 10f877b5..395fa0f6 100644 --- a/pkg/kbld/config/config_buildah.go +++ b/pkg/kbld/config/config_buildah.go @@ -5,7 +5,7 @@ package config import "strings" -// Options for builds using Containerfiles +// ContainerFileOpts stores options for all build systems using Containerfiles. // // see https://github.com/containers/common/blob/main/docs/Containerfile.5.md type ContainerFileOpts struct { @@ -23,12 +23,14 @@ type ContainerFileOpts struct { Platforms []string } +// SourceBuildahOpts stores options for buildah only. type SourceBuildahOpts struct { ContainerFileOpts // More options RawOptions *[]string `json:"rawOptions"` } +// Args create the `buildah build` command arguments from options. func (opts SourceBuildahOpts) Args() []string { args := []string{} diff --git a/test/e2e/build_buildah_test.go b/test/e2e/build_buildah_test.go index b6a6ffd4..0ee7a365 100644 --- a/test/e2e/build_buildah_test.go +++ b/test/e2e/build_buildah_test.go @@ -1,3 +1,5 @@ +//go:build e2e + // Copyright 2026 The Carvel Authors. // SPDX-License-Identifier: Apache-2.0 From 2457d0e68350383e6ca5627e4a11f1aac2405df0 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Thu, 9 Apr 2026 22:32:29 +0200 Subject: [PATCH 19/22] :whale: Install buildah in test image Signed-off-by: Benjamin Le Rohellec --- hack/Dockerfile.dev | 2 +- test/e2e/assets/simple-app/Dockerfile | 2 +- test/e2e/assets/simple-app/dev/Dockerfile.dev | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/Dockerfile.dev b/hack/Dockerfile.dev index f814cf02..5b57551d 100644 --- a/hack/Dockerfile.dev +++ b/hack/Dockerfile.dev @@ -1,7 +1,7 @@ FROM golang:1.23 RUN apt-get update -y -RUN apt-get install docker.io apt-transport-https ca-certificates gnupg python-is-python3 -y +RUN apt-get install docker.io apt-transport-https ca-certificates gnupg python-is-python3 buildah -y RUN mkdir -p ~/.docker/cli-plugins/ RUN curl -sLo ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.8.2/buildx-v0.8.2.linux-amd64 diff --git a/test/e2e/assets/simple-app/Dockerfile b/test/e2e/assets/simple-app/Dockerfile index 63637232..caa608e2 100644 --- a/test/e2e/assets/simple-app/Dockerfile +++ b/test/e2e/assets/simple-app/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.10.1 AS build-env +FROM docker.io/library/golang:1.10.1 AS build-env WORKDIR /go/src/github.com/mchmarny/simple-app/ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -v -o app diff --git a/test/e2e/assets/simple-app/dev/Dockerfile.dev b/test/e2e/assets/simple-app/dev/Dockerfile.dev index 1c7c8791..232e20b0 100644 --- a/test/e2e/assets/simple-app/dev/Dockerfile.dev +++ b/test/e2e/assets/simple-app/dev/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM golang:1.10.1 +FROM docker.io/library/golang:1.10.1 WORKDIR /go/src/github.com/mchmarny/simple-app/ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -v -o app From b260a393221861146aaa6880a0ab8a18bcba4b17 Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Fri, 10 Apr 2026 07:42:43 +0200 Subject: [PATCH 20/22] Fix format string with prefix writer Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index f87a1a8f..42ff64c1 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -97,7 +97,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco // Use current directory as context // cmdArgs = append(cmdArgs, "./") - prefixedLogger.WriteStr("=> buildah " + strings.Join(cmdArgs, " ")) + prefixedLogger.Write([]byte("=> buildah " + strings.Join(cmdArgs, " "))) { cmd := exec.Command("buildah", cmdArgs...) cmd.Dir = directory @@ -118,7 +118,7 @@ func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlco return noName, pushErr } remoteName = remoteName + "@" + digest - prefixedLogger.WriteStr("Image build : " + remoteName) + prefixedLogger.Write([]byte("Image build : " + remoteName)) return remoteName, nil } @@ -138,7 +138,7 @@ func Push(src string, dest string, log *ctllog.PrefixWriter) (string, error) { }() // !!! with --digestfile, buildah will not return an error if an authentication is required. - log.WriteStr("=> buildah manifest push --all --digestfile=" + digestFile.Name() + " " + src + " docker://" + dest) + log.Write([]byte("=> buildah manifest push --all --digestfile=" + digestFile.Name() + " " + src + " docker://" + dest)) pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digestFile.Name(), src, "docker://"+dest) pushCommand.Stdout = log pushCommand.Stderr = log From 3cfb73afa05531ec5f6b068e7d916322262cf22a Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Mon, 27 Apr 2026 21:59:32 +0200 Subject: [PATCH 21/22] :art: golangci-lint line-length-limit = 80 Signed-off-by: Benjamin Le Rohellec --- pkg/kbld/builder/buildah/buildah.go | 57 ++++++++++++++++++++--------- pkg/kbld/image/built.go | 10 +++-- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/pkg/kbld/builder/buildah/buildah.go b/pkg/kbld/builder/buildah/buildah.go index 42ff64c1..71edf5a4 100644 --- a/pkg/kbld/builder/buildah/buildah.go +++ b/pkg/kbld/builder/buildah/buildah.go @@ -3,8 +3,9 @@ // Package buildah use Buildah to build container images // -// Buildah will consume a directory as context and a Dockerfile/Containerfile as instructions. -// To support multiples architectures at once, buildah create and push manifests. +// Buildah will consume a directory as context and a Dockerfile as instructions. +// To support multiples architectures at once, buildah create and push +// manifests instead of simple images. // // https://github.com/containers/buildah package buildah @@ -34,20 +35,24 @@ func New(logger ctllog.Logger) Buildah { func ensureDirectory(directory string) error { stat, err := os.Stat(directory) if err != nil { - return fmt.Errorf("Checking if path '%s' is a directory: %s", directory, err) + return fmt.Errorf("checking if path '%s' is a directory: %s", + directory, err) } // Buildah requires a directory as context if !stat.IsDir() { - return fmt.Errorf("Expected path '%s' to be a directory, but was not", directory) + return fmt.Errorf("expected path '%s' to be a directory, but was not", + directory) } return nil } // Generate a name to send the image to the server -// This name is not random to avoid cluttering the server with an endless stream of persistent tags -func remoteImageName(configImageName string, imgDst *ctlconf.ImageDestination) string { +// This name is not random to avoid cluttering the server with an endless +// stream of persistent tags +func remoteImageName(configImageName string, + imgDst *ctlconf.ImageDestination) string { if imgDst == nil { return configImageName } @@ -58,8 +63,10 @@ func remoteImageName(configImageName string, imgDst *ctlconf.ImageDestination) s } // Generate a name to store the image in local -// The local name is always new and random. The manifest is new each time and do not accumulate images. -func localImageName(configImageName string, imgDest *ctlconf.ImageDestination) string { +// The local name is always new and random. +// The manifest is new each time and do not accumulate images. +func localImageName(configImageName string, + imgDest *ctlconf.ImageDestination) string { if imgDest != nil { configImageName = imgDest.NewImage } @@ -71,11 +78,15 @@ func localImageName(configImageName string, imgDest *ctlconf.ImageDestination) s return configImageName + ":kbld-" + randSuffix } -// BuildAndPushImage builds an image using a directory and some options, send the result to a remote server and return the tag with hash. -func (b Buildah) BuildAndPushImage(image string, directory string, imgDst *ctlconf.ImageDestination, opts ctlconf.SourceBuildahOpts) (string, error) { +// BuildAndPushImage builds an image using a directory and some options, +// send the result to a remote server and return the tag with hash. +func (b Buildah) BuildAndPushImage(image string, directory string, + imgDst *ctlconf.ImageDestination, + opts ctlconf.SourceBuildahOpts) (string, error) { const noName = "" if imgDst == nil { - return noName, errors.New("a destination is required to store the built image") + return noName, errors.New( + "a destination is required to store the built image") } err := ensureDirectory(directory) @@ -130,27 +141,37 @@ func Push(src string, dest string, log *ctllog.PrefixWriter) (string, error) { } defer func() { if err := digestFile.Close(); err != nil { - fmt.Printf("ERROR: Closing temp file %q: %v", digestFile.Name(), err) + fmt.Printf( + "ERROR: Closing temporary file %q: %v", digestFile.Name(), err) } if err := os.Remove(digestFile.Name()); err != nil { - fmt.Printf("ERROR: Removing temp file %q: %v", digestFile.Name(), err) + fmt.Printf( + "ERROR: Deleting temporary file %q: %v", digestFile.Name(), err) } }() - // !!! with --digestfile, buildah will not return an error if an authentication is required. - log.Write([]byte("=> buildah manifest push --all --digestfile=" + digestFile.Name() + " " + src + " docker://" + dest)) - pushCommand := exec.Command("buildah", "manifest", "push", "--all", "--digestfile="+digestFile.Name(), src, "docker://"+dest) + // !!! with --digestfile, buildah will not return an error if an + // authentication is required but the file will be empty. + log.WriteStr( + "=> buildah manifest push --all --digestfile=%s %s docker://%s", + digestFile.Name(), src, dest) + pushCommand := exec.Command("buildah", "manifest", "push", "--all", + "--digestfile="+digestFile.Name(), src, "docker://"+dest) pushCommand.Stdout = log pushCommand.Stderr = log pushErr := pushCommand.Run() if pushErr != nil { - return "", fmt.Errorf("error pushing to %q (check if you are authenticated) : %w", dest, pushErr) + return "", fmt.Errorf( + "error pushing to %q (check if you are authenticated) : %w", + dest, pushErr) } digest := make([]byte, 64+7) digestLen, readErr := digestFile.Read(digest) if readErr != nil { - return "", fmt.Errorf("cannot read digest in file %q (check if you are authenticated) : %w", digestFile.Name(), readErr) + return "", fmt.Errorf( + "cannot read digest in file %q (you may not be authenticated) : %w", + digestFile.Name(), readErr) } return string(digest[0:digestLen]), nil } // BuildahPush diff --git a/pkg/kbld/image/built.go b/pkg/kbld/image/built.go index b6936d8a..799ed100 100644 --- a/pkg/kbld/image/built.go +++ b/pkg/kbld/image/built.go @@ -31,9 +31,10 @@ type BuiltImage struct { func NewBuiltImage(url string, buildSource ctlconf.Source, imgDst *ctlconf.ImageDestination, docker ctlbdk.Docker, dockerBuildx ctlbdk.Buildx, pack ctlbpk.Pack, - kubectlBuildkit ctlbkb.KubectlBuildkit, ko ctlbko.Ko, bazel ctlbbz.Bazel, buildah ctlbah.Buildah) BuiltImage { - - return BuiltImage{url, buildSource, imgDst, docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel, buildah} + kubectlBuildkit ctlbkb.KubectlBuildkit, ko ctlbko.Ko, bazel ctlbbz.Bazel, + buildah ctlbah.Buildah) BuiltImage { + return BuiltImage{url, buildSource, imgDst, + docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel, buildah} } func (i BuiltImage) URL() (string, []ctlconf.Origin, error) { @@ -87,7 +88,8 @@ func (i BuiltImage) URL() (string, []ctlconf.Origin, error) { return url, origins, err case i.buildSource.Buildah != nil: - tag, err := i.buildah.BuildAndPushImage(urlRepo, i.buildSource.Path, i.imgDst, *i.buildSource.Buildah) + tag, err := i.buildah.BuildAndPushImage( + urlRepo, i.buildSource.Path, i.imgDst, *i.buildSource.Buildah) return tag, origins, err // Fall back on Docker by default From 8e78554259b0408297aa728ac7825d85d1299a6b Mon Sep 17 00:00:00 2001 From: Benjamin Le Rohellec Date: Tue, 28 Apr 2026 19:06:18 +0200 Subject: [PATCH 22/22] CI install qemu-user-static to run cross platform build Signed-off-by: Benjamin Le Rohellec --- hack/test-all-locally.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hack/test-all-locally.sh b/hack/test-all-locally.sh index 91328476..daa8377d 100755 --- a/hack/test-all-locally.sh +++ b/hack/test-all-locally.sh @@ -31,6 +31,8 @@ minikube docker-env | while read env; do echo $env | grep -E 'export*' | awk '{print $2}' | sed 's/"//g' done > $tempConfigFile +docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker run \ --privileged \ --env-file $tempConfigFile \