From de955d455a05ade61c9b8e087530b99876491810 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 9 Apr 2026 13:26:51 +0200 Subject: [PATCH 01/10] Use Go packages to get rid of the skopeo runtime dependency entirely --- Dockerfile | 2 - README.md | 1 - go.mod | 20 +++- go.sum | 43 ++++++-- internal/deployer/deployer.go | 2 +- internal/deployer/operator.go | 2 +- internal/imagecache/imagecache.go | 2 +- internal/skopeohelper/skopeohelper.go | 143 ++++++++++++-------------- 8 files changed, 119 insertions(+), 96 deletions(-) diff --git a/Dockerfile b/Dockerfile index f6309f65..1abe7850 100644 --- a/Dockerfile +++ b/Dockerfile @@ -106,8 +106,6 @@ RUN microdnf install -y \ gzip \ unzip \ ca-certificates \ - # Container tools (skopeo for OCI image operations, no daemon required) - skopeo \ # Python (required for gcloud SDK) python3 \ # Clean up diff --git a/README.md b/README.md index a968974f..b76a2f8c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,6 @@ the cluster to succeed. Prerequisites: - `kubectl` configured to point at your target cluster -- `skopeo` is available (for OCI image operations) - The `roxctl` CLI - The `roxie` branch forked and cloned to your local machine diff --git a/go.mod b/go.mod index 8f3b9390..08f23767 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,44 @@ module github.com/stackrox/roxie -go 1.25.0 +go 1.25.6 require ( github.com/fatih/color v1.16.0 - github.com/spf13/cobra v1.8.0 + github.com/google/go-containerregistry v0.21.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 golang.org/x/term v0.38.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.35.3 ) require ( + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v29.2.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/stretchr/testify v1.11.1 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index f06dd208..10f53010 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= +github.com/docker/cli v29.2.1+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-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -10,44 +18,64 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI= +github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= @@ -56,8 +84,11 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index c6947423..8fd6bd12 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -810,7 +810,7 @@ func (d *Deployer) waitForNamespaceDeletion(namespace string) error { // checkRequiredTools verifies that required CLI tools are available func checkRequiredTools() error { - requiredTools := []string{"kubectl", "roxctl", "skopeo"} + requiredTools := []string{"kubectl", "roxctl"} var missing []string for _, tool := range requiredTools { diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index 919209d8..9878e148 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -80,7 +80,7 @@ func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleI d.logger.Dimf("Created temporary directory: %s", bundleDir) d.logger.Info("Pulling and extracting operator bundle image...") - // Force amd64 platform - the bundle images only contain platform-agnostic YAML files. + // The bundle images only contain platform-agnostic YAML files. if err := skopeohelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil { os.RemoveAll(bundleDir) return "", fmt.Errorf("failed to copy bundle contents: %w", err) diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index e072389a..a6f132e0 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -133,7 +133,7 @@ func (ic *ImageCache) VerifyImagePullable(ctx context.Context, imageRef string) return true } - // Use skopeo inspect to verify image accessibility. + // Use OCI registry client to verify image accessibility. err := skopeohelper.InspectImage(ctx, ic.logger, imageRef) if err == nil { ic.AddToCache(imageRef) diff --git a/internal/skopeohelper/skopeohelper.go b/internal/skopeohelper/skopeohelper.go index 6431f321..a545c839 100644 --- a/internal/skopeohelper/skopeohelper.go +++ b/internal/skopeohelper/skopeohelper.go @@ -4,21 +4,24 @@ import ( "archive/tar" "compress/gzip" "context" - "encoding/json" "fmt" "io" "os" - "os/exec" "path/filepath" "strings" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/stackrox/roxie/internal/logger" ) // ExtractManifestsFromImage extracts the /manifests/ directory from an operator bundle image. -// Authentication is handled automatically by skopeo from ~/.docker/config.json or $REGISTRY_AUTH_FILE. +// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef, destDir string) error { - tempDir, err := os.MkdirTemp("", "skopeo-image-") + tempDir, err := os.MkdirTemp("", "oci-image-") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } @@ -26,12 +29,13 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef log.Dimf("Using temporary directory: %s", tempDir) - if err := copyImageToDir(ctx, log, imageRef, tempDir); err != nil { + img, err := fetchImage(ctx, log, imageRef) + if err != nil { return err } log.Dim("Extracting /manifests/ directory from image layers...") - if err := extractManifestsFromDir(log, tempDir, destDir); err != nil { + if err := extractManifestsFromImage(log, img, tempDir, destDir); err != nil { return err } @@ -40,75 +44,70 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef } // InspectImage verifies that an OCI image is accessible. -// Authentication is handled automatically by skopeo from ~/.docker/config.json or $REGISTRY_AUTH_FILE. +// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. func InspectImage(ctx context.Context, log *logger.Logger, imageRef string) error { log.Dimf("Inspecting image %s", imageRef) - cmd := exec.CommandContext(ctx, "skopeo", "inspect", "--raw", withDockerTransport(imageRef)) - output, err := cmd.CombinedOutput() + ref, err := name.ParseReference(imageRef) + if err != nil { + return fmt.Errorf("invalid image reference: %w", err) + } + + // Use HEAD request to verify image exists without downloading + _, err = remote.Head(ref, + remote.WithContext(ctx), + remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { - log.Dimf("skopeo command output: %s", string(output)) - return fmt.Errorf("skopeo inspect failed: %w", err) + return fmt.Errorf("image inspection failed: %w", err) } log.Dim("✓ Image is accessible") return nil } -// copyImageToDir downloads an OCI image to a local directory. -func copyImageToDir(ctx context.Context, log *logger.Logger, imageRef, destDir string) error { - log.Dimf("Copying image %s to %s", imageRef, destDir) +// fetchImage downloads an OCI image from a registry. +func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Image, error) { + log.Dimf("Fetching image %s", imageRef) - args := []string{ - "copy", - "--override-os", "linux", - "--override-arch", "amd64", - withDockerTransport(imageRef), - fmt.Sprintf("dir:%s", destDir), - } - - cmd := exec.CommandContext(ctx, "skopeo", args...) - output, err := cmd.CombinedOutput() + ref, err := name.ParseReference(imageRef) if err != nil { - log.Dimf("skopeo command output: %s", string(output)) - return fmt.Errorf("skopeo copy failed: %w", err) + return nil, fmt.Errorf("invalid image reference: %w", err) } - log.Dim("✓ Image copied successfully") - return nil -} - -type imageManifest struct { - Layers []imageLayer `json:"layers"` -} - -type imageLayer struct { - Digest string `json:"digest"` -} + // For operator bundles, we fetch linux/amd64 by default as they contain + // platform-agnostic YAML files. + platform := v1.Platform{ + OS: "linux", + Architecture: "amd64", + } -// extractManifestsFromDir extracts /manifests/ from a skopeo dir-format image. -func extractManifestsFromDir(log *logger.Logger, skopeoDir, destDir string) error { - manifestPath := filepath.Join(skopeoDir, "manifest.json") - manifestData, err := os.ReadFile(manifestPath) + img, err := remote.Image(ref, + remote.WithContext(ctx), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithPlatform(platform)) if err != nil { - return fmt.Errorf("failed to read manifest: %w", err) + return nil, fmt.Errorf("failed to fetch image: %w", err) } - var manifest imageManifest - if err := json.Unmarshal(manifestData, &manifest); err != nil { - return fmt.Errorf("failed to parse manifest: %w", err) - } + log.Dim("✓ Image fetched successfully") + return img, nil +} - // Extract all image layers into tempExtractDir. - log.Dimf("Found %d layer(s) in image", len(manifest.Layers)) - tempExtractDir, err := os.MkdirTemp("", "layer-extract-") +// extractManifestsFromImage extracts /manifests/ from an OCI image. +func extractManifestsFromImage(log *logger.Logger, img v1.Image, tempExtractDir, destDir string) error { + layers, err := img.Layers() if err != nil { - return fmt.Errorf("failed to create temp extract dir: %w", err) + return fmt.Errorf("failed to get image layers: %w", err) } - defer os.RemoveAll(tempExtractDir) - if err := extractLayers(log, manifest.Layers, skopeoDir, tempExtractDir); err != nil { - return err + log.Dimf("Found %d layer(s) in image", len(layers)) + + // Extract all layers into tempExtractDir + for i, layer := range layers { + log.Dimf("Extracting layer %d/%d...", i+1, len(layers)) + if err := extractLayerToDir(layer, tempExtractDir); err != nil { + return fmt.Errorf("failed to extract layer %d: %w", i+1, err) + } } // From the directory into which all layers have been extracted, copy the @@ -122,32 +121,20 @@ func extractManifestsFromDir(log *logger.Logger, skopeoDir, destDir string) erro return os.CopyFS(destDir, os.DirFS(manifestsDir)) } -// extractLayers extracts all image layers to a destination directory. -func extractLayers(log *logger.Logger, layers []imageLayer, skopeoDir, destDir string) error { - for i, layer := range layers { - log.Dimf("Extracting layer %d/%d...", i+1, len(layers)) - // Strip algorithm prefix (sha256:, sha512:, blake3:, etc.) from digest to obtain the filename. - _, encoded, ok := strings.Cut(layer.Digest, ":") - if !ok { - return fmt.Errorf("invalid digest format (missing ':' separator): %s", layer.Digest) - } - layerFile := filepath.Join(skopeoDir, encoded) - if err := extractTarToDir(layerFile, destDir); err != nil { - return fmt.Errorf("failed to extract layer %s: %w", layer.Digest, err) - } - } - return nil -} - -// extractTarToDir extracts a gzip-compressed tar file to a directory. -func extractTarToDir(tarPath, destDir string) error { - f, err := os.Open(tarPath) +// extractLayerToDir extracts a single image layer to a directory. +func extractLayerToDir(layer v1.Layer, destDir string) error { + rc, err := layer.Compressed() if err != nil { - return fmt.Errorf("failed to open tar file: %w", err) + return fmt.Errorf("failed to get layer contents: %w", err) } - defer f.Close() + defer rc.Close() - gzr, err := gzip.NewReader(f) + return extractTarGzToDir(rc, destDir) +} + +// extractTarGzToDir extracts a gzip-compressed tar stream to a directory. +func extractTarGzToDir(r io.Reader, destDir string) error { + gzr, err := gzip.NewReader(r) if err != nil { return fmt.Errorf("failed to create gzip reader: %w", err) } @@ -206,7 +193,3 @@ func extractTarToDir(tarPath, destDir string) error { return nil } - -func withDockerTransport(imageRef string) string { - return "docker://" + imageRef -} From 637fb78a1cd215568c2f333e9db48eab570c81c3 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 9 Apr 2026 13:29:28 +0200 Subject: [PATCH 02/10] Rename from skopeohelper to ocihelper --- internal/deployer/operator.go | 4 ++-- internal/imagecache/imagecache.go | 4 ++-- .../{skopeohelper/skopeohelper.go => ocihelper/ocihelper.go} | 2 +- .../ocihelper_integration_test.go} | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename internal/{skopeohelper/skopeohelper.go => ocihelper/ocihelper.go} (99%) rename internal/{skopeohelper/skopeohelper_integration_test.go => ocihelper/ocihelper_integration_test.go} (99%) diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index 9878e148..50bb6d3d 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -15,7 +15,7 @@ import ( "gopkg.in/yaml.v3" "github.com/stackrox/roxie/internal/env" - "github.com/stackrox/roxie/internal/skopeohelper" + "github.com/stackrox/roxie/internal/ocihelper" ) const ( @@ -81,7 +81,7 @@ func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleI d.logger.Info("Pulling and extracting operator bundle image...") // The bundle images only contain platform-agnostic YAML files. - if err := skopeohelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil { + if err := ocihelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil { os.RemoveAll(bundleDir) return "", fmt.Errorf("failed to copy bundle contents: %w", err) } diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index a6f132e0..9a887b2b 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -9,7 +9,7 @@ import ( "sync" "github.com/stackrox/roxie/internal/logger" - "github.com/stackrox/roxie/internal/skopeohelper" + "github.com/stackrox/roxie/internal/ocihelper" ) // ImageCache manages cache of verified pullable Docker images @@ -134,7 +134,7 @@ func (ic *ImageCache) VerifyImagePullable(ctx context.Context, imageRef string) } // Use OCI registry client to verify image accessibility. - err := skopeohelper.InspectImage(ctx, ic.logger, imageRef) + err := ocihelper.InspectImage(ctx, ic.logger, imageRef) if err == nil { ic.AddToCache(imageRef) return true diff --git a/internal/skopeohelper/skopeohelper.go b/internal/ocihelper/ocihelper.go similarity index 99% rename from internal/skopeohelper/skopeohelper.go rename to internal/ocihelper/ocihelper.go index a545c839..352412f6 100644 --- a/internal/skopeohelper/skopeohelper.go +++ b/internal/ocihelper/ocihelper.go @@ -1,4 +1,4 @@ -package skopeohelper +package ocihelper import ( "archive/tar" diff --git a/internal/skopeohelper/skopeohelper_integration_test.go b/internal/ocihelper/ocihelper_integration_test.go similarity index 99% rename from internal/skopeohelper/skopeohelper_integration_test.go rename to internal/ocihelper/ocihelper_integration_test.go index 869dc19e..682353ac 100644 --- a/internal/skopeohelper/skopeohelper_integration_test.go +++ b/internal/ocihelper/ocihelper_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package skopeohelper +package ocihelper import ( "context" From 699b928b0125a40cacc621dc46e5acfcec227d25 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 9 Apr 2026 16:05:22 +0200 Subject: [PATCH 03/10] Use os.Root --- internal/ocihelper/ocihelper.go | 43 +++++++++++++++------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 352412f6..bf479e06 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -8,7 +8,6 @@ import ( "io" "os" "path/filepath" - "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -140,8 +139,14 @@ func extractTarGzToDir(r io.Reader, destDir string) error { } defer gzr.Close() + // Open a Root directory to prevent path traversal attacks. + root, err := os.OpenRoot(destDir) + if err != nil { + return fmt.Errorf("failed to open directory %q as root directory: %w", destDir, err) + } + defer root.Close() + tr := tar.NewReader(gzr) - dirPermissions := make(map[string]os.FileMode) // Map to track directory permissions. for { header, err := tr.Next() @@ -152,44 +157,34 @@ func extractTarGzToDir(r io.Reader, destDir string) error { return fmt.Errorf("failed to read tar header: %w", err) } - // Prevent path traversal attacks. - target := filepath.Join(destDir, header.Name) - relPath, err := filepath.Rel(destDir, target) - if err != nil || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) || relPath == ".." { - continue - } + // Clean the path to remove redundant separators and .. components. + // This optimization reduces the cost of Root operations. + cleanPath := filepath.Clean(header.Name) switch header.Typeflag { case tar.TypeSymlink, tar.TypeLink: // Skip symlinks and hardlinks for security. case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) + // Create directory using Root (automatically prevents traversal). + // Root will reject absolute paths, "..", and other traversal attempts. + err := root.Mkdir(cleanPath, 0755) + if err != nil && !os.IsExist(err) { + return err } - dirPermissions[target] = os.FileMode(header.Mode) case tar.TypeReg: - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return fmt.Errorf("failed to create parent directory: %w", err) - } - - outFile, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) + // Create file using Root (automatically prevents traversal). + outFile, err := root.OpenFile(cleanPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { - return fmt.Errorf("failed to create file %s: %w", target, err) + return fmt.Errorf("failed to create file %s: %w", cleanPath, err) } if _, err := io.Copy(outFile, tr); err != nil { outFile.Close() - return fmt.Errorf("failed to write file %s: %w", target, err) + return fmt.Errorf("failed to write file %s: %w", cleanPath, err) } outFile.Close() } } - for dir, perm := range dirPermissions { - if err := os.Chmod(dir, perm); err != nil { - return fmt.Errorf("failed to set directory permissions for %s: %w", dir, err) - } - } - return nil } From 8f8e08fc7aebfb2be162e214845a2852a6ade49a Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 13 Apr 2026 11:04:55 +0200 Subject: [PATCH 04/10] Rename InspectImage -> VerifyImageExistence --- internal/imagecache/imagecache.go | 2 +- internal/ocihelper/ocihelper.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index 9a887b2b..262a6cec 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -134,7 +134,7 @@ func (ic *ImageCache) VerifyImagePullable(ctx context.Context, imageRef string) } // Use OCI registry client to verify image accessibility. - err := ocihelper.InspectImage(ctx, ic.logger, imageRef) + err := ocihelper.VerifyImageExistence(ctx, ic.logger, imageRef) if err == nil { ic.AddToCache(imageRef) return true diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index bf479e06..5a51327f 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -42,9 +42,9 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef return nil } -// InspectImage verifies that an OCI image is accessible. +// VerifyImageExistence verifies that an OCI image is accessible. // Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. -func InspectImage(ctx context.Context, log *logger.Logger, imageRef string) error { +func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef string) error { log.Dimf("Inspecting image %s", imageRef) ref, err := name.ParseReference(imageRef) From dd5796d3d7560e075567551790d0b0a0362595be Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier <111092021+mclasmeier@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:08:31 +0200 Subject: [PATCH 05/10] Update internal/ocihelper/ocihelper.go Co-authored-by: Marcin Owsiany --- internal/ocihelper/ocihelper.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 5a51327f..88bf7f42 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -165,8 +165,6 @@ func extractTarGzToDir(r io.Reader, destDir string) error { case tar.TypeSymlink, tar.TypeLink: // Skip symlinks and hardlinks for security. case tar.TypeDir: - // Create directory using Root (automatically prevents traversal). - // Root will reject absolute paths, "..", and other traversal attempts. err := root.Mkdir(cleanPath, 0755) if err != nil && !os.IsExist(err) { return err From d92922ef5c474bc6726cd3943e2fdc00adeb165c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier <111092021+mclasmeier@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:08:45 +0200 Subject: [PATCH 06/10] Update internal/ocihelper/ocihelper.go Co-authored-by: Marcin Owsiany --- internal/ocihelper/ocihelper.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 88bf7f42..3c7709fb 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -170,7 +170,6 @@ func extractTarGzToDir(r io.Reader, destDir string) error { return err } case tar.TypeReg: - // Create file using Root (automatically prevents traversal). outFile, err := root.OpenFile(cleanPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { return fmt.Errorf("failed to create file %s: %w", cleanPath, err) From cf7e036f6a7231699f919852bab933f02a3d8387 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 13 Apr 2026 11:13:58 +0200 Subject: [PATCH 07/10] Use MkdirAll instead of Mkdir and remove IsExist check --- internal/ocihelper/ocihelper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 3c7709fb..17303232 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -165,8 +165,8 @@ func extractTarGzToDir(r io.Reader, destDir string) error { case tar.TypeSymlink, tar.TypeLink: // Skip symlinks and hardlinks for security. case tar.TypeDir: - err := root.Mkdir(cleanPath, 0755) - if err != nil && !os.IsExist(err) { + err := root.MkdirAll(cleanPath, 0755) + if err != nil { return err } case tar.TypeReg: From cb5b7218d26a0d218a4b145049e404b35111f55d Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 13 Apr 2026 11:19:51 +0200 Subject: [PATCH 08/10] Remove file path cleaning --- internal/ocihelper/ocihelper.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 17303232..68d28feb 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -157,27 +157,25 @@ func extractTarGzToDir(r io.Reader, destDir string) error { return fmt.Errorf("failed to read tar header: %w", err) } - // Clean the path to remove redundant separators and .. components. - // This optimization reduces the cost of Root operations. - cleanPath := filepath.Clean(header.Name) + entryName := header.Name switch header.Typeflag { case tar.TypeSymlink, tar.TypeLink: // Skip symlinks and hardlinks for security. case tar.TypeDir: - err := root.MkdirAll(cleanPath, 0755) + err := root.MkdirAll(entryName, 0755) if err != nil { return err } case tar.TypeReg: - outFile, err := root.OpenFile(cleanPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) + outFile, err := root.OpenFile(entryName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { - return fmt.Errorf("failed to create file %s: %w", cleanPath, err) + return fmt.Errorf("failed to create file %s: %w", entryName, err) } if _, err := io.Copy(outFile, tr); err != nil { outFile.Close() - return fmt.Errorf("failed to write file %s: %w", cleanPath, err) + return fmt.Errorf("failed to write file %s: %w", entryName, err) } outFile.Close() } From 5504ebe0e6b7f1107d65474f107949bcd487c7e0 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 13 Apr 2026 11:20:17 +0200 Subject: [PATCH 09/10] Log on unsupported entry types --- internal/ocihelper/ocihelper.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 68d28feb..35bc2858 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -104,7 +104,7 @@ func extractManifestsFromImage(log *logger.Logger, img v1.Image, tempExtractDir, // Extract all layers into tempExtractDir for i, layer := range layers { log.Dimf("Extracting layer %d/%d...", i+1, len(layers)) - if err := extractLayerToDir(layer, tempExtractDir); err != nil { + if err := extractLayerToDir(log, layer, tempExtractDir); err != nil { return fmt.Errorf("failed to extract layer %d: %w", i+1, err) } } @@ -121,18 +121,18 @@ func extractManifestsFromImage(log *logger.Logger, img v1.Image, tempExtractDir, } // extractLayerToDir extracts a single image layer to a directory. -func extractLayerToDir(layer v1.Layer, destDir string) error { +func extractLayerToDir(log *logger.Logger, layer v1.Layer, destDir string) error { rc, err := layer.Compressed() if err != nil { return fmt.Errorf("failed to get layer contents: %w", err) } defer rc.Close() - return extractTarGzToDir(rc, destDir) + return extractTarGzToDir(log, rc, destDir) } // extractTarGzToDir extracts a gzip-compressed tar stream to a directory. -func extractTarGzToDir(r io.Reader, destDir string) error { +func extractTarGzToDir(log *logger.Logger, r io.Reader, destDir string) error { gzr, err := gzip.NewReader(r) if err != nil { return fmt.Errorf("failed to create gzip reader: %w", err) @@ -160,8 +160,6 @@ func extractTarGzToDir(r io.Reader, destDir string) error { entryName := header.Name switch header.Typeflag { - case tar.TypeSymlink, tar.TypeLink: - // Skip symlinks and hardlinks for security. case tar.TypeDir: err := root.MkdirAll(entryName, 0755) if err != nil { @@ -178,6 +176,9 @@ func extractTarGzToDir(r io.Reader, destDir string) error { return fmt.Errorf("failed to write file %s: %w", entryName, err) } outFile.Close() + default: + log.Dimf("Skipping unsupported tar entry %s of type %c", entryName, header.Typeflag) + continue } } From c0230990e9e65eb827692974e778e635a5576e79 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 13 Apr 2026 21:14:24 +0200 Subject: [PATCH 10/10] Review feedback --- internal/ocihelper/ocihelper.go | 61 +++++++++++++++------------------ 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/internal/ocihelper/ocihelper.go b/internal/ocihelper/ocihelper.go index 35bc2858..4b88022a 100644 --- a/internal/ocihelper/ocihelper.go +++ b/internal/ocihelper/ocihelper.go @@ -2,7 +2,6 @@ package ocihelper import ( "archive/tar" - "compress/gzip" "context" "fmt" "io" @@ -17,6 +16,28 @@ import ( "github.com/stackrox/roxie/internal/logger" ) +// VerifyImageExistence verifies that an OCI image is accessible. +// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. +func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef string) error { + log.Dimf("Inspecting image %s", imageRef) + + ref, err := name.ParseReference(imageRef) + if err != nil { + return fmt.Errorf("invalid image reference: %w", err) + } + + // Use HEAD request to verify image exists without downloading + _, err = remote.Head(ref, + remote.WithContext(ctx), + remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return fmt.Errorf("image inspection failed: %w", err) + } + + log.Dim("✓ Image is accessible") + return nil +} + // ExtractManifestsFromImage extracts the /manifests/ directory from an operator bundle image. // Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef, destDir string) error { @@ -42,28 +63,6 @@ func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef return nil } -// VerifyImageExistence verifies that an OCI image is accessible. -// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE. -func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef string) error { - log.Dimf("Inspecting image %s", imageRef) - - ref, err := name.ParseReference(imageRef) - if err != nil { - return fmt.Errorf("invalid image reference: %w", err) - } - - // Use HEAD request to verify image exists without downloading - _, err = remote.Head(ref, - remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return fmt.Errorf("image inspection failed: %w", err) - } - - log.Dim("✓ Image is accessible") - return nil -} - // fetchImage downloads an OCI image from a registry. func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Image, error) { log.Dimf("Fetching image %s", imageRef) @@ -122,23 +121,17 @@ func extractManifestsFromImage(log *logger.Logger, img v1.Image, tempExtractDir, // extractLayerToDir extracts a single image layer to a directory. func extractLayerToDir(log *logger.Logger, layer v1.Layer, destDir string) error { - rc, err := layer.Compressed() + rc, err := layer.Uncompressed() if err != nil { return fmt.Errorf("failed to get layer contents: %w", err) } defer rc.Close() - return extractTarGzToDir(log, rc, destDir) + return extractTarToDir(log, rc, destDir) } -// extractTarGzToDir extracts a gzip-compressed tar stream to a directory. -func extractTarGzToDir(log *logger.Logger, r io.Reader, destDir string) error { - gzr, err := gzip.NewReader(r) - if err != nil { - return fmt.Errorf("failed to create gzip reader: %w", err) - } - defer gzr.Close() - +// extractTarToDir extracts an uncompressed tar stream to a directory. +func extractTarToDir(log *logger.Logger, r io.Reader, destDir string) error { // Open a Root directory to prevent path traversal attacks. root, err := os.OpenRoot(destDir) if err != nil { @@ -146,7 +139,7 @@ func extractTarGzToDir(log *logger.Logger, r io.Reader, destDir string) error { } defer root.Close() - tr := tar.NewReader(gzr) + tr := tar.NewReader(r) for { header, err := tr.Next()