-
Notifications
You must be signed in to change notification settings - Fork 47
Buildah/podman support #543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
a4c36c7
308dcba
93710e5
fde3ab7
37746f1
89cee0b
50e71df
74c14b2
b3408d6
5b578f1
bdc5ff5
dfe5ff9
035ac30
6c1cdc0
88a9216
ac12cc8
e1f435e
9cf350a
2457d0e
b260a39
2e4e7d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
| } | ||
|
|
||
| // 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) | ||
|
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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should be |
||
| // 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 | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should be |
||
| "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) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be: