Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// The sync-containers command synchronizes container image tags from one// registry to another.//// It is intended as a workaround for ghcr.io's lack of good push credentials:// you can either authorize "classic" Personal Access Tokens in your org (which// are a common vector of very bad compromise), or you can get a short-lived// credential in a Github action.//// Since we publish to both Docker Hub and ghcr.io, we use this program in a// Github action to effectively rsync from docker hub into ghcr.io, so that we// can continue to forbid dangerous Personal Access Tokens in the tailscale org.package main
import ( "context" "flag" "fmt" "log" "sort" "strings"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn/github" "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/google/go-containerregistry/pkg/v1/types")
var ( src = flag.String("src", "", "Source image") dst = flag.String("dst", "", "Destination image") max = flag.Int("max", 0, "Maximum number of tags to sync (0 for all tags)") dryRun = flag.Bool("dry-run", true, "Don't actually sync anything"))
func main() { flag.Parse()
if *src == "" { log.Fatalf("--src is required") } if *dst == "" { log.Fatalf("--dst is required") }
keychain := authn.NewMultiKeychain(authn.DefaultKeychain, github.Keychain) opts := []remote.Option{ remote.WithAuthFromKeychain(keychain), remote.WithContext(context.Background()), }
stags, err := listTags(*src, opts...) if err != nil { log.Fatalf("listing source tags: %v", err) } dtags, err := listTags(*dst, opts...) if err != nil { log.Fatalf("listing destination tags: %v", err) }
add, remove := diffTags(stags, dtags) if ln := len(add); ln > 0 { log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", ")) if *max > 0 && ln > *max { log.Printf("Limiting sync to %d tags", *max) add = add[:*max] } } for _, tag := range add { if !*dryRun { log.Printf("Syncing tag %q", tag) if err := copyTag(*src, *dst, tag, opts...); err != nil { log.Printf("Syncing tag %q: progress error: %v", tag, err) } } else { log.Printf("Dry run: would sync tag %q", tag) } }
if len(remove) > 0 { log.Printf("%d tags to remove: %s\n", len(remove), strings.Join(remove, ", ")) log.Printf("Not removing any tags for safety.\n") }
var wellKnown = [...]string{"latest", "stable"} for _, tag := range wellKnown { if needsUpdate(*src, *dst, tag) { if err := copyTag(*src, *dst, tag, opts...); err != nil { log.Printf("Updating tag %q: progress error: %v", tag, err) } } }}
func copyTag(srcStr, dstStr, tag string, opts ...remote.Option) error { src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag)) if err != nil { return err } dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag)) if err != nil { return err }
desc, err := remote.Get(src) if err != nil { return err }
ch := make(chan v1.Update, 10) opts = append(opts, remote.WithProgress(ch)) progressDone := make(chan struct{})
go func() { defer close(progressDone) for p := range ch { fmt.Printf("Syncing tag %q: %d%% (%d/%d)\n", tag, int(float64(p.Complete)/float64(p.Total)*100), p.Complete, p.Total) if p.Error != nil { fmt.Printf("error: %v\n", p.Error) } } }()
switch desc.MediaType { case types.OCIManifestSchema1, types.DockerManifestSchema2: img, err := desc.Image() if err != nil { return err } if err := remote.Write(dst, img, opts...); err != nil { return err } case types.OCIImageIndex, types.DockerManifestList: idx, err := desc.ImageIndex() if err != nil { return err } if err := remote.WriteIndex(dst, idx, opts...); err != nil { return err } }
<-progressDone return nil}
func listTags(repoStr string, opts ...remote.Option) ([]string, error) { repo, err := name.NewRepository(repoStr) if err != nil { return nil, err }
tags, err := remote.List(repo, opts...) if err != nil { return nil, err }
sort.Strings(tags) return tags, nil}
func diffTags(src, dst []string) (add, remove []string) { srcd := make(map[string]bool) for _, tag := range src { srcd[tag] = true } dstd := make(map[string]bool) for _, tag := range dst { dstd[tag] = true }
for _, tag := range src { if !dstd[tag] { add = append(add, tag) } } for _, tag := range dst { if !srcd[tag] { remove = append(remove, tag) } } sort.Strings(add) sort.Strings(remove) return add, remove}
func needsUpdate(srcStr, dstStr, tag string) bool { src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag)) if err != nil { return false } dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag)) if err != nil { return false }
srcDesc, err := remote.Get(src) if err != nil { return false }
dstDesc, err := remote.Get(dst) if err != nil { return true }
return srcDesc.Digest != dstDesc.Digest}