Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ linters:
enable-all-rules: true
rules:
- name: dot-imports
- name: add-constant
disabled: false
arguments:
- allowInts: "0,1,2,3,4,5,6,7,8,9,10,16,20,22,32,64,80,128,255,443,1024,8080"
maxLitCount: "3"
allowStrs: "\"\""
issues:
max-issues-per-linter: 0
max-same-issues: 0
Expand Down
2 changes: 1 addition & 1 deletion hack/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM golang:1.25.7

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 qemu-user-static -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
Expand Down
4 changes: 4 additions & 0 deletions hack/test-all-locally.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ image_name=$(build_test_deps)
tempConfigFile=$(mktemp)
trap "rm -f $tempConfigFile" EXIT

# Ensure the host kernel (Minikube VM) is configured to run cross-platform binaries.
# This is required for Buildah to execute steps (like RUN) for linux/arm64 on an amd64 host.
docker run --privileged --rm tonistiigi/binfmt --install all

minikube docker-env | while read env; do
echo $env | grep -E 'export*' | awk '{print $2}' | sed 's/"//g'
done > $tempConfigFile
Expand Down
191 changes: 191 additions & 0 deletions pkg/kbld/builder/buildah/buildah.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

// Package buildah provides a builder implementation using Buildah.
package buildah

import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"

ctlb "carvel.dev/kbld/pkg/kbld/builder"
ctlconf "carvel.dev/kbld/pkg/kbld/config"
ctllog "carvel.dev/kbld/pkg/kbld/logger"
)

var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)

// Buildah struct to define the Builder
type Buildah struct {
logger ctllog.Logger
}

// New create a new Buildah builder
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
}

// 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 {
configImageName = imgDest.NewImage
}
tb := ctlb.TagBuilder{}
randSuffix, err := tb.RandomStr50()
if err != nil {
return configImageName + ":kbld"
}
return configImageName + ":kbld-" + randSuffix
}

// BuildAndPushImage builds and pushed the images to the registry
func (b Buildah) BuildAndPushImage(image string, directory string,
imgDst *ctlconf.ImageDestination,
opts ctlconf.SourceBuildahOpts) (string, error) {
if imgDst == nil {
return "", errors.New(
"a destination is required to store the built image")
}

err := ensureDirectory(directory)
if err != nil {
return "", err
}

prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ")
prefixedLogger.WriteStr("Start building using buildah")

localName := localImageName(image, imgDst)
cmdArgs := []string{"build", "--manifest=" + localName}

if opts.File != nil {
cmdArgs = append(cmdArgs, "--file="+*opts.File)
}
cmdArgs = append(cmdArgs, opts.Args()...)

// Use current directory as context
// cmdArgs = append(cmdArgs, "./")

prefixedLogger.WriteStr("=> buildah %s", strings.Join(cmdArgs, " "))
{
cmd := exec.Command("buildah", cmdArgs...)
cmd.Dir = directory
cmd.Stdout = prefixedLogger
cmd.Stderr = io.MultiWriter(os.Stderr, prefixedLogger)

err := cmd.Run()
if err != nil {
prefixedLogger.WriteStr("error: %s", err)
return "", err
}
}

pushLogger := b.logger.NewPrefixedWriter(image + " push | ")
// Push using a temporary, random tag,
// and return a canonical digest reference.
tempTagBytes := make([]byte, 8)
if _, err := rand.Read(tempTagBytes); err != nil {
return "", fmt.Errorf("generating temporary tag: %w", err)
}
tempTag := hex.EncodeToString(tempTagBytes)
tempRemoteName := fmt.Sprintf("%s:%s", imgDst.NewImage, tempTag)
digest, pushErr := Push(localName, tempRemoteName, pushLogger)

if pushErr != nil {
return "", pushErr
}
remoteName := fmt.Sprintf("%s@%s", imgDst.NewImage, digest)
prefixedLogger.WriteStr("Image build : %s", remoteName)
return remoteName, nil
}

// Push the buildah manifest 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)
}
defer func() {
if err := digestFile.Close(); err != nil {
log.WriteStr("ERROR: Closing temp file %q: %v",
digestFile.Name(), err)
}
if err := os.Remove(digestFile.Name()); err != nil {
log.WriteStr("ERROR: Removing temp file %q: %v",
digestFile.Name(), err)
}
}()

// !!! with --digestfile, buildah will not return an error
// even if an authentication is required.
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)
}

digest, err := calculateDigest(digestFile)
if err != nil {
return "", err
}
return digest, nil
}

func calculateDigest(digestFile *os.File) (string, error) {
digestBytes, readErr := os.ReadFile(digestFile.Name())
if readErr != nil {
return "", fmt.Errorf(
"cannot read digest from temporary file %q: %w",
digestFile.Name(), readErr)
}

digest := strings.TrimSpace(string(digestBytes))
if digest == "" {
return "", fmt.Errorf(
"no digest found in file %q",
digestFile.Name())
}
if !digestPattern.MatchString(digest) {
return "", fmt.Errorf(
"invalid digest format %q in file %q", digest, digestFile.Name())
}
return digest, nil
}
1 change: 1 addition & 0 deletions pkg/kbld/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type Source struct {
KubectlBuildkit *SourceKubectlBuildkitOpts
Ko *SourceKoOpts
Bazel *SourceBazelOpts
Buildah *SourceBuildahOpts
}

type ImageOverride struct {
Expand Down
79 changes: 79 additions & 0 deletions pkg/kbld/config/config_buildah.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package config

import (
"slices"
"strings"
)

// ContainerFileOpts Options for builds using Containerfiles
//
// see https://github.com/containers/common/blob/main/docs/Containerfile.5.md
type ContainerFileOpts struct {
// Pull Always pull images
Pull *bool
// NoCache Do not use cache when building the image
NoCache *bool
// File containing instructions
// Docker will use "Dockerfile" as default
// Buildah can detect "Containerfile" or "Dockerfile" as default
File *string
// BuildArgs Option "--build-arg=K=V"
BuildArgs map[string]string `json:"buildArgs"`
// Target Set the target build stage to build.
Target *string
// Platforms to build for
Platforms []string
}

// SourceBuildahOpts specifies options for building images using Buildah.
type SourceBuildahOpts struct {
ContainerFileOpts
// More options
RawOptions *[]string `json:"rawOptions"`
}

// Args stringifies the options
//
//revive:disable-next-line:cognitive-complexity
func (opts SourceBuildahOpts) Args() []string {
args := []string{}

if opts.Pull != nil && *opts.Pull {
args = append(args, "--pull")
}
if opts.NoCache != nil && *opts.NoCache {
args = append(args, "--no-cache")
}

if opts.BuildArgs != nil {
args = opts.buildArgs()
}

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
}

func (opts SourceBuildahOpts) buildArgs() []string {
var args []string
keys := make([]string, 0, len(opts.BuildArgs))
for k := range opts.BuildArgs {
keys = append(keys, k)
}
slices.Sort(keys)
for _, arg := range keys {
value := opts.BuildArgs[arg]
args = append(args, "--build-arg="+arg+"="+value)
}
return args
}
14 changes: 11 additions & 3 deletions pkg/kbld/image/built.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"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"
Expand All @@ -25,13 +26,15 @@ 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 {

return BuiltImage{url, buildSource, imgDst, docker, dockerBuildx, pack, kubectlBuildkit, ko, bazel}
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) {
Expand Down Expand Up @@ -84,6 +87,11 @@ 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:
url, err := i.buildah.BuildAndPushImage(urlRepo,
i.buildSource.Path, i.imgDst, *i.buildSource.Buildah)
return url, origins, err

// Fall back on Docker by default
default:
if i.buildSource.Docker == nil {
Expand Down
4 changes: 3 additions & 1 deletion pkg/kbld/image/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ To run the end to end tests, you must have the following utilities installed on
- [kubectl-buildkit 0.1.0](https://github.com/vmware-tanzu/buildkit-cli-for-kubectl/releases/tag/v0.1.0)
- [ko 0.8.0](https://github.com/google/ko/releases/tag/v0.8.0)
- [bazel 4.2.0](https://github.com/bazelbuild/bazel/releases/tag/4.2.0)
- [buildah 1.43.0](https://github.com/containers/buildah/blob/main/install.md)

### Run End to End Tests

Expand Down
Loading
Loading