Skip to content

Commit bdb68e3

Browse files
authored
Merge pull request #184 from rancherlabs/mirrored
2 parents 05fe2ba + d481c18 commit bdb68e3

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)