Skip to content

Commit d481c18

Browse files
committed
product: Add the copy subcommand
This enables the copying of signatures from mirrored images which were not copied over during the image sync process. At present it will only copy mirrored images. Signed-off-by: Paulo Gomes <[email protected]>
1 parent 05fe2ba commit d481c18

File tree

8 files changed

+397
-62
lines changed

8 files changed

+397
-62
lines changed

cmd/product.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
)
1111

1212
const productf = `usage:
13-
%[1]s product verify rancher-prime:v2.12.2
14-
%[1]s product artifacts rancher-prime:v2.12.2
13+
%[1]s product verify --registry <src_registry> rancher-prime:v2.12.2
14+
%[1]s product copy --registry <src_registry> rancher-prime:v2.12.2 <target_registry>
1515
`
1616

1717
func productCmd(args []string) error {
@@ -33,7 +33,21 @@ func productCmd(args []string) error {
3333
return fmt.Errorf("invalid name version %q: format expected <name>:<version>", arg)
3434
}
3535

36-
return product.Verify(registry, nameVer[0], nameVer[1], true, true)
36+
switch args[0] {
37+
case "verify":
38+
return product.Verify(registry, nameVer[0], nameVer[1], true, true)
39+
case "copy":
40+
if f.NArg() != 2 {
41+
showProductUsage()
42+
}
43+
44+
targetRegistry := f.Arg(1)
45+
return product.Copy(registry, nameVer[0], nameVer[1], targetRegistry)
46+
default:
47+
showProductUsage()
48+
}
49+
50+
return nil
3751
}
3852

