add ability to add images from local docker daemon (#551)

signed-off-by: Adam Martin <adam.martin@ranchergovernment.com>
This commit is contained in:
Adam Martin
2026-04-19 18:51:36 -04:00
committed by GitHub
parent b2d0f9f01e
commit 40c4fdded4
10 changed files with 319 additions and 2 deletions

View File

@@ -355,7 +355,10 @@ func addStoreAddImage(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Co
hauler store add image rgcrprod.azurecr.us/rancher/rke2-runtime:v1.31.5-rke2r1 --platform linux/amd64 --key carbide-key.pub
# fetch image and rewrite path
hauler store add image busybox --rewrite custom-path/busybox:latest`,
hauler store add image busybox --rewrite custom-path/busybox:latest
# add image from local Docker daemon
hauler store add image my-local-app:latest --local`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

View File

@@ -69,6 +69,17 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re
cfg := v1.Image{
Name: reference,
Rewrite: o.Rewrite,
Local: o.Local,
}
if o.Local {
if o.Key != "" || o.CertIdentity != "" || o.CertIdentityRegexp != "" {
return fmt.Errorf("--local cannot be combined with cosign verification flags (--key, --certificate-identity, --certificate-identity-regexp): signatures are not available from the Docker daemon")
}
if o.Platform != "" {
l.Warnf("--platform is ignored when --local is set: the Docker daemon stores only the host platform image")
}
return storeLocalImage(ctx, s, cfg, rso, ro, o.Rewrite)
}
// Check if the user provided a key.
@@ -95,6 +106,60 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re
return storeImage(ctx, s, cfg, o.Platform, o.ExcludeExtras, rso, ro, o.Rewrite)
}
func storeLocalImage(ctx context.Context, s *store.Layout, i v1.Image, _ *flags.StoreRootOpts, ro *flags.CliRootOpts, rewrite string) error {
l := log.FromContext(ctx)
if !ro.IgnoreErrors {
envVar := os.Getenv(consts.HaulerIgnoreErrors)
if envVar == "true" {
ro.IgnoreErrors = true
}
}
l.Infof("adding image [%s] from local Docker daemon to the store", i.Name)
r, err := name.ParseReference(i.Name)
if err != nil {
if ro.IgnoreErrors {
l.Warnf("unable to parse image [%s]: %v... skipping...", i.Name, err)
return nil
}
l.Errorf("unable to parse image [%s]: %v", i.Name, err)
return err
}
if err := s.AddLocalImage(ctx, r.Name()); err != nil {
if ro.IgnoreErrors {
l.Warnf("unable to add image [%s] from Docker daemon to store: %v... skipping...", r.Name(), err)
return nil
}
l.Errorf("unable to add image [%s] from Docker daemon to store: %v", r.Name(), err)
return err
}
if rewrite != "" {
rawRewrite := rewrite
rewrite = strings.TrimPrefix(rewrite, "/")
if !strings.Contains(rewrite, ":") {
if tag, ok := r.(name.Tag); ok {
rewrite = rewrite + ":" + tag.TagStr()
} else {
return fmt.Errorf("cannot rewrite digest reference [%s] without an explicit tag in the rewrite", r.Name())
}
}
newRef, err := name.ParseReference(rewrite)
if err != nil {
return fmt.Errorf("unable to parse rewrite name [%s]: %w", rewrite, err)
}
if err := rewriteReference(ctx, s, r, newRef, rawRewrite); err != nil {
return err
}
}
l.Infof("successfully added image [%s] from local Docker daemon", r.Name())
return nil
}
func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform string, excludeExtras bool, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, rewrite string) error {
l := log.FromContext(ctx)

View File

@@ -855,3 +855,85 @@ func TestStoreChart_AddImages_IncludeExtras(t *testing.T) {
assertArtifactKindInStore(t, s, "test/chart-image:v2", consts.KindAnnotationSboms)
})
}
// --------------------------------------------------------------------------
// --local flag validation tests
// --------------------------------------------------------------------------
func TestAddImageCmd_LocalFlagValidation(t *testing.T) {
ctx := newTestContext(t)
tests := []struct {
name string
opts *flags.AddImageOpts
}{
{
name: "Local with Key returns error",
opts: &flags.AddImageOpts{
Local: true,
Key: "some.pub",
},
},
{
name: "Local with CertIdentity returns error",
opts: &flags.AddImageOpts{
Local: true,
CertIdentity: "foo@bar.com",
},
},
{
name: "Local with CertIdentityRegexp returns error",
opts: &flags.AddImageOpts{
Local: true,
CertIdentityRegexp: ".*",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s := newTestStore(t)
rso := defaultRootOpts(s.Root)
ro := defaultCliOpts()
err := AddImageCmd(ctx, tc.opts, s, "nginx:latest", rso, ro)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "--local cannot be combined") {
t.Errorf("expected '--local cannot be combined' in error, got: %v", err)
}
})
}
}
// --------------------------------------------------------------------------
// storeLocalImage unit tests
// --------------------------------------------------------------------------
func TestStoreLocalImage_InvalidReference(t *testing.T) {
ctx := newTestContext(t)
t.Run("malformed reference returns error", func(t *testing.T) {
s := newTestStore(t)
rso := defaultRootOpts(s.Root)
ro := defaultCliOpts()
err := storeLocalImage(ctx, s, v1.Image{Name: "INVALID:::ref"}, rso, ro, "")
if err == nil {
t.Fatal("expected error for malformed reference, got nil")
}
})
t.Run("malformed reference with IgnoreErrors returns nil", func(t *testing.T) {
s := newTestStore(t)
rso := defaultRootOpts(s.Root)
ro := defaultCliOpts()
ro.IgnoreErrors = true
err := storeLocalImage(ctx, s, v1.Image{Name: "INVALID:::ref"}, rso, ro, "")
if err != nil {
t.Fatalf("expected nil with IgnoreErrors=true, got: %v", err)
}
})
}

