Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions cmd/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
)

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

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

return product.Verify(registry, nameVer[0], nameVer[1], true, true)
switch args[0] {
case "verify":
return product.Verify(registry, nameVer[0], nameVer[1], true, true)
case "copy":
if f.NArg() != 2 {
showProductUsage()
}

targetRegistry := f.Arg(1)
return product.Copy(registry, nameVer[0], nameVer[1], targetRegistry)
default:
showProductUsage()
}

return nil
}

func showProductUsage() {
Expand Down
148 changes: 148 additions & 0 deletions internal/imagelist/copier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package imagelist

import (
"context"
"errors"
"fmt"
"os"
"strings"
"sync"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
)

var externalImages = map[string]string{
"sig-storage/snapshot-controller": "registry.k8s.io/sig-storage/snapshot-controller",
"sig-storage/snapshot-validation-webhook": "registry.k8s.io/sig-storage/snapshot-validation-webhook",
"rancher/mirrored-sig-storage-csi-node-driver-registrar": "registry.k8s.io/sig-storage/csi-node-driver-registrar",
"rancher/mirrored-sig-storage-csi-attacher": "registry.k8s.io/sig-storage/csi-attacher",
"rancher/mirrored-longhornio-csi-attacher": "registry.k8s.io/sig-storage/csi-attacher",
"rancher/mirrored-sig-storage-csi-provisioner": "registry.k8s.io/sig-storage/csi-provisioner",
"rancher/mirrored-sig-storage-csi-resizer": "registry.k8s.io/sig-storage/csi-resizer",
"rancher/mirrored-sig-storage-csi-snapshotter": "registry.k8s.io/sig-storage/csi-snapshotter",
"rancher/mirrored-sig-storage-livenessprobe": "registry.k8s.io/sig-storage/rancher/mirrored-sig-storage-livenessprobe",
"rancher/mirrored-sig-storage-snapshot-controller": "registry.k8s.io/sig-storage/snapshot-controller",
"rancher/appco-redis": "dp.apps.rancher.io/containers/redis",
"rancher/mirrored-cilium-cilium": "quay.io/cilium/cilium",
"rancher/mirrored-cilium-cilium-envoy": "quay.io/cilium/cilium-envoy",
"rancher/mirrored-cilium-clustermesh-apiserver": "quay.io/cilium/clustermesh-apiserver",
"rancher/mirrored-cilium-hubble-relay": "quay.io/cilium/hubble-relay",
"rancher/mirrored-cilium-operator-aws": "quay.io/cilium/operator-aws",
"rancher/mirrored-cilium-operator-azure": "quay.io/cilium/operator-azure",
"rancher/mirrored-cilium-operator-generic": "quay.io/cilium/operator-generic",
"rancher/mirrored-kube-logging-config-reloader": "ghcr.io/kube-logging/config-reloader",
"rancher/mirrored-kube-logging-logging-operator": "ghcr.io/kube-logging/logging-operator",
"rancher/mirrored-kube-state-metrics-kube-state-metrics": "registry.k8s.io/kube-state-metrics/kube-state-metrics",
"rancher/mirrored-elemental-operator": "registry.suse.com/rancher/elemental-operator",
"rancher/mirrored-elemental-seedimage-builder": "registry.suse.com/rancher/seedimage-builder",
"rancher/mirrored-cluster-api-controller": "registry.k8s.io/cluster-api/cluster-api-controller",
}

type imageCopier struct {
m sync.Mutex
mirroredOnly bool
}

func (i *imageCopier) Copy(srcImg, dstRegistry string) Entry {
entry := Entry{
Image: srcImg,
}

if i.mirroredOnly && !strings.Contains(srcImg, "mirrored") {
entry.Error = errors.New("skipping non-mirrored image: " + srcImg)
return entry
}

ref, err := name.ParseReference(srcImg, name.WeakValidation)
if err != nil {
entry.Error = err
return entry
}

reg, err := name.NewRegistry(dstRegistry)
if err != nil {
entry.Error = err
return entry
}

repo := reg.Repo(ref.Context().RepositoryStr())
dst := repo.Tag(ref.Identifier()).String()

ctx := context.TODO()

// Reset stdout/stderr to avoid verbose output from cosign.
i.m.Lock()
stdout := os.Stdout
stderr := os.Stderr
os.Stdout = nil
os.Stderr = nil

// We shouldn't copy signatures if the image isn't there, so copy images
// but don't override them if they are already present.
err = copySignature(ctx, srcImg, dst, true)
if err != nil {
entry.Error = err
}
entry.Signed = (err == nil)

os.Stdout = stdout
os.Stderr = stderr
i.m.Unlock()

return entry
}

func signatureSource(srcRef name.Reference, tag string) string {
repo := srcRef.Context().RepositoryStr()
if upstream, found := externalImages[repo]; found {
return fmt.Sprintf("%s:%s", upstream, tag)
}

// Fully qualified reference: <registry>/<repository>:<signature_tag>
return fmt.Sprintf("%s:%s", srcRef.Context().Name(), tag)
}

func copySignature(ctx context.Context, srcImgRef, dstImgRef string, copyImage bool) error {
fmt.Println(srcImgRef, dstImgRef)
if copyImage {
err := crane.Copy(srcImgRef, dstImgRef,
crane.WithContext(ctx),
crane.WithNoClobber(true)) // ensure tags won't be overwritten.

if err != nil && !strings.Contains(err.Error(), "refusing to clobber existing tag") {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be replaced with errors.Is when upstream exposes this error.

return fmt.Errorf("failed to copy image from %q to %q: %w",
srcImgRef, dstImgRef, err)
}
}

digest, err := crane.Digest(srcImgRef)
if err != nil {
return fmt.Errorf("failed to get signed image digest for %q: %w", srcImgRef, err)
}

sourceRef, err := name.ParseReference(srcImgRef)
if err != nil {
return fmt.Errorf("failed to parse source image reference: %w", err)
}

hex := strings.TrimPrefix(digest, "sha256:")
signatureTag := fmt.Sprintf("sha256-%s.sig", hex)
sourceSigRef := signatureSource(sourceRef, signatureTag)

targetRef, err := name.ParseReference(dstImgRef)
if err != nil {
return fmt.Errorf("failed to parse target image reference: %w", err)
}

dstSigRef := fmt.Sprintf("%s:%s", targetRef.Context().Name(), signatureTag)
err = crane.Copy(sourceSigRef, dstSigRef,
crane.WithContext(ctx),
crane.WithNoClobber(true), // ensure existing signatures won't be overwritten.
)
if err != nil && !strings.Contains(err.Error(), "refusing to clobber existing tag") {
return fmt.Errorf("failed to copy signature from %q to %q: %w", sourceSigRef, dstSigRef, err)
}

return nil
}
84 changes: 84 additions & 0 deletions internal/imagelist/copier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package imagelist

import (
"testing"

"github.com/google/go-containerregistry/pkg/name"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestOverrideSignatureSource(t *testing.T) {
t.Parallel()

tests := []struct {
image string
want string
}{
{
image: "rancher/rancher",
want: "index.docker.io/rancher/rancher",
},
{
image: "127.0.0.1:5000/sig-storage/snapshot-controller",
want: "registry.k8s.io/sig-storage/snapshot-controller",
},
{
image: "127.0.0.1:5000/sig-storage/snapshot-validation-webhook",
want: "registry.k8s.io/sig-storage/snapshot-validation-webhook",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-node-driver-registrar",
want: "registry.k8s.io/sig-storage/csi-node-driver-registrar",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-attacher",
want: "registry.k8s.io/sig-storage/csi-attacher",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-provisioner",
want: "registry.k8s.io/sig-storage/csi-provisioner",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-resizer",
want: "registry.k8s.io/sig-storage/csi-resizer",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-csi-snapshotter",
want: "registry.k8s.io/sig-storage/csi-snapshotter",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-livenessprobe",
want: "registry.k8s.io/sig-storage/rancher/mirrored-sig-storage-livenessprobe",
},
{
image: "127.0.0.1:5000/rancher/mirrored-sig-storage-snapshot-controller",
want: "registry.k8s.io/sig-storage/snapshot-controller",
},
{
image: "127.0.0.1:5000/rancher/mirrored-longhornio-csi-attacher",
want: "registry.k8s.io/sig-storage/csi-attacher",
},
{
image: "127.0.0.1:5000/rancher/appco-redis",
want: "dp.apps.rancher.io/containers/redis",
},
{
image: "127.0.0.1:5000/rancher/mirrored-cilium-cilium",
want: "quay.io/cilium/cilium",
},
}

for _, tc := range tests {
t.Run(tc.image, func(t *testing.T) {
t.Parallel()

tag := "sha256-aabbeedd.sig"
srcRef, err := name.ParseReference(tc.image + ":" + tag)
require.NoError(t, err)

got := signatureSource(srcRef, tag)
assert.Equal(t, tc.want+":"+tag, got)
})
}
}
56 changes: 41 additions & 15 deletions internal/imagelist/imagelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,29 @@ var (

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

type ImageVerifier interface {
Verify(img string) Entry
}

type ImageCopier interface {
Copy(img, targetRegistry string) Entry
}

type Result struct {
Product string `json:"product,omitempty"`
Version string `json:"version,omitempty"`
Entries []Entry `json:"entries,omitempty"`
}

type Entry struct {
Image string `json:"image,omitempty"`
Error error `json:"error,omitempty"`
Signed bool `json:"signed,omitempty"`
}

type Processor struct {
ip ImageProcessor
ip ImageVerifier
copier ImageCopier
fetcher Fetcher
registry string
}
Expand All @@ -31,14 +52,31 @@ func NewProcessor(registry string) *Processor {
registry = registry + "/"
}

copier := &imageCopier{
mirroredOnly: true,
}

return &Processor{
registry: registry,
ip: new(imageVerifier),
fetcher: new(HttpFetcher),
copier: copier,
}
}

func (p *Processor) Verify(url string) (*Result, error) {
return p.process(url, "Verify images", "", func(img, _ string) Entry {
return p.ip.Verify(img)
})
}

func (p *Processor) Copy(url, dstRegistry string) (*Result, error) {
return p.process(url, "Copy images", dstRegistry, func(img, dstRegistry string) Entry {
return p.copier.Copy(img, dstRegistry)
})
}

func (p *Processor) process(url, status, dstRegistry string, action func(string, string) Entry) (*Result, error) {
url = strings.TrimSpace(url)
if len(url) == 0 {
return nil, ErrURLCannotBeEmpty
Expand All @@ -65,7 +103,7 @@ func (p *Processor) Verify(url string) (*Result, error) {

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

s = spinner.New("Verify images")
s = spinner.New(status)
s.Start()

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

s.UpdateStatus(image)

entry := p.ip.Verify(image)
entry := action(image, dstRegistry)

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

return &result, nil
}

type Result struct {
Product string `json:"product,omitempty"`
Version string `json:"version,omitempty"`
Entries []Entry `json:"entries,omitempty"`
}

type Entry struct {
Image string `json:"image,omitempty"`
Error error `json:"error,omitempty"`
Signed bool `json:"signed,omitempty"`
}
4 changes: 0 additions & 4 deletions internal/imagelist/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import (
"github.com/rancherlabs/slsactl/pkg/verify"
)

type ImageProcessor interface {
Verify(img string) Entry
}

type imageVerifier struct {
m sync.Mutex
}
Expand Down
Loading