mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	I'm not saying it works, but it compiles. Updates #5794 Change-Id: I2f3c99732e67fe57a05edb25b758d083417f083e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			215 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			215 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // 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 l := len(add); l > 0 {
 | |
| 		log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", "))
 | |
| 		if *max > 0 && l > *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
 | |
| }
 |