View File

@@ -316,7 +316,7 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
a := cfg.GetAnnotations()
for _, i := range cfg.Spec.Images {
if a[consts.ImageAnnotationRegistry] != "" || o.Registry != "" {
if !i.Local && (a[consts.ImageAnnotationRegistry] != "" || o.Registry != "") {
newRef, _ := reference.Parse(i.Name)
newReg := o.Registry
if o.Registry == "" && a[consts.ImageAnnotationRegistry] != "" {
@@ -331,6 +331,25 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
i.Name = newRef.Name()
}
if i.Local {
needsPubKeyVerification := a[consts.ImageAnnotationKey] != "" || o.Key != "" || i.Key != ""
needsKeylessVerification := a[consts.ImageAnnotationCertIdentityRegexp] != "" || a[consts.ImageAnnotationCertIdentity] != "" ||
o.CertIdentityRegexp != "" || o.CertIdentity != "" ||
i.CertIdentityRegexp != "" || i.CertIdentity != ""
if needsPubKeyVerification || needsKeylessVerification {
return fmt.Errorf("image [%s]: --local cannot be combined with cosign verification options", i.Name)
}
rewrite := ""
if i.Rewrite != "" {
rewrite = i.Rewrite
}
if err := storeLocalImage(ctx, s, i, rso, ro, rewrite); err != nil {
return err
}
continue
}
hasAnnotationIdentityOptions := a[consts.ImageAnnotationCertIdentityRegexp] != "" || a[consts.ImageAnnotationCertIdentity] != ""
hasCliIdentityOptions := o.CertIdentityRegexp != "" || o.CertIdentity != ""
hasImageIdentityOptions := i.CertIdentityRegexp != "" || i.CertIdentity != ""

6
go.mod
View File

@@ -112,6 +112,7 @@ require (
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
@@ -123,11 +124,15 @@ require (
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -224,6 +229,7 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

18
go.sum
View File

@@ -277,6 +277,8 @@ github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34Pl
github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
@@ -318,18 +320,26 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
@@ -706,10 +716,16 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/5
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -724,6 +740,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mozillazg/docker-credential-acr-helper v0.4.0 h1:Uoh3Z9CcpEDnLiozDx+D7oDgRq7X+R296vAqAumnOcw=
github.com/mozillazg/docker-credential-acr-helper v0.4.0/go.mod h1:2kiicb3OlPytmlNC9XGkLvVC+f0qTiJw3f/mhmeeQBg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=

View File

@@ -18,6 +18,7 @@ type AddImageOpts struct {
Platform string
Rewrite string
ExcludeExtras bool
Local bool
}
func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
@@ -32,6 +33,7 @@ func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
f.StringVarP(&o.Platform, "platform", "p", "", "(Optional) Specify the platform of the image... i.e. linux/amd64 (defaults to all)")
f.StringVar(&o.Rewrite, "rewrite", "", "(EXPERIMENTAL & Optional) Rewrite artifact path to specified string")
f.BoolVar(&o.ExcludeExtras, "exclude-extras", false, "(Optional) Exclude cosign signatures, attestations, SBOMs, and OCI referrers when pulling the image")
f.BoolVar(&o.Local, "local", false, "(Optional) Add image from the local Docker daemon instead of a remote registry")
}
type AddFileOpts struct {

View File

@@ -39,4 +39,5 @@ type Image struct {
Platform string `json:"platform"`
Rewrite string `json:"rewrite"`
ExcludeExtras bool `json:"exclude-extras"`
Local bool `json:"local"`
}

View File

@@ -0,0 +1,70 @@
package store
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnsureDockerHost(t *testing.T) {
t.Run("DOCKER_HOST already set", func(t *testing.T) {
t.Setenv("DOCKER_HOST", "unix:///some/custom/docker.sock")
err := ensureDockerHost()
if err != nil {
t.Errorf("ensureDockerHost() returned unexpected error: %v", err)
}
if got := os.Getenv("DOCKER_HOST"); got != "unix:///some/custom/docker.sock" {
t.Errorf("DOCKER_HOST was modified: got %q, want %q", got, "unix:///some/custom/docker.sock")
}
})
t.Run("no socket found returns error", func(t *testing.T) {
if _, err := os.Stat("/var/run/docker.sock"); err == nil {
t.Skip("default docker socket exists at /var/run/docker.sock")
}
tmp := t.TempDir()
t.Setenv("DOCKER_HOST", "")
t.Setenv("HOME", tmp)
err := ensureDockerHost()
if err == nil {
t.Fatal("ensureDockerHost() expected error when no socket exists, got nil")
}
if !strings.Contains(err.Error(), "no Docker socket found") {
t.Errorf("error %q does not contain %q", err.Error(), "no Docker socket found")
}
})
t.Run("sets DOCKER_HOST for Docker Desktop socket", func(t *testing.T) {
if _, err := os.Stat("/var/run/docker.sock"); err == nil {
t.Skip("default docker socket exists at /var/run/docker.sock")
}
tmp := t.TempDir()
t.Setenv("DOCKER_HOST", "")
t.Setenv("HOME", tmp)
sockDir := filepath.Join(tmp, ".docker", "run")
if err := os.MkdirAll(sockDir, 0o755); err != nil {
t.Fatalf("creating socket dir: %v", err)
}
sockPath := filepath.Join(sockDir, "docker.sock")
f, err := os.Create(sockPath)
if err != nil {
t.Fatalf("creating socket file: %v", err)
}
f.Close()
if err := ensureDockerHost(); err != nil {
t.Fatalf("ensureDockerHost() unexpected error: %v", err)
}
want := "unix://" + sockPath
if got := os.Getenv("DOCKER_HOST"); got != want {
t.Errorf("DOCKER_HOST = %q, want %q", got, want)
}
})
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/google/go-containerregistry/pkg/authn"
gname "github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/opencontainers/go-digest"
@@ -216,6 +217,56 @@ func (l *Layout) AddImage(ctx context.Context, ref string, platform string, excl
return nil
}
// AddLocalImage fetches a container image from the local Docker daemon and saves it to the store.
// No cosign signatures, attestations, SBOMs, or OCI referrers are fetched (registry-only concepts).
func (l *Layout) AddLocalImage(ctx context.Context, ref string) error {
parsedRef, err := gname.ParseReference(ref)
if err != nil {
return fmt.Errorf("parsing reference %q: %w", ref, err)
}
if err := ensureDockerHost(); err != nil {
return fmt.Errorf("failed to locate Docker daemon socket: %w -- is the Docker daemon running?", err)
}
img, err := daemon.Image(parsedRef, daemon.WithContext(ctx))
if err != nil {
return fmt.Errorf("failed to fetch image from Docker daemon: %w -- is the Docker daemon running?", err)
}
if _, err := img.Digest(); err != nil {
return fmt.Errorf("getting image digest for %q: %w", ref, err)
}
return l.writeImage(parsedRef, img, consts.KindAnnotationImage, "")
}
// ensureDockerHost sets DOCKER_HOST if it is not already set and the default
// socket (/var/run/docker.sock) does not exist. Docker Desktop on macOS places
// its socket at ~/.docker/run/docker.sock instead of the default path.
func ensureDockerHost() error {
if os.Getenv("DOCKER_HOST") != "" {
return nil
}
if _, err := os.Stat("/var/run/docker.sock"); err == nil {
return nil
}
home, err := os.UserHomeDir()
if err != nil {
return err
}
sock := filepath.Join(home, ".docker", "run", "docker.sock")
if _, err := os.Stat(sock); err != nil {
return fmt.Errorf("no Docker socket found at /var/run/docker.sock or %s", sock)
}
if err := os.Setenv("DOCKER_HOST", "unix://"+sock); err != nil {
return fmt.Errorf("setting DOCKER_HOST: %w", err)
}
return nil
}
// writeImageBlobs writes all blobs for a single image (layers, config, manifest) to the store's
// blob directory. It does not add an entry to the OCI index.
func (l *Layout) writeImageBlobs(img v1.Image) error {