3953
func showProductUsage() {

internal/imagelist/copier.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package imagelist
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"sync"
10+
11+
"github.com/google/go-containerregistry/pkg/crane"
12+
"github.com/google/go-containerregistry/pkg/name"
13+
)
14+
15+
var externalImages = map[string]string{
16+
"sig-storage/snapshot-controller": "registry.k8s.io/sig-storage/snapshot-controller",
17+
"sig-storage/snapshot-validation-webhook": "registry.k8s.io/sig-storage/snapshot-validation-webhook",
18+
"rancher/mirrored-sig-storage-csi-node-driver-registrar": "registry.k8s.io/sig-storage/csi-node-driver-registrar",
19+
"rancher/mirrored-sig-storage-csi-attacher": "registry.k8s.io/sig-storage/csi-attacher",
20+
"rancher/mirrored-longhornio-csi-attacher": "registry.k8s.io/sig-storage/csi-attacher",
21+
"rancher/mirrored-sig-storage-csi-provisioner": "registry.k8s.io/sig-storage/csi-provisioner",
22+
"rancher/mirrored-sig-storage-csi-resizer": "registry.k8s.io/sig-storage/csi-resizer",
23+
"rancher/mirrored-sig-storage-csi-snapshotter": "registry.k8s.io/sig-storage/csi-snapshotter",
24+
"rancher/mirrored-sig-storage-livenessprobe": "registry.k8s.io/sig-storage/rancher/mirrored-sig-storage-livenessprobe",
25+
"rancher/mirrored-sig-storage-snapshot-controller": "registry.k8s.io/sig-storage/snapshot-controller",
26+
"rancher/appco-redis": "dp.apps.rancher.io/containers/redis",
27+
"rancher/mirrored-cilium-cilium": "quay.io/cilium/cilium",
28+
"rancher/mirrored-cilium-cilium-envoy": "quay.io/cilium/cilium-envoy",
29+
"rancher/mirrored-cilium-clustermesh-apiserver": "quay.io/cilium/clustermesh-apiserver",
30+
"rancher/mirrored-cilium-hubble-relay": "quay.io/cilium/hubble-relay",
31+
"rancher/mirrored-cilium-operator-aws": "quay.io/cilium/operator-aws",
32+
"rancher/mirrored-cilium-operator-azure": "quay.io/cilium/operator-azure",
33+
"rancher/mirrored-cilium-operator-generic": "quay.io/cilium/operator-generic",
34+
"rancher/mirrored-kube-logging-config-reloader": "ghcr.io/kube-logging/config-reloader",
35+
"rancher/mirrored-kube-logging-logging-operator": "ghcr.io/kube-logging/logging-operator",
36+
"rancher/mirrored-kube-state-metrics-kube-state-metrics": "registry.k8s.io/kube-state-metrics/kube-state-metrics",
37+
"rancher/mirrored-elemental-operator": "registry.suse.com/rancher/elemental-operator",
38+
"rancher/mirrored-elemental-seedimage-builder": "registry.suse.com/rancher/seedimage-builder",
39+
"rancher/mirrored-cluster-api-controller": "registry.k8s.io/cluster-api/cluster-api-controller",
40+
}
41+
42+
type imageCopier struct {
43+
m sync.Mutex
44+
mirroredOnly bool
45+
}
46+
47+
func (i *imageCopier) Copy(srcImg, dstRegistry string) Entry {
48+
entry := Entry{
49+
Image: srcImg,
50+
}
51+
52+
if i.mirroredOnly && !strings.Contains(srcImg, "mirrored") {
53+
entry.Error = errors.New("skipping non-mirrored image: " + srcImg)
54+
return entry
55+
}
56+
57+
ref, err := name.ParseReference(srcImg, name.WeakValidation)
58+
if err != nil {
59+
entry.Error = err
60+
return entry
61+
}
62+
63+
reg, err := name.NewRegistry(dstRegistry)
64+
if err != nil {
65+
entry.Error = err
66+
return entry
67+
}
68+
69+
repo := reg.Repo(ref.Context().RepositoryStr())
70+
dst := repo.Tag(ref.Identifier()).String()
71+
72+
ctx := context.TODO()
73+
74+
// Reset stdout/stderr to avoid verbose output from cosign.
75+
i.m.Lock()
76+
stdout := os.Stdout
77+
stderr := os.Stderr
78+
os.Stdout = nil
79+
os.Stderr = nil
80+
81+
// We shouldn't copy signatures if the image isn't there, so copy images
82+
// but don't override them if they are already present.
83+
err = copySignature(ctx, srcImg, dst, true)
84+
if err != nil {
85+
entry.Error = err
86+
}
87+
entry.Signed = (err == nil)
88+
89+
os.Stdout = stdout
90+
os.Stderr = stderr
91+
i.m.Unlock()
92+
93+
return entry
94+
}
95+
96+
func signatureSource(srcRef name.Reference, tag string) string {
97+
repo := srcRef.Context().RepositoryStr()
98+
if upstream, found := externalImages[repo]; found {
99+
return fmt.Sprintf("%s:%s", upstream, tag)
100+
}
101+
102+
// Fully qualified reference: <registry>/<repository>:<signature_tag>
103+
return fmt.Sprintf("%s:%s", srcRef.Context().Name(), tag)
104+
}
105+
106+
func copySignature(ctx context.Context, srcImgRef, dstImgRef string, copyImage bool) error {
107+
fmt.Println(srcImgRef, dstImgRef)
108+
if copyImage {
109+
err := crane.Copy(srcImgRef, dstImgRef,
110+
crane.WithContext(ctx),
111+
crane.WithNoClobber(true)) // ensure tags won't be overwritten.
112+
113+
if err != nil && !strings.Contains(err.Error(), "refusing to clobber existing tag") {
114+
return fmt.Errorf("failed to copy image from %q to %q: %w",
115+
srcImgRef, dstImgRef, err)
116+
}
117+
}
118+
119+
digest, err := crane.Digest(srcImgRef)
120+
if err != nil {
121+
return fmt.Errorf("failed to get signed image digest for %q: %w", srcImgRef, err)
122+
}
123+
124+
sourceRef, err := name.ParseReference(srcImgRef)
125+
if err != nil {
126+
return fmt.Errorf("failed to parse source image reference: %w", err)
127+
}
128+
129+
hex := strings.TrimPrefix(digest, "sha256:")
130+
signatureTag := fmt.Sprintf("sha256-%s.sig", hex)
131+
sourceSigRef := signatureSource(sourceRef, signatureTag)
132+
133+
targetRef, err := name.ParseReference(dstImgRef)
134+
if err != nil {
135+
return fmt.Errorf("failed to parse target image reference: %w", err)
136+
}
137+
138+
dstSigRef := fmt.Sprintf("%s:%s", targetRef.Context().Name(), signatureTag)
139+
err = crane.Copy(sourceSigRef, dstSigRef,
140+
crane.WithContext(ctx),
141+
crane.WithNoClobber(true), // ensure existing signatures won't be overwritten.
142+
)
143+
if err != nil && !strings.Contains(err.Error(), "refusing to clobber existing tag") {
144+
return fmt.Errorf("failed to copy signature from %q to %q: %w", sourceSigRef, dstSigRef, err)
145+
}
146+
147+
return nil
148+
}

