5 Commits

Author SHA1 Message Date
Avi Deitcher
c0c5668116 swap 'pkg push' for 'pkg build --push', keeping 'pkg push' as deprecated but still working (#4141)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2025-07-04 18:00:28 +03:00
Avi Deitcher
2b4687338b add support for pkg build authentication (#4137)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2025-07-02 18:52:05 +03:00
Avi Deitcher
940c1b7b3b simplify cache locking (#4136)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2025-06-30 20:58:50 +03:00
Daniel S.
818bccf20f docs: Add instructions for OCI export from Docker (#4135)
Signed-off-by: Daniel Smith <daniel@razorsecure.com>
2025-06-30 16:27:54 +03:00
Avi Deitcher
50120bce2d ensure that new index does not break on missing lock file (#4134)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2025-06-27 11:01:43 +03:00
21 changed files with 715 additions and 326 deletions

View File

@@ -59,3 +59,31 @@ is provided, it always will pull, independent of what is in the cache.
The read process is smart enough to check each blob in the local cache before downloading
it from a registry.
## Imports from local Docker instance
To import an image from your local Docker daemon into LinuxKit, youll need to ensure the image is exported in the [OCI image format](https://docs.docker.com/build/exporters/oci-docker/), which LinuxKit understands.
This requires using a `docker-container` [buildx driver](https://docs.docker.com/build/builders/drivers/docker-container/), rather than the default.
Set it up like so:
```shell
docker buildx create --driver docker-container --driver-opt image=moby/buildkit:latest --name=ocibuilder --bootstrap
```
Then build and export your image using the OCI format:
```shell
docker buildx build --builder=ocibuilder --output type=oci,name=foo . > foo.tar
```
You can now import it into LinuxKit with:
```shell
linuxkit cache import foo.tar
```
Note that this process, as described, will only produce images for the platform/architecture you're currently on. To produce multi-platform images requires extra docker build flags and external builder or QEMU support - see [here](https://docs.docker.com/build/building/multi-platform/).
This workaround is only necessary when working with the local Docker daemon. If youre pulling from Docker Hub or another registry, you dont need to do any of this.

View File

@@ -1,71 +0,0 @@
// ALL writes to index.json at the root of the cache directory
// must be done through calls in this file. This is to ensure that it always does
// proper locking.
package cache
import (
"errors"
"fmt"
"path/filepath"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/util"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
log "github.com/sirupsen/logrus"
)
const (
indexFile = "index.json"
)
// DescriptorWrite writes a descriptor to the cache index; it validates that it has a name
// and replaces any existing one
func (p *Provider) DescriptorWrite(image string, desc v1.Descriptor) error {
if image == "" {
return errors.New("cannot write descriptor without reference name")
}
if desc.Annotations == nil {
desc.Annotations = map[string]string{}
}
desc.Annotations[imagespec.AnnotationRefName] = image
log.Debugf("writing descriptor for image %s", image)
// get our lock
lock, err := util.Lock(filepath.Join(p.dir, indexFile))
if err != nil {
return fmt.Errorf("unable to lock cache index for writing descriptor for %s: %v", image, err)
}
defer func() {
if err := lock.Unlock(); err != nil {
log.Errorf("unable to close lock for cache index after writing descriptor for %s: %v", image, err)
}
}()
// do we update an existing one? Or create a new one?
if err := p.cache.RemoveDescriptors(match.Name(image)); err != nil {
return fmt.Errorf("unable to remove old descriptors for %s: %v", image, err)
}
if err := p.cache.AppendDescriptor(desc); err != nil {
return fmt.Errorf("unable to append new descriptor for %s: %v", image, err)
}
return nil
}
// RemoveDescriptors removes all descriptors that match the provided matcher.
// It does so in a parallel-access-safe way
func (p *Provider) RemoveDescriptors(matcher match.Matcher) error {
// get our lock
lock, err := util.Lock(filepath.Join(p.dir, indexFile))
if err != nil {
return fmt.Errorf("unable to lock cache index for removing descriptor for %v: %v", matcher, err)
}
defer func() {
if err := lock.Unlock(); err != nil {
log.Errorf("unable to close lock for cache index after writing descriptor for %v: %v", matcher, err)
}
}()
return p.cache.RemoveDescriptors(matcher)
}

5
src/cmd/linuxkit/cache/const.go vendored Normal file
View File

@@ -0,0 +1,5 @@
package cache
const (
lockfile = ".lock"
)

View File

@@ -2,32 +2,44 @@ package cache
import (
"fmt"
"path/filepath"
"os"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/util"
log "github.com/sirupsen/logrus"
)
// Get get or initialize the cache
func Get(cache string) (layout.Path, error) {
func (p *Provider) Get(cache string) (layout.Path, error) {
// ensure the dir exists
if err := os.MkdirAll(cache, os.ModePerm); err != nil {
return "", fmt.Errorf("unable to create cache directory %s: %v", cache, err)
}
// first try to read the layout from the path
// if it exists, we can use it
// if it does not exist, we will initialize it
//
// do not lock for first read, because we do not need the lock except for initialization
// and future writes, so why slow down reads?
l, err := layout.FromPath(cache)
// initialize the cache path if needed
p, err := layout.FromPath(cache)
if err != nil {
lock, err := util.Lock(filepath.Join(cache, indexFile))
if err != nil {
return "", fmt.Errorf("unable to lock cache index for writing descriptor for new cache: %v", err)
if err := p.Lock(); err != nil {
return "", fmt.Errorf("unable to lock cache %s: %v", cache, err)
}
defer func() {
if err := lock.Unlock(); err != nil {
log.Errorf("unable to close lock for cache index after writing descriptor for new cache: %v", err)
}
}()
p, err = layout.Write(cache, empty.Index)
defer p.Unlock()
// after lock, try to read the layout again
// in case another process initialized it while we were waiting for the lock
// if it still does not exist, we will initialize it
l, err = layout.FromPath(cache)
if err != nil {
return p, fmt.Errorf("could not initialize cache at path %s: %v", cache, err)
l, err = layout.Write(cache, empty.Index)
if err != nil {
return l, fmt.Errorf("could not initialize cache at path %s: %v", cache, err)
}
}
}
return p, nil
return l, nil
}

View File

@@ -1,21 +1,30 @@
package cache
import (
"fmt"
"path/filepath"
"sync"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/util"
log "github.com/sirupsen/logrus"
)
// Provider cache implementation of cacheProvider
type Provider struct {
cache layout.Path
store content.Store
dir string
cache layout.Path
store content.Store
dir string
lock *util.FileLock
lockMut sync.Mutex
}
// NewProvider create a new CacheProvider based in the provided directory
func NewProvider(dir string) (*Provider, error) {
p, err := Get(dir)
p := &Provider{dir: dir, lockMut: sync.Mutex{}}
layout, err := p.Get(dir)
if err != nil {
return nil, err
}
@@ -23,5 +32,39 @@ func NewProvider(dir string) (*Provider, error) {
if err != nil {
return nil, err
}
return &Provider{p, store, dir}, nil
p.cache = layout
p.store = store
return p, nil
}
// Lock locks the cache directory to prevent concurrent access
func (p *Provider) Lock() error {
// if the lock is already set, we do not need to do anything
if p.lock != nil {
return nil
}
p.lockMut.Lock()
defer p.lockMut.Unlock()
var lockFile = filepath.Join(p.dir, lockfile)
lock, err := util.Lock(lockFile)
if err != nil {
return fmt.Errorf("unable to retrieve cache lock %s: %v", lockFile, err)
}
p.lock = lock
return nil
}
// Unlock releases the lock on the cache directory
func (p *Provider) Unlock() {
p.lockMut.Lock()
defer p.lockMut.Unlock()
// if the lock is not set, we do not need to do anything
if p.lock == nil {
return
}
var lockFile = filepath.Join(p.dir, lockfile)
if err := p.lock.Unlock(); err != nil {
log.Errorf("unable to close lock for cache %s: %v", lockFile, err)
}
p.lock = nil
}

View File

@@ -180,6 +180,12 @@ func (p *Provider) Pull(name string, withArchReferences bool) error {
return fmt.Errorf("error getting manifest for trusted image %s: %v", name, err)
}
// lock the cache so we can write to it
if err := p.Lock(); err != nil {
return fmt.Errorf("unable to lock cache for writing: %v", err)
}
defer p.Unlock()
// first attempt as an index
ii, err := desc.ImageIndex()
if err == nil {

View File

@@ -76,6 +76,12 @@ func (p *Provider) ImagePull(ref *reference.Spec, platforms []imagespec.Platform
return fmt.Errorf("error getting manifest for image %s: %v", pullImageName, err)
}
// get our lock
if err := p.Lock(); err != nil {
return fmt.Errorf("unable to lock cache for removing descriptors: %v", err)
}
defer p.Unlock()
// first attempt as an index
ii, err := desc.ImageIndex()
if err == nil {
@@ -146,6 +152,11 @@ func (p *Provider) ImageLoad(r io.Reader) ([]v1.Descriptor, error) {
index bytes.Buffer
)
log.Debugf("ImageWriteTar to cache")
// get our lock
if err := p.Lock(); err != nil {
return nil, fmt.Errorf("unable to lock cache: %v", err)
}
defer p.Unlock()
for {
header, err := tr.Next()
if err == io.EOF {
@@ -244,6 +255,11 @@ func (p *Provider) IndexWrite(ref *reference.Spec, descriptors ...v1.Descriptor)
return fmt.Errorf("error parsing index: %v", err)
}
var im v1.IndexManifest
// get our lock
if err := p.Lock(); err != nil {
return fmt.Errorf("unable to lock cache: %v", err)
}
defer p.Unlock()
// do we update an existing one? Or create a new one?
if len(indexes) > 0 {
// we already had one, so update just the referenced index and return
@@ -452,3 +468,46 @@ func (p *Provider) ImageInRegistry(ref *reference.Spec, trustedRef, architecture
}
return false, nil
}
// DescriptorWrite writes a descriptor to the cache index; it validates that it has a name
// and replaces any existing one
func (p *Provider) DescriptorWrite(image string, desc v1.Descriptor) error {
if image == "" {
return errors.New("cannot write descriptor without reference name")
}
if desc.Annotations == nil {
desc.Annotations = map[string]string{}
}
desc.Annotations[imagespec.AnnotationRefName] = image
log.Debugf("writing descriptor for image %s", image)
// get our lock
if err := p.Lock(); err != nil {
return fmt.Errorf("unable to lock cache for writing descriptors: %v", err)
}
defer p.Unlock()
// get our lock
// do we update an existing one? Or create a new one?
if err := p.cache.RemoveDescriptors(match.Name(image)); err != nil {
return fmt.Errorf("unable to remove old descriptors for %s: %v", image, err)
}
if err := p.cache.AppendDescriptor(desc); err != nil {
return fmt.Errorf("unable to append new descriptor for %s: %v", image, err)
}
return nil
}
// RemoveDescriptors removes all descriptors that match the provided matcher.
// It does so in a parallel-access-safe way
func (p *Provider) RemoveDescriptors(matcher match.Matcher) error {
// get our lock
if err := p.Lock(); err != nil {
return fmt.Errorf("unable to lock cache for removing descriptors: %v", err)
}
defer p.Unlock()
return p.cache.RemoveDescriptors(matcher)
}

View File

@@ -7,7 +7,10 @@ import (
"github.com/spf13/cobra"
)
var pkglibConfig pkglib.PkglibConfig
var (
pkglibConfig pkglib.PkglibConfig
registryCreds []string
)
func pkgCmd() *cobra.Command {
var (
@@ -72,9 +75,11 @@ func pkgCmd() *cobra.Command {
},
}
cmd.AddCommand(pkgBuildCmd())
// because there is an alias 'pkg push' for 'pkg build --push', we need to add the build command first
buildCmd := pkgBuildCmd()
cmd.AddCommand(buildCmd)
cmd.AddCommand(pkgBuilderCmd())
cmd.AddCommand(pkgPushCmd())
cmd.AddCommand(pkgPushCmd(buildCmd))
cmd.AddCommand(pkgShowTagCmd())
cmd.AddCommand(pkgManifestCmd())
cmd.AddCommand(pkgRemoteTagCmd())
@@ -96,5 +101,6 @@ func pkgCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(&dirty, "force-dirty", false, "Force the pkg(s) to be considered dirty")
cmd.PersistentFlags().BoolVar(&devMode, "dev", false, "Force org and hash to $USER and \"dev\" respectively")
cmd.PersistentFlags().StringSliceVar(&registryCreds, "registry-creds", nil, "Registry auths to use for building images, format is <registry>=<username>:<password> OR <registry>=<registry-token>. If no username is provided, it is treated as a registry token. <registry> must be a URL, e.g. 'https://index.docker.io/'. May be provided as many times as desired. Will override anything in your default.")
return cmd
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
)
@@ -19,27 +20,28 @@ const (
)
// some logic clarification:
// pkg build - builds unless is in cache or published in registry
// pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache
// pkg build --force - always builds even if is in cache or published in registry
// pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored
// pkg push - always builds unless is in cache
// pkg push --force - always builds even if is in cache
// pkg push --nobuild - skips build; if not in cache, fails
// pkg push --nobuild --force - nonsensical
// pkg build - builds unless is in cache or published in registry
// pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache
// pkg build --force - always builds even if is in cache or published in registry
// pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored
// pkg build --push - always builds unless is in cache or published in registry; pushes to registry
// pkg build --push --force - always builds even if is in cache
// pkg build --push --nobuild - skips build; if not in cache, fails
// pkg build --push --nobuild --force - nonsensical
// pkg push - equivalent to pkg build --push
// addCmdRunPkgBuildPush adds the RunE function and flags to a cobra.Command
// for "pkg build" or "pkg push".
func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
func pkgBuildCmd() *cobra.Command {
var (
force bool
pull bool
push bool
ignoreCache bool
docker bool
platforms string
skipPlatforms string
builders string
builderImage string
builderConfig string
builderRestart bool
release string
nobuild bool
@@ -51,179 +53,235 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
progress string
ssh []string
)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...)
if err != nil {
return err
}
if nobuild && force {
return errors.New("flags -force and -nobuild conflict")
}
if pull && force {
return errors.New("flags -force and -pull conflict")
}
var opts []pkglib.BuildOpt
if force {
opts = append(opts, pkglib.WithBuildForce())
}
if ignoreCache {
opts = append(opts, pkglib.WithBuildIgnoreCache())
}
if pull {
opts = append(opts, pkglib.WithBuildPull())
}
opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String()))
if withPush {
opts = append(opts, pkglib.WithBuildPush())
if nobuild {
opts = append(opts, pkglib.WithBuildSkip())
}
if release != "" {
opts = append(opts, pkglib.WithRelease(release))
}
if manifest {
opts = append(opts, pkglib.WithBuildManifest())
}
}
if docker {
opts = append(opts, pkglib.WithBuildTargetDockerCache())
}
if sbomScanner != "false" {
opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner))
}
opts = append(opts, pkglib.WithDockerfile(dockerfile))
// read any build arg files
var buildArgs []string
for _, filename := range buildArgFiles {
f, err := os.Open(filename)
cmd := &cobra.Command{
Use: "build",
Short: "build an OCI package from a directory with a yaml configuration file",
Long: `Build an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
`,
Example: ` linuxkit pkg build [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...)
if err != nil {
return fmt.Errorf("error opening build args file %s: %w", filename, err)
return err
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
buildArgs = append(buildArgs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading build args file %s: %w", filename, err)
}
}
opts = append(opts, pkglib.WithBuildArgs(buildArgs))
// skipPlatformsMap contains platforms that should be skipped
skipPlatformsMap := make(map[string]bool)
if skipPlatforms != "" {
for _, platform := range strings.Split(skipPlatforms, ",") {
parts := strings.SplitN(platform, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" {
return fmt.Errorf("invalid target platform specification '%s'", platform)
if nobuild && force {
return errors.New("flags -force and -nobuild conflict")
}
if pull && force {
return errors.New("flags -force and -pull conflict")
}
var opts []pkglib.BuildOpt
if force {
opts = append(opts, pkglib.WithBuildForce())
}
if ignoreCache {
opts = append(opts, pkglib.WithBuildIgnoreCache())
}
if pull {
opts = append(opts, pkglib.WithBuildPull())
}
opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String()))
if push {
opts = append(opts, pkglib.WithBuildPush())
if nobuild {
opts = append(opts, pkglib.WithBuildSkip())
}
skipPlatformsMap[strings.Trim(parts[1], " ")] = true
}
}
// if requested specific platforms, build those. If not, then we will
// retrieve the defaults in the loop over each package.
var plats []imagespec.Platform
// don't allow the use of --skip-platforms with --platforms
if platforms != "" && skipPlatforms != "" {
return errors.New("--skip-platforms and --platforms may not be used together")
}
// process the platforms if provided
if platforms != "" {
for _, p := range strings.Split(platforms, ",") {
parts := strings.SplitN(p, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p)
os.Exit(1)
if release != "" {
opts = append(opts, pkglib.WithRelease(release))
}
if manifest {
opts = append(opts, pkglib.WithBuildManifest())
}
plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]})
}
}
if docker {
opts = append(opts, pkglib.WithBuildTargetDockerCache())
}
// build the builders map
buildersMap := map[string]string{}
// look for builders env var
buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap)
if err != nil {
return fmt.Errorf("error in environment variable %s: %w", buildersEnvVar, err)
}
// any CLI options override env var
buildersMap, err = buildPlatformBuildersMap(builders, buildersMap)
if err != nil {
return fmt.Errorf("error in --builders flag: %w", err)
}
opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage))
opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart))
opts = append(opts, pkglib.WithProgress(progress))
if len(ssh) > 0 {
opts = append(opts, pkglib.WithSSH(ssh))
}
if sbomScanner != "false" {
opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner))
}
opts = append(opts, pkglib.WithDockerfile(dockerfile))
for _, p := range pkgs {
// things we need our own copies of
var (
pkgOpts = make([]pkglib.BuildOpt, len(opts))
pkgPlats = make([]imagespec.Platform, len(plats))
)
copy(pkgOpts, opts)
copy(pkgPlats, plats)
// unless overridden, platforms are specific to a package, so this needs to be inside the for loop
if len(pkgPlats) == 0 {
for _, a := range p.Arches() {
if _, ok := skipPlatformsMap[a]; ok {
continue
// read any build arg files
var buildArgs []string
for _, filename := range buildArgFiles {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("error opening build args file %s: %w", filename, err)
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
buildArgs = append(buildArgs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading build args file %s: %w", filename, err)
}
}
opts = append(opts, pkglib.WithBuildArgs(buildArgs))
// skipPlatformsMap contains platforms that should be skipped
skipPlatformsMap := make(map[string]bool)
if skipPlatforms != "" {
for _, platform := range strings.Split(skipPlatforms, ",") {
parts := strings.SplitN(platform, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" {
return fmt.Errorf("invalid target platform specification '%s'", platform)
}
pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a})
skipPlatformsMap[strings.Trim(parts[1], " ")] = true
}
}
// if requested specific platforms, build those. If not, then we will
// retrieve the defaults in the loop over each package.
var plats []imagespec.Platform
// don't allow the use of --skip-platforms with --platforms
if platforms != "" && skipPlatforms != "" {
return errors.New("--skip-platforms and --platforms may not be used together")
}
// process the platforms if provided
if platforms != "" {
for _, p := range strings.Split(platforms, ",") {
parts := strings.SplitN(p, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p)
os.Exit(1)
}
plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]})
}
}
// if there are no platforms to build for, do nothing.
// note that this is *not* an error; we simply skip it
if len(pkgPlats) == 0 {
fmt.Printf("Skipping %s with no architectures to build\n", p.Tag())
continue
// build the builders map
buildersMap := map[string]string{}
// look for builders env var
buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap)
if err != nil {
return fmt.Errorf("error in environment variable %s: %w", buildersEnvVar, err)
}
// any CLI options override env var
buildersMap, err = buildPlatformBuildersMap(builders, buildersMap)
if err != nil {
return fmt.Errorf("error in --builders flag: %w", err)
}
if builderConfig != "" {
if _, err := os.Stat(builderConfig); err != nil {
return fmt.Errorf("error reading builder config file %s: %w", builderConfig, err)
}
opts = append(opts, pkglib.WithBuildBuilderConfig(builderConfig))
}
pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...))
var msg, action string
switch {
case !withPush:
msg = fmt.Sprintf("Building %q", p.Tag())
action = "building"
case nobuild:
msg = fmt.Sprintf("Pushing %q without building", p.Tag())
action = "building and pushing"
default:
msg = fmt.Sprintf("Building and pushing %q", p.Tag())
action = "building and pushing"
opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage))
opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart))
opts = append(opts, pkglib.WithProgress(progress))
if len(ssh) > 0 {
opts = append(opts, pkglib.WithSSH(ssh))
}
if len(registryCreds) > 0 {
registryCredMap := make(map[string]spec.RegistryAuth)
for _, cred := range registryCreds {
parts := strings.SplitN(cred, "=", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid registry auth specification '%s'", cred)
}
registryPart := strings.TrimSpace(parts[0])
authPart := strings.TrimSpace(parts[1])
var auth spec.RegistryAuth
// if the auth is a token, we don't need a username
credParts := strings.SplitN(authPart, ":", 2)
var userPart, credPart string
userPart = strings.TrimSpace(credParts[0])
if len(credParts) == 2 {
credPart = strings.TrimSpace(credParts[1])
}
switch {
case len(registryPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', registry must not be blank", cred)
case len(credParts) == 2 && (len(userPart) == 0 || len(credPart) == 0):
return fmt.Errorf("invalid registry auth specification '%s', username and password must not be blank", cred)
case len(credParts) == 1 && len(userPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', token must not be blank", cred)
case len(credParts) == 2:
auth = spec.RegistryAuth{
Username: userPart,
Password: credPart,
}
case len(credParts) == 1:
auth = spec.RegistryAuth{
RegistryToken: authPart,
}
default:
return fmt.Errorf("invalid registry auth specification '%s'", cred)
}
registryCredMap[registryPart] = auth
}
opts = append(opts, pkglib.WithRegistryAuth(registryCredMap))
}
fmt.Println(msg)
for _, p := range pkgs {
// things we need our own copies of
var (
pkgOpts = make([]pkglib.BuildOpt, len(opts))
pkgPlats = make([]imagespec.Platform, len(plats))
)
copy(pkgOpts, opts)
copy(pkgPlats, plats)
// unless overridden, platforms are specific to a package, so this needs to be inside the for loop
if len(pkgPlats) == 0 {
for _, a := range p.Arches() {
if _, ok := skipPlatformsMap[a]; ok {
continue
}
pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a})
}
}
if err := p.Build(pkgOpts...); err != nil {
return fmt.Errorf("error %s %q: %w", action, p.Tag(), err)
// if there are no platforms to build for, do nothing.
// note that this is *not* an error; we simply skip it
if len(pkgPlats) == 0 {
fmt.Printf("Skipping %s with no architectures to build\n", p.Tag())
continue
}
pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...))
var msg, action string
switch {
case !push:
msg = fmt.Sprintf("Building %q", p.Tag())
action = "building"
case nobuild:
msg = fmt.Sprintf("Pushing %q without building", p.Tag())
action = "building and pushing"
default:
msg = fmt.Sprintf("Building and pushing %q", p.Tag())
action = "building and pushing"
}
fmt.Println(msg)
if err := p.Build(pkgOpts...); err != nil {
return fmt.Errorf("error %s %q: %w", action, p.Tag(), err)
}
}
}
return nil
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Force rebuild even if image is in local cache")
cmd.Flags().BoolVar(&pull, "pull", false, "Pull image if in registry but not in local cache; conflicts with --force")
cmd.Flags().BoolVar(&push, "push", false, "After building, if successful, push the image to the registry; if --nobuild is set, just push")
cmd.Flags().BoolVar(&ignoreCache, "ignore-cached", false, "Ignore cached intermediate images, always pulling from registry")
cmd.Flags().BoolVar(&docker, "docker", false, "Store the built image in the docker image cache instead of the default linuxkit cache")
cmd.Flags().StringVar(&platforms, "platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built")
cmd.Flags().StringVar(&skipPlatforms, "skip-platforms", "", "Platforms that should be skipped, even if present in build.yml")
cmd.Flags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes")
cmd.Flags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use")
cmd.Flags().StringVar(&builderConfig, "builder-config", "", "path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image; USE WITH CAUTION")
cmd.Flags().BoolVar(&builderRestart, "builder-restart", false, "force restarting builder, even if container with correct name and image exists")
cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))
cmd.Flags().StringVar(&release, "release", "", "Release the given version")
@@ -237,18 +295,6 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
return cmd
}
func pkgBuildCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "build",
Short: "build an OCI package from a directory with a yaml configuration file",
Long: `Build an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
`,
Example: ` linuxkit pkg build [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
}
return addCmdRunPkgBuildPush(cmd, false)
}
func buildPlatformBuildersMap(inputs string, existing map[string]string) (map[string]string, error) {
if inputs == "" {

View File

@@ -12,9 +12,10 @@ import (
func pkgBuilderCmd() *cobra.Command {
var (
builders string
platforms string
builderImage string
builders string
platforms string
builderImage string
builderConfigPath string
)
cmd := &cobra.Command{
Use: "builder",
@@ -40,11 +41,11 @@ func pkgBuilderCmd() *cobra.Command {
platformsToClean := strings.Split(platforms, ",")
switch command {
case "du":
if err := pkglib.DiskUsage(buildersMap, builderImage, platformsToClean, verbose); err != nil {
if err := pkglib.DiskUsage(buildersMap, builderImage, builderConfigPath, platformsToClean, verbose); err != nil {
return fmt.Errorf("unable to print disk usage of builder: %w", err)
}
case "prune":
if err := pkglib.PruneBuilder(buildersMap, builderImage, platformsToClean, verbose); err != nil {
if err := pkglib.PruneBuilder(buildersMap, builderImage, builderConfigPath, platformsToClean, verbose); err != nil {
return fmt.Errorf("unable to prune builder: %w", err)
}
default:
@@ -57,6 +58,7 @@ func pkgBuilderCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes")
cmd.PersistentFlags().StringVar(&platforms, "platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for")
cmd.PersistentFlags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use")
cmd.Flags().StringVar(&builderConfigPath, "builder-config", "", "path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image; USE WITH CAUTION")
return cmd
}

View File

@@ -1,18 +1,35 @@
package main
import "github.com/spf13/cobra"
import (
"fmt"
func pkgPushCmd() *cobra.Command {
"github.com/spf13/cobra"
)
func pkgPushCmd(buildCmd *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "push",
Short: "build and push an OCI package from a directory with a yaml configuration file",
Short: "Alias for 'pkg build --push'",
Long: `Build and push an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
The package may or may not be built first, depending on options
`,
Example: ` linuxkit pkg push [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
Example: ` linuxkit pkg push [options] pkg/dir/`,
SuggestFor: []string{"build"},
Args: cobra.MinimumNArgs(1),
Deprecated: "use 'pkg build --push' instead",
RunE: func(cmd *cobra.Command, args []string) error {
// Create a copy of buildCmd with push=true
if err := buildCmd.Flags().Set("push", "true"); err != nil {
return fmt.Errorf("'pkg push' unable to set 'pkg build --push': %w", err)
}
// Pass the args to the build command
buildCmd.SetArgs(args)
return buildCmd.RunE(buildCmd, args)
},
}
return addCmdRunPkgBuildPush(cmd, true)
cmd.Flags().AddFlagSet(buildCmd.Flags())
return cmd
}

View File

@@ -24,28 +24,30 @@ import (
)
type buildOpts struct {
skipBuild bool
force bool
pull bool
ignoreCache bool
push bool
release string
manifest bool
targetDocker bool
cacheDir string
cacheProvider spec.CacheProvider
platforms []imagespec.Platform
builders map[string]string
runner dockerRunner
writer io.Writer
builderImage string
builderRestart bool
sbomScan bool
sbomScannerImage string
dockerfile string
buildArgs []string
progress string
ssh []string
skipBuild bool
force bool
pull bool
ignoreCache bool
push bool
release string
manifest bool
targetDocker bool
cacheDir string
cacheProvider spec.CacheProvider
platforms []imagespec.Platform
builders map[string]string
runner dockerRunner
writer io.Writer
builderImage string
builderConfigPath string
builderRestart bool
sbomScan bool
sbomScannerImage string
dockerfile string
buildArgs []string
progress string
ssh []string
registryAuth map[string]spec.RegistryAuth
}
// BuildOpt allows callers to specify options to Build
@@ -164,6 +166,14 @@ func WithBuildBuilderImage(image string) BuildOpt {
}
}
// WithBuildBuilderConfig set the contents of the
func WithBuildBuilderConfig(builderConfigPath string) BuildOpt {
return func(bo *buildOpts) error {
bo.builderConfigPath = builderConfigPath
return nil
}
}
// WithBuildBuilderRestart restart the builder container even if it already is running with the correct image version
func WithBuildBuilderRestart(restart bool) BuildOpt {
return func(bo *buildOpts) error {
@@ -223,6 +233,14 @@ func WithSSH(ssh []string) BuildOpt {
}
}
// WithRegistryAuth stores registry credentials
func WithRegistryAuth(creds map[string]spec.RegistryAuth) BuildOpt {
return func(bo *buildOpts) error {
bo.registryAuth = creds
return nil
}
}
// Build builds the package
func (p Pkg) Build(bos ...BuildOpt) error {
var bo buildOpts
@@ -449,10 +467,11 @@ func (p Pkg) Build(bos ...BuildOpt) error {
}
imageBuildOpts.SSH = bo.ssh
imageBuildOpts.RegistryAuths = bo.registryAuth
// build for each arch and save in the linuxkit cache
for _, platform := range platformsToBuild {
builtDescs, err := p.buildArch(ctx, d, c, bo.builderImage, platform.Architecture, bo.builderRestart, writer, bo, imageBuildOpts)
builtDescs, err := p.buildArch(ctx, d, c, bo.builderImage, bo.builderConfigPath, platform.Architecture, bo.builderRestart, writer, bo, imageBuildOpts)
if err != nil {
return fmt.Errorf("error building for arch %s: %v", platform.Architecture, err)
}
@@ -602,7 +621,7 @@ func (p Pkg) Build(bos ...BuildOpt) error {
// C - manifest, saved in cache as is, referenced by the index (E), and returned as a descriptor
// D - attestations (if any), saved in cache as is, referenced by the index (E), and returned as a descriptor
// E - index, saved in cache as is, stored in cache as tag "image:tag-arch", *not* returned as a descriptor
func (p Pkg) buildArch(ctx context.Context, d dockerRunner, c spec.CacheProvider, builderImage, arch string, restart bool, writer io.Writer, bo buildOpts, imageBuildOpts spec.ImageBuildOptions) ([]registry.Descriptor, error) {
func (p Pkg) buildArch(ctx context.Context, d dockerRunner, c spec.CacheProvider, builderImage, builderConfigPath, arch string, restart bool, writer io.Writer, bo buildOpts, imageBuildOpts spec.ImageBuildOptions) ([]registry.Descriptor, error) {
var (
tagArch string
tag = p.FullTag()
@@ -671,7 +690,7 @@ func (p Pkg) buildArch(ctx context.Context, d dockerRunner, c spec.CacheProvider
imageBuildOpts.Dockerfile = bo.dockerfile
if err := d.build(ctx, tagArch, p.path, builderName, builderImage, platform, restart, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, bo.progress, imageBuildOpts); err != nil {
if err := d.build(ctx, tagArch, p.path, builderName, builderImage, builderConfigPath, platform, restart, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, bo.progress, imageBuildOpts); err != nil {
stdoutCloser()
if strings.Contains(err.Error(), "executor failed running [/dev/.buildkit_qemu_emulator") {
return nil, fmt.Errorf("buildkit was unable to emulate %s. check binfmt has been set up and works for this platform: %v", platform, err)

View File

@@ -53,10 +53,10 @@ func (d *dockerMocker) contextSupportCheck() error {
}
return errors.New("contexts not supported")
}
func (d *dockerMocker) builder(_ context.Context, _, _, _ string, _ bool) (*buildkitClient.Client, error) {
func (d *dockerMocker) builder(_ context.Context, _, _, _, _ string, _ bool) (*buildkitClient.Client, error) {
return nil, fmt.Errorf("not implemented")
}
func (d *dockerMocker) build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, builderRestart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progress string, imageBuildOpts spec.ImageBuildOptions) error {
func (d *dockerMocker) build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, builderRestart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progress string, imageBuildOpts spec.ImageBuildOptions) error {
if !d.enableBuild {
return errors.New("build disabled")
}

View File

@@ -37,12 +37,16 @@ import (
// golint requires comments on non-main(test)
// package for blank import
dockerconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
dockerconfigtypes "github.com/docker/cli/cli/config/types"
_ "github.com/moby/buildkit/client/connhelper/dockercontainer"
_ "github.com/moby/buildkit/client/connhelper/ssh"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/linter"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/sshforward/sshprovider"
"github.com/moby/buildkit/session/upload/uploadprovider"
log "github.com/sirupsen/logrus"
@@ -54,16 +58,17 @@ const (
buildkitWaitServer = 30 // seconds
buildkitCheckInterval = 1 // seconds
sbomFrontEndKey = "attest:sbom"
buildkitConfigPath = "/etc/buildkit/buildkitd.toml"
)
type dockerRunner interface {
tag(ref, tag string) error
build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, platformType string, imageBuildOpts spec.ImageBuildOptions) error
build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, restart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, platformType string, imageBuildOpts spec.ImageBuildOptions) error
save(tgt string, refs ...string) error
load(src io.Reader) error
pull(img string) (bool, error)
contextSupportCheck() error
builder(ctx context.Context, dockerContext, builderImage, platform string, restart bool) (*buildkitClient.Client, error)
builder(ctx context.Context, dockerContext, builderImage, builderConfigPath, platform string, restart bool) (*buildkitClient.Client, error)
}
type dockerRunnerImpl struct {
@@ -218,14 +223,14 @@ func (dr *dockerRunnerImpl) contextSupportCheck() error {
// 1. if dockerContext is provided, try to create a builder with that context; if it succeeds, we are done; if not, return an error.
// 2. try to find an existing named runner with the pattern; if it succeeds, we are done; if not, try next.
// 3. try to create a generic builder using the default context named "linuxkit".
func (dr *dockerRunnerImpl) builder(ctx context.Context, dockerContext, builderImage, platform string, restart bool) (*buildkitClient.Client, error) {
func (dr *dockerRunnerImpl) builder(ctx context.Context, dockerContext, builderImage, builderConfigPath, platform string, restart bool) (*buildkitClient.Client, error) {
// if we were given a context, we must find a builder and use it, or create one and use it
if dockerContext != "" {
// does the context exist?
if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err != nil {
return nil, fmt.Errorf("provided docker context '%s' not found", dockerContext)
}
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, platform, dockerContext, restart)
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, platform, dockerContext, restart)
if err != nil {
return nil, fmt.Errorf("error preparing builder based on context '%s': %v", dockerContext, err)
}
@@ -236,13 +241,13 @@ func (dr *dockerRunnerImpl) builder(ctx context.Context, dockerContext, builderI
dockerContext = fmt.Sprintf("%s-%s", "linuxkit", strings.ReplaceAll(platform, "/", "-"))
if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err == nil {
// we found an appropriately named context, so let us try to use it or error out
if client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, platform, dockerContext, restart); err == nil {
if client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, platform, dockerContext, restart); err == nil {
return client, nil
}
}
// create a generic builder
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, "", "default", restart)
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, "", "default", restart)
if err != nil {
return nil, fmt.Errorf("error ensuring builder container in default context: %v", err)
}
@@ -254,7 +259,7 @@ func (dr *dockerRunnerImpl) builder(ctx context.Context, dockerContext, builderI
// but has the wrong version of buildkit, or not running buildkit at all, remove it and create an appropriate
// one.
// Returns a network connection to the buildkit builder in the container.
func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, image, platform, dockerContext string, forceRestart bool) (*buildkitClient.Client, error) {
func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, image, configPath, platform, dockerContext string, forceRestart bool) (*buildkitClient.Client, error) {
// if no error, then we have a builder already
// inspect it to make sure it is of the right type
var (
@@ -288,6 +293,30 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
cid = containerJSON[0].ID
existingImage := containerJSON[0].Config.Image
isRunning := containerJSON[0].State.Status == "running"
// need to check for mounts, in case the builder-config is provided
// by default, we assume the configPath is correct
var configPathCorrect = true
if configPath != "" {
// if it is provided, we assume it is false until proven true
configPathCorrect = false
for _, mount := range containerJSON[0].Mounts {
// if this mount is not the buildkit config path, we can ignore it
if mount.Destination != buildkitConfigPath {
continue
}
// if the mount source does not match the provided configPath,
// we should restart it
// Just break. Since configPathCorrect is set to false, the switch statement below
// will catch it
if mount.Source != configPath {
fmt.Printf("existing container %s has config mounted from %s instead of expected %s, replacing\n", name, mount.Source, configPath)
} else {
configPathCorrect = true
}
// no need to cheak any more, we found the specific mount
break
}
}
switch {
case forceRestart:
@@ -308,6 +337,11 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
recreate = true
stop = isRunning
remove = true
case !configPathCorrect:
fmt.Printf("existing container has wrong configPath mount, restarting")
recreate = true
stop = isRunning
remove = true
case isRunning:
// if already running with the right image and permissions, just use it
fmt.Printf("using existing container %s\n", name)
@@ -351,7 +385,17 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
}
if recreate {
// create the builder
args := []string{"--context", dockerContext, "container", "run", "-d", "--name", name, "--privileged", image, "--allow-insecure-entitlement", "network.host", "--addr", fmt.Sprintf("unix://%s", buildkitSocketPath), "--debug"}
args := []string{"--context", dockerContext, "container", "run", "-d", "--name", name, "--privileged"}
// was a config file provided?
if configPath != "" {
// if so, we need to pass it as a buildkitd config file
args = append(args, "-v", fmt.Sprintf("%s:%s:ro", configPath, buildkitConfigPath))
}
args = append(args, image, "--allow-insecure-entitlement", "network.host", "--addr", fmt.Sprintf("unix://%s", buildkitSocketPath), "--debug")
if configPath != "" {
// set the config path explicitly
args = append(args, "--config", buildkitConfigPath)
}
msg := fmt.Sprintf("creating builder container '%s' in context '%s'", name, dockerContext)
fmt.Println(msg)
if err := dr.command(nil, nil, io.Discard, args...); err != nil {
@@ -442,9 +486,9 @@ func (dr *dockerRunnerImpl) tag(ref, tag string) error {
return dr.command(nil, nil, nil, "image", "tag", ref, tag)
}
func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error {
func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, restart bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error {
// ensure we have a builder
client, err := dr.builder(ctx, dockerContext, builderImage, platform, restart)
client, err := dr.builder(ctx, dockerContext, builderImage, builderConfigPath, platform, restart)
if err != nil {
return fmt.Errorf("unable to ensure builder container: %v", err)
}
@@ -495,6 +539,7 @@ func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext,
attachable := []session.Attachable{}
localDirs := map[string]string{}
// Add SSH agent provider if needed
if len(imageBuildOpts.SSH) > 0 {
configs, err := build.ParseSSH(imageBuildOpts.SSH)
if err != nil {
@@ -515,8 +560,30 @@ func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext,
} else {
localDirs[dockerui.DefaultLocalNameDockerfile] = pkg
localDirs[dockerui.DefaultLocalNameContext] = pkg
}
// add credentials
var cf *configfile.ConfigFile
if len(imageBuildOpts.RegistryAuths) > 0 {
// if static ones were provided, use those
cf = configfile.New("custom")
// merge imageBuildOpts.RegistryAuths into dockercfg
for registry, auth := range imageBuildOpts.RegistryAuths {
bareRegistry := strings.TrimPrefix(registry, "https://")
bareRegistry = strings.TrimPrefix(bareRegistry, "http://")
cf.AuthConfigs[bareRegistry] = dockerconfigtypes.AuthConfig{
ServerAddress: bareRegistry,
Username: auth.Username,
Password: auth.Password,
RegistryToken: auth.RegistryToken,
}
}
} else {
// Else use Docker authentication provider so BuildKit can use ~/.docker/config.json or OS-specific credential helpers.
cf = dockerconfig.LoadDefaultConfigFile(io.Discard)
}
attachable = append(attachable,
authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{ConfigFile: cf}),
)
solveOpts := buildkitClient.SolveOpt{
Frontend: "dockerfile.v0",

View File

@@ -93,14 +93,14 @@ func printVerbose(tw *tabwriter.Writer, du []*buildkitClient.UsageInfo) {
_ = tw.Flush()
}
func getClientForPlatform(ctx context.Context, buildersMap map[string]string, builderImage, platform string) (*buildkitClient.Client, error) {
func getClientForPlatform(ctx context.Context, buildersMap map[string]string, builderImage, builderConfigPath, platform string) (*buildkitClient.Client, error) {
p, err := platforms.Parse(platform)
if err != nil {
return nil, fmt.Errorf("failed to parse platform: %s", err)
}
dr := newDockerRunner(false)
builderName := getBuilderForPlatform(p.Architecture, buildersMap)
client, err := dr.builder(ctx, builderName, builderImage, platform, false)
client, err := dr.builder(ctx, builderName, builderImage, builderConfigPath, platform, false)
if err != nil {
return nil, fmt.Errorf("unable to ensure builder container: %v", err)
}
@@ -108,11 +108,11 @@ func getClientForPlatform(ctx context.Context, buildersMap map[string]string, bu
}
// DiskUsage of builder
func DiskUsage(buildersMap map[string]string, builderImage string, platformsToClean []string, verbose bool) error {
func DiskUsage(buildersMap map[string]string, builderImage, builderConfigPath string, platformsToClean []string, verbose bool) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for _, platform := range platformsToClean {
client, err := getClientForPlatform(ctx, buildersMap, builderImage, platform)
client, err := getClientForPlatform(ctx, buildersMap, builderImage, builderConfigPath, platform)
if err != nil {
return fmt.Errorf("cannot get client: %s", err)
}
@@ -143,12 +143,12 @@ func DiskUsage(buildersMap map[string]string, builderImage string, platformsToCl
}
// PruneBuilder clean build cache of builder
func PruneBuilder(buildersMap map[string]string, builderImage string, platformsToClean []string, verbose bool) error {
func PruneBuilder(buildersMap map[string]string, builderImage, builderConfigPath string, platformsToClean []string, verbose bool) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
total := int64(0)
for _, platform := range platformsToClean {
client, err := getClientForPlatform(ctx, buildersMap, builderImage, platform)
client, err := getClientForPlatform(ctx, buildersMap, builderImage, builderConfigPath, platform)
if err != nil {
return fmt.Errorf("cannot get client: %s", err)
}

View File

@@ -1,9 +1,16 @@
package spec
type ImageBuildOptions struct {
Labels map[string]string
BuildArgs map[string]*string
NetworkMode string
Dockerfile string
SSH []string
type RegistryAuth struct {
Username string
Password string
RegistryToken string // base64 encoded auth token
}
type ImageBuildOptions struct {
Labels map[string]string
BuildArgs map[string]*string
NetworkMode string
Dockerfile string
SSH []string
RegistryAuths map[string]RegistryAuth
}

View File

@@ -35,6 +35,9 @@ func Lock(path string) (*FileLock, error) {
// Unlock releases the lock and closes the file.
func (l *FileLock) Unlock() error {
if l == nil || l.file == nil {
return fmt.Errorf("unlock: file handle is nil")
}
flock := unix.Flock_t{
Type: unix.F_UNLCK,
Whence: int16(io.SeekStart),
@@ -44,7 +47,11 @@ func (l *FileLock) Unlock() error {
if err := unix.FcntlFlock(l.file.Fd(), unix.F_SETLKW, &flock); err != nil {
return fmt.Errorf("unlock: %w", err)
}
return l.file.Close()
if err := l.file.Close(); err != nil {
return fmt.Errorf("close lock file: %w", err)
}
l.file = nil // Prevent further use of the file handle
return nil
}
// CheckLock attempts to detect if the file is locked by another process.

View File

@@ -0,0 +1,3 @@
Dockerfile
buildkitd.toml
docker-config/

View File

@@ -0,0 +1,2 @@
FROM alpine:3.21
RUN echo hi

View File

@@ -0,0 +1,2 @@
org: linuxkit
image: auth-registry

View File

@@ -0,0 +1,129 @@
#!/bin/sh
# SUMMARY: Check that we can access a registry with auth
# LABELS:
set -e
# Source libraries. Uncomment if needed/defined
#. "${RT_LIB}"
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
clean_up() {
docker kill "${REGISTRY_NAME}" || true
DOCKER_CONFIG="${DOCKER_CONFIG}" docker buildx rm "${BUILDKIT_NAME}" || true
[ -n "${CACHDIR}" ] && rm -rf "${CACHDIR}"
[ -n "${DOCKER_CONFIG}" ] && rm -rf "${DOCKER_CONFIG}"
[ -n "${REGISTRY_DIR}" ] && rm -rf "${REGISTRY_DIR}"
}
trap clean_up EXIT
# determine platform
ARCH=$(uname -m)
if [ "${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "${ARCH}" = "aarch64" ]; then
ARCH="arm64"
fi
PLATFORM="linux/${ARCH}"
# container names
REGISTRY_NAME="test-registry-$$"
BUILDKIT_NAME="test-buildkitd-$$"
# start a registry with auth
REGISTRY_USER="testuser"
REGISTRY_PASS="testpass"
REGISTRY_PORT="5000"
REGISTRY_DIR=$(mktemp -d)
mkdir -p "$REGISTRY_DIR/auth"
docker run --rm \
--entrypoint htpasswd \
httpd:2 -Bbn "${REGISTRY_USER}" "${REGISTRY_PASS}" > "$REGISTRY_DIR/auth/htpasswd"
# Start registry
REGISTRY_CID=$(docker run -d --rm \
-p ":${REGISTRY_PORT}" \
-v "$REGISTRY_DIR/auth:/auth" \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
--name "${REGISTRY_NAME}" \
registry:3)
REGISTRY_IP=$(docker inspect "${REGISTRY_NAME}" \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
IMAGENAME="${REGISTRY_IP}:${REGISTRY_PORT}/myimage"
# start an insecure buildkit so we can load an image to the registry
cat > buildkitd.toml <<EOF
[registry."${REGISTRY_IP}:${REGISTRY_PORT}"]
insecure = true
http = true
EOF
# save the credentials
credsb64=$(printf "%s" "${REGISTRY_USER}:${REGISTRY_PASS}" | base64)
# DO NOT export DOCKER_CONFIG, as that will cause the thing we are testing to succeed.
# we need to be explicit about it.
DOCKER_CONFIG=$(pwd)/docker-config
rm -rf "${DOCKER_CONFIG}"
mkdir -p "${DOCKER_CONFIG}"
cat > "${DOCKER_CONFIG}/config.json" <<EOF
{
"auths": {
"${REGISTRY_IP}:5000": {
"auth": "${credsb64}"
}
}
}
EOF
DOCKER_CONFIG=${DOCKER_CONFIG} docker buildx create \
--name "${BUILDKIT_NAME}" \
--driver docker-container \
--buildkitd-config "$(pwd)/buildkitd.toml" \
--bootstrap
DOCKER_CONFIG=${DOCKER_CONFIG} docker buildx build \
--builder "${BUILDKIT_NAME}" \
--file Dockerfile.base \
--tag "${IMAGENAME}" \
--push \
--progress plain \
--platform "${PLATFORM}" \
.
# Generate Dockerfile for pkg with FROM
cat > Dockerfile <<EOF
FROM "${IMAGENAME}"
RUN echo SUCCESS
EOF
CACHEDIR=$(mktemp -d)
# 3 tests:
# 1. build a package with no auth - should fail
# 2. build a package with explicit auth - should succeed
# 3. build a package with auth in the config - should succeed
if linuxkit --cache "${CACHEDIR}" pkg build --platforms "${PLATFORM}" \
--builder-config "$(pwd)/buildkitd.toml" --force \
.; then
echo "Test 1 failed: build succeeded without auth"
exit 1
fi
linuxkit --cache "${CACHEDIR}" pkg build --platforms "${PLATFORM}" \
--builder-config "$(pwd)/buildkitd.toml" --force \
--registry-creds "${REGISTRY_IP}:${REGISTRY_PORT}=${REGISTRY_USER}:${REGISTRY_PASS}" \
.
DOCKER_CONFIG=${DOCKER_CONFIG} linuxkit --cache "${CACHEDIR}" pkg build --platforms "${PLATFORM}" \
--builder-config "$(pwd)/buildkitd.toml" --force \
.
exit 0