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
5 changes: 4 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ linters:
enable-all-rules: true
rules:
- name: dot-imports
disabled: true
- name: add-constant
disabled: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
new-from-rev: 0ad1b35d4334a8e10856bf9c6d687266430f71d4
new-from-rev: 0ad1b35d4334a8e10856bf9c6d687266430f71d4
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 -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
156 changes: 156 additions & 0 deletions pkg/kbld/builder/buildah/buildah.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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

Comment on lines +10 to +11
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be:

// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

import (
"errors"
"fmt"
"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"
)

// 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}
}

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)
}

// Buildah requires a directory as context
if !stat.IsDir() {
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 {
if imgDst == nil {
return configImageName
}
if len(imgDst.Tags) == 0 {
return imgDst.NewImage + ":latest"
}
return imgDst.NewImage + ":" + imgDst.Tags[0]
}

// 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 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")
}

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

prefixedLogger := b.logger.NewPrefixedWriter(image + " build | ")
prefixedLogger.Write([]byte("Start building using buildah\n"))

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.Write([]byte("=> buildah " + strings.Join(cmdArgs, " ")))
{
cmd := exec.Command("buildah", cmdArgs...)
cmd.Dir = directory
cmd.Stdout = prefixedLogger
cmd.Stderr = os.Stderr

Comment on lines +102 to +106
err := cmd.Run()
if err != nil {
prefixedLogger.Write([]byte(fmt.Sprintf("error: %s\n", err)))
return noName, err
}
}

pushLogger := b.logger.NewPrefixedWriter(image + " push | ")
remoteName := remoteImageName(image, imgDst)
digest, pushErr := Push(localName, remoteName, pushLogger)
if pushErr != nil {
return noName, pushErr
}
remoteName = remoteName + "@" + digest
prefixedLogger.Write([]byte("Image build : " + remoteName))
return remoteName, nil
Comment on lines +114 to +122
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using a random tag when one is provided by the user or "latest" is an easy choice. Random tags needs to be cleaned.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the behavior that all the builders have so I think it does make sense to keep consistency in the tool.
There are some things we need to keep in mind;

  • what happens when the user do not provide a tag?
  • if this image does not pass tests, would you want it to always update the latest tag?
  • what happens if the user provide more than 1 tag, which one do we choose? (i do not think it is a major deal because in the end we will have to tag with the other ones, but is just a deviation of what is done with other builders)

}

// 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)
}
defer func() {
if err := digestFile.Close(); err != nil {
fmt.Printf("ERROR: Closing temp 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)
Comment thread
joaopapereira marked this conversation as resolved.
}
}()

// !!! 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)
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 := 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 string(digest[0:digestLen]), nil
Comment on lines +140 to +155
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buildah will write only the digest and nothing else (no space, no newline). This is a buildah specification.

} // BuildahPush
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
54 changes: 54 additions & 0 deletions pkg/kbld/config/config_buildah.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package config

import "strings"

Comment on lines +4 to +7
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be

// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

// ContainerFileOpts stores options for all build systems 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
// Option "--build-arg=K=V"
BuildArgs map[string]string `json:"buildArgs"`
//
Target *string
//
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{}

if opts.Pull {
args = append(args, "--pull")
}
for arg, value := range opts.BuildArgs {
args = append(args, "--build-arg="+arg+"="+value)
}
Comment on lines +40 to +42
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
10 changes: 8 additions & 2 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,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) {
Expand Down Expand Up @@ -84,6 +86,10 @@ 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:
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 {
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
5 changes: 4 additions & 1 deletion pkg/kbld/image/tagged.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Comment on lines +31 to 33
if err != nil {
return "", nil, err
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/assets/simple-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/assets/simple-app/dev/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -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
Expand Down
64 changes: 64 additions & 0 deletions test/e2e/build_buildah_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build e2e

// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package e2e

import (
Comment on lines +6 to +8
Comment on lines +6 to +8
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be

// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

"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)
}
}
Loading