internal/imagelist/copier_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package imagelist
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-containerregistry/pkg/name"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestOverrideSignatureSource(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
image string
16+
want string
17+
}{
18+
{
19+
image: "rancher/rancher",
20+
want: "index.docker.io/rancher/rancher",
21+
},
22+
{
23+
image: "127.0.0.1:5000/sig-storage/snapshot-controller",
24+
want: "registry.k8s.io/sig-storage/snapshot-controller",
25+
},
26+
{
27+
image: "127.0.0.1:5000/sig-storage/snapshot-validation-webhook",
28+
want: "registry.k8s.io/sig-storage/snapshot-validation-webhook",
29+
},
30+
{
31+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-node-driver-registrar",
32+
want: "registry.k8s.io/sig-storage/csi-node-driver-registrar",
33+
},
34+
{
35+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-attacher",
36+
want: "registry.k8s.io/sig-storage/csi-attacher",
37+
},
38+
{
39+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-provisioner",
40+
want: "registry.k8s.io/sig-storage/csi-provisioner",
41+
},
42+
{
43+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-resizer",
44+
want: "registry.k8s.io/sig-storage/csi-resizer",
45+
},
46+
{
47+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-snapshotter",
48+
want: "registry.k8s.io/sig-storage/csi-snapshotter",
49+
},
50+
{
51+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-livenessprobe",
52+
want: "registry.k8s.io/sig-storage/rancher/mirrored-sig-storage-livenessprobe",
53+
},
54+
{
55+
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-snapshot-controller",
56+
want: "registry.k8s.io/sig-storage/snapshot-controller",
57+
},
58+
{
59+
image: "127.0.0.1:5000/rancher/mirrored-longhornio-csi-attacher",
60+
want: "registry.k8s.io/sig-storage/csi-attacher",
61+
},
62+
{
63+
image: "127.0.0.1:5000/rancher/appco-redis",
64+
want: "dp.apps.rancher.io/containers/redis",
65+
},
66+
{
67+
image: "127.0.0.1:5000/rancher/mirrored-cilium-cilium",
68+
want: "quay.io/cilium/cilium",
69+
},
70+
}
71+
72+
for _, tc := range tests {
73+
t.Run(tc.image, func(t *testing.T) {
74+
t.Parallel()
75+
76+
tag := "sha256-aabbeedd.sig"
77+
srcRef, err := name.ParseReference(tc.image + ":" + tag)
78+
require.NoError(t, err)
79+
80+
got := signatureSource(srcRef, tag)
81+
assert.Equal(t, tc.want+":"+tag, got)
82+
})
83+
}
84+
}

internal/imagelist/imagelist.go

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,29 @@ var (
2020

2121
const maxProcessingSizeInBytes = 5 * (1 << 20) // 5MB
2222

23+
type ImageVerifier interface {
24+
Verify(img string) Entry
25+
}
26+
27+
type ImageCopier interface {
28+
Copy(img, targetRegistry string) Entry
29+
}
30+
31+
type Result struct {
32+
Product string `json:"product,omitempty"`
33+
Version string `json:"version,omitempty"`
34+
Entries []Entry `json:"entries,omitempty"`
35+
}
36+
37+
type Entry struct {
38+
Image string `json:"image,omitempty"`
39+
Error error `json:"error,omitempty"`
40+
Signed bool `json:"signed,omitempty"`
41+
}
42+
2343
type Processor struct {
24-
ip ImageProcessor
44+
ip ImageVerifier
45+
copier ImageCopier
2546
fetcher Fetcher
2647
registry string
2748
}
@@ -31,14 +52,31 @@ func NewProcessor(registry string) *Processor {
3152
registry = registry + "/"
3253
}
3354

55+
copier := &imageCopier{
56+
mirroredOnly: true,
57+
}
58+
3459
return &Processor{
3560
registry: registry,
3661
ip: new(imageVerifier),
3762
fetcher: new(HttpFetcher),
63+
copier: copier,
3864
}
3965
}
4066

4167
func (p *Processor) Verify(url string) (*Result, error) {
68+
return p.process(url, "Verify images", "", func(img, _ string) Entry {
69+
return p.ip.Verify(img)
70+
})
71+
}
72+
73+
func (p *Processor) Copy(url, dstRegistry string) (*Result, error) {
74+
return p.process(url, "Copy images", dstRegistry, func(img, dstRegistry string) Entry {
75+
return p.copier.Copy(img, dstRegistry)
76+
})
77+
}
78+
79+
func (p *Processor) process(url, status, dstRegistry string, action func(string, string) Entry) (*Result, error) {
4280
url = strings.TrimSpace(url)
4381
if len(url) == 0 {
4482
return nil, ErrURLCannotBeEmpty
@@ -65,7 +103,7 @@ func (p *Processor) Verify(url string) (*Result, error) {
65103

66104
scanner := bufio.NewScanner(io.LimitReader(r, maxProcessingSizeInBytes))
67105

68-
s = spinner.New("Verify images")
106+
s = spinner.New(status)
69107
s.Start()
70108

71109
for scanner.Scan() {
@@ -88,7 +126,7 @@ func (p *Processor) Verify(url string) (*Result, error) {
88126

89127
s.UpdateStatus(image)
90128

91-
entry := p.ip.Verify(image)
129+
entry := action(image, dstRegistry)
92130

93131
result.Entries = append(result.Entries, entry)
94132
}
@@ -105,15 +143,3 @@ func (p *Processor) Verify(url string) (*Result, error) {
105143

106144
return &result, nil
107145
}
108-
109-
type Result struct {
110-
Product string `json:"product,omitempty"`
111-
Version string `json:"version,omitempty"`
112-
Entries []Entry `json:"entries,omitempty"`
113-
}
114-
115-
type Entry struct {
116-
Image string `json:"image,omitempty"`
117-
Error error `json:"error,omitempty"`
118-
Signed bool `json:"signed,omitempty"`
119-
}

internal/imagelist/verifier.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import (
77
"github.com/rancherlabs/slsactl/pkg/verify"
88
)
99

10-
type ImageProcessor interface {
11-
Verify(img string) Entry
12-
}
13-
1410
type imageVerifier struct {
1511
m sync.Mutex
1612
}

0 commit comments

Comments
 (0)