Skip to content

Commit 862c463

Browse files
mclasmeierMoritz Clasmeierporridge
authored
Remove skopeo dependency (#83)
Co-authored-by: Moritz Clasmeier <mclasmeier@redhat.com> Co-authored-by: Marcin Owsiany <porridge@redhat.com>
1 parent 8651361 commit 862c463

10 files changed

Lines changed: 240 additions & 233 deletions

File tree

Dockerfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@ RUN microdnf install -y \
106106
gzip \
107107
unzip \
108108
ca-certificates \
109-
# Container tools (skopeo for OCI image operations, no daemon required)
110-
skopeo \
111109
# Python (required for gcloud SDK)
112110
python3 \
113111
# Clean up

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ the cluster to succeed.
6464

6565
Prerequisites:
6666
- `kubectl` configured to point at your target cluster
67-
- `skopeo` is available (for OCI image operations)
6867
- The `roxctl` CLI
6968
- The `roxie` branch forked and cloned to your local machine
7069

go.mod

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
11
module github.com/stackrox/roxie
22

3-
go 1.25.0
3+
go 1.25.6
44

55
require (
66
github.com/fatih/color v1.16.0
7-
github.com/spf13/cobra v1.8.0
7+
github.com/google/go-containerregistry v0.21.0
8+
github.com/spf13/cobra v1.10.2
9+
github.com/stretchr/testify v1.11.1
810
golang.org/x/term v0.38.0
911
gopkg.in/yaml.v3 v3.0.1
1012
k8s.io/apimachinery v0.35.3
1113
)
1214

1315
require (
16+
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
1417
github.com/davecgh/go-spew v1.1.1 // indirect
18+
github.com/docker/cli v29.2.1+incompatible // indirect
19+
github.com/docker/distribution v2.8.3+incompatible // indirect
20+
github.com/docker/docker-credential-helpers v0.9.3 // indirect
1521
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
1622
github.com/go-logr/logr v1.4.3 // indirect
1723
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1824
github.com/json-iterator/go v1.1.12 // indirect
25+
github.com/klauspost/compress v1.18.4 // indirect
1926
github.com/mattn/go-colorable v0.1.13 // indirect
2027
github.com/mattn/go-isatty v0.0.20 // indirect
28+
github.com/mitchellh/go-homedir v1.1.0 // indirect
2129
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
2230
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
31+
github.com/opencontainers/go-digest v1.0.0 // indirect
32+
github.com/opencontainers/image-spec v1.1.1 // indirect
2333
github.com/pmezard/go-difflib v1.0.0 // indirect
34+
github.com/sirupsen/logrus v1.9.3 // indirect
2435
github.com/spf13/pflag v1.0.9 // indirect
25-
github.com/stretchr/testify v1.11.1 // indirect
36+
github.com/vbatts/tar-split v0.12.2 // indirect
2637
github.com/x448/float16 v0.8.4 // indirect
2738
go.yaml.in/yaml/v2 v2.4.3 // indirect
2839
golang.org/x/net v0.48.0 // indirect
29-
golang.org/x/sys v0.40.0 // indirect
40+
golang.org/x/sync v0.20.0 // indirect
41+
golang.org/x/sys v0.41.0 // indirect
3042
golang.org/x/text v0.35.0 // indirect
3143
gopkg.in/inf.v0 v0.9.1 // indirect
3244
k8s.io/klog/v2 v2.130.1 // indirect

go.sum

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
1+
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
2+
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
3+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
46
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
8+
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
9+
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
10+
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
11+
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
12+
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
513
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
614
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
715
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=
1018
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
1119
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1220
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
21+
github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI=
22+
github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
1323
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
1424
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
1525
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
1626
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
1727
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
28+
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
29+
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
1830
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
1931
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
2032
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
2133
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
2234
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
35+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
36+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
2337
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
2438
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
2539
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
2640
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
2741
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
2842
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
43+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
44+
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
45+
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
46+
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
47+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
48+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2949
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3050
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3151
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
32-
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
33-
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
34-
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
52+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
53+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
54+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
55+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
3556
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
3657
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3758
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3859
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
60+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
3961
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
4062
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
63+
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
64+
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
4165
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
4266
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
4367
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
4468
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
69+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
4570
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
4671
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
72+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
73+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
74+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4775
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4876
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49-
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
50-
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
77+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
78+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
5179
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
5280
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
5381
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+
5684
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
5785
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
5886
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
87+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5988
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
6089
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90+
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
91+
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
6192
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
6293
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
6394
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

internal/deployer/deployer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ func (d *Deployer) waitForNamespaceDeletion(namespace string) error {
814814

815815
// checkRequiredTools verifies that required CLI tools are available
816816
func checkRequiredTools() error {
817-
requiredTools := []string{"kubectl", "roxctl", "skopeo"}
817+
requiredTools := []string{"kubectl", "roxctl"}
818818

819819
var missing []string
820820
for _, tool := range requiredTools {

internal/deployer/operator.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"gopkg.in/yaml.v3"
1616

1717
"github.com/stackrox/roxie/internal/env"
18-
"github.com/stackrox/roxie/internal/skopeohelper"
18+
"github.com/stackrox/roxie/internal/ocihelper"
1919
)
2020

2121
const (
@@ -80,8 +80,8 @@ func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleI
8080
d.logger.Dimf("Created temporary directory: %s", bundleDir)
8181
d.logger.Info("Pulling and extracting operator bundle image...")
8282

83-
// Force amd64 platform - the bundle images only contain platform-agnostic YAML files.
84-
if err := skopeohelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil {
83+
// The bundle images only contain platform-agnostic YAML files.
84+
if err := ocihelper.ExtractManifestsFromImage(ctx, d.logger, bundleImage, bundleDir); err != nil {
8585
os.RemoveAll(bundleDir)
8686
return "", fmt.Errorf("failed to copy bundle contents: %w", err)
8787
}

internal/imagecache/imagecache.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"sync"
1010

1111
"github.com/stackrox/roxie/internal/logger"
12-
"github.com/stackrox/roxie/internal/skopeohelper"
12+
"github.com/stackrox/roxie/internal/ocihelper"
1313
)
1414

1515
// ImageCache manages cache of verified pullable Docker images
@@ -134,8 +134,8 @@ func (ic *ImageCache) VerifyImagePullable(ctx context.Context, imageRef string)
134134
return true
135135
}
136136

137-
// Use skopeo inspect to verify image accessibility.
138-
err := skopeohelper.InspectImage(ctx, ic.logger, imageRef)
137+
// Use OCI registry client to verify image accessibility.
138+
err := ocihelper.VerifyImageExistence(ctx, ic.logger, imageRef)
139139
if err == nil {
140140
ic.AddToCache(imageRef)
141141
return true

internal/ocihelper/ocihelper.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package ocihelper
2+
3+
import (
4+
"archive/tar"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/google/go-containerregistry/pkg/authn"
12+
"github.com/google/go-containerregistry/pkg/name"
13+
v1 "github.com/google/go-containerregistry/pkg/v1"
14+
"github.com/google/go-containerregistry/pkg/v1/remote"
15+
16+
"github.com/stackrox/roxie/internal/logger"
17+
)
18+
19+
// VerifyImageExistence verifies that an OCI image is accessible.
20+
// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE.
21+
func VerifyImageExistence(ctx context.Context, log *logger.Logger, imageRef string) error {
22+
log.Dimf("Inspecting image %s", imageRef)
23+
24+
ref, err := name.ParseReference(imageRef)
25+
if err != nil {
26+
return fmt.Errorf("invalid image reference: %w", err)
27+
}
28+
29+
// Use HEAD request to verify image exists without downloading
30+
_, err = remote.Head(ref,
31+
remote.WithContext(ctx),
32+
remote.WithAuthFromKeychain(authn.DefaultKeychain))
33+
if err != nil {
34+
return fmt.Errorf("image inspection failed: %w", err)
35+
}
36+
37+
log.Dim("✓ Image is accessible")
38+
return nil
39+
}
40+
41+
// ExtractManifestsFromImage extracts the /manifests/ directory from an operator bundle image.
42+
// Authentication is handled automatically from ~/.docker/config.json or $REGISTRY_AUTH_FILE.
43+
func ExtractManifestsFromImage(ctx context.Context, log *logger.Logger, imageRef, destDir string) error {
44+
tempDir, err := os.MkdirTemp("", "oci-image-")
45+
if err != nil {
46+
return fmt.Errorf("failed to create temp dir: %w", err)
47+
}
48+
defer os.RemoveAll(tempDir)
49+
50+
log.Dimf("Using temporary directory: %s", tempDir)
51+
52+
img, err := fetchImage(ctx, log, imageRef)
53+
if err != nil {
54+
return err
55+
}
56+
57+
log.Dim("Extracting /manifests/ directory from image layers...")
58+
if err := extractManifestsFromImage(log, img, tempDir, destDir); err != nil {
59+
return err
60+
}
61+
62+
log.Dimf("✓ Manifests extracted to: %s", destDir)
63+
return nil
64+
}
65+
66+
// fetchImage downloads an OCI image from a registry.
67+
func fetchImage(ctx context.Context, log *logger.Logger, imageRef string) (v1.Image, error) {
68+
log.Dimf("Fetching image %s", imageRef)
69+
70+
ref, err := name.ParseReference(imageRef)
71+
if err != nil {
72+
return nil, fmt.Errorf("invalid image reference: %w", err)
73+
}
74+
75+
// For operator bundles, we fetch linux/amd64 by default as they contain
76+
// platform-agnostic YAML files.
77+
platform := v1.Platform{
78+
OS: "linux",
79+
Architecture: "amd64",
80+
}
81+
82+
img, err := remote.Image(ref,
83+
remote.WithContext(ctx),
84+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
85+
remote.WithPlatform(platform))
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to fetch image: %w", err)
88+
}
89+
90+
log.Dim("✓ Image fetched successfully")
91+
return img, nil
92+
}
93+
94+
// extractManifestsFromImage extracts /manifests/ from an OCI image.
95+
func extractManifestsFromImage(log *logger.Logger, img v1.Image, tempExtractDir, destDir string) error {
96+
layers, err := img.Layers()
97+
if err != nil {
98+
return fmt.Errorf("failed to get image layers: %w", err)
99+
}
100+
101+
log.Dimf("Found %d layer(s) in image", len(layers))
102+
103+
// Extract all layers into tempExtractDir
104+
for i, layer := range layers {
105+
log.Dimf("Extracting layer %d/%d...", i+1, len(layers))
106+
if err := extractLayerToDir(log, layer, tempExtractDir); err != nil {
107+
return fmt.Errorf("failed to extract layer %d: %w", i+1, err)
108+
}
109+
}
110+
111+
// From the directory into which all layers have been extracted, copy the
112+
// /manifests/ directory to the destination.
113+
log.Dim("Copying manifests to destination...")
114+
manifestsDir := filepath.Join(tempExtractDir, "manifests")
115+
if _, err := os.Stat(manifestsDir); err != nil {
116+
return fmt.Errorf("no /manifests directory found in image: %w", err)
117+
}
118+
119+
return os.CopyFS(destDir, os.DirFS(manifestsDir))
120+
}
121+
122+
// extractLayerToDir extracts a single image layer to a directory.
123+
func extractLayerToDir(log *logger.Logger, layer v1.Layer, destDir string) error {
124+
rc, err := layer.Uncompressed()
125+
if err != nil {
126+
return fmt.Errorf("failed to get layer contents: %w", err)
127+
}
128+
defer rc.Close()
129+
130+
return extractTarToDir(log, rc, destDir)
131+
}
132+
133+
// extractTarToDir extracts an uncompressed tar stream to a directory.
134+
func extractTarToDir(log *logger.Logger, r io.Reader, destDir string) error {
135+
// Open a Root directory to prevent path traversal attacks.
136+
root, err := os.OpenRoot(destDir)
137+
if err != nil {
138+
return fmt.Errorf("failed to open directory %q as root directory: %w", destDir, err)
139+
}
140+
defer root.Close()
141+
142+
tr := tar.NewReader(r)
143+
144+
for {
145+
header, err := tr.Next()
146+
if err == io.EOF {
147+
break
148+
}
149+
if err != nil {
150+
return fmt.Errorf("failed to read tar header: %w", err)
151+
}
152+
153+
entryName := header.Name
154+
155+
switch header.Typeflag {
156+
case tar.TypeDir:
157+
err := root.MkdirAll(entryName, 0755)
158+
if err != nil {
159+
return err
160+
}
161+
case tar.TypeReg:
162+
outFile, err := root.OpenFile(entryName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
163+
if err != nil {
164+
return fmt.Errorf("failed to create file %s: %w", entryName, err)
165+
}
166+
167+
if _, err := io.Copy(outFile, tr); err != nil {
168+
outFile.Close()
169+
return fmt.Errorf("failed to write file %s: %w", entryName, err)
170+
}
171+
outFile.Close()
172+
default:
173+
log.Dimf("Skipping unsupported tar entry %s of type %c", entryName, header.Typeflag)
174+
continue
175+
}
176+
}
177+
178+
return nil
179+
}

0 commit comments

Comments
 (0)