tooling/image-sync/internal/sync.go (193 lines of code) (raw):
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/types"
"go.uber.org/zap"
)
func Log() *zap.SugaredLogger {
return zap.L().Sugar()
}
// SyncConfig is the configuration for the image sync
type SyncConfig struct {
Repositories []string
NumberOfTags int
Secrets []Secrets
AcrTargetRegistry string
TenantId string
RequestTimeout int
AddLatest bool
ManagedIdentityClientID string
}
type Secrets struct {
Registry string
SecretFile string
}
// BearerSecret is the secret for the source OCI registry
type BearerSecret struct {
BearerToken string
}
// AzureSecret is the token configured in the ACR
type AzureSecretFile struct {
Username string
Password string
}
func (a AzureSecretFile) BasicAuthEncoded() string {
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", a.Username, a.Password)))
}
// Copy copies an image from one registry to another
func Copy(ctx context.Context, dstreference, srcreference string, dstauth, srcauth *types.DockerAuthConfig) error {
policyctx, err := signature.NewPolicyContext(&signature.Policy{
Default: signature.PolicyRequirements{
signature.NewPRInsecureAcceptAnything(),
},
})
if err != nil {
return err
}
src, err := docker.ParseReference("//" + srcreference)
if err != nil {
return err
}
dst, err := docker.ParseReference("//" + dstreference)
if err != nil {
return err
}
_, err = copy.Image(ctx, policyctx, dst, src, ©.Options{
SourceCtx: &types.SystemContext{
DockerAuthConfig: srcauth,
},
DestinationCtx: &types.SystemContext{
DockerAuthConfig: dstauth,
},
})
return err
}
func readBearerSecret(filename string) (*BearerSecret, error) {
secretBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var secret BearerSecret
err = json.Unmarshal(secretBytes, &secret)
if err != nil {
return nil, err
}
return &secret, nil
}
func readAzureSecret(filename string) (*AzureSecretFile, error) {
secretBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var secret AzureSecretFile
err = json.Unmarshal(secretBytes, &secret)
if err != nil {
return nil, err
}
return &secret, nil
}
func filterTagsToSync(src, target []string) []string {
var tagsToSync []string
targetMap := make(map[string]bool)
for _, targetTag := range target {
targetMap[targetTag] = true
}
for _, srcTag := range src {
if _, ok := targetMap[srcTag]; !ok {
tagsToSync = append(tagsToSync, srcTag)
}
}
return tagsToSync
}
// DoSync syncs the images from the source registry to the target registry
func DoSync(cfg *SyncConfig) error {
Log().Infow("Syncing images", "images", cfg.Repositories, "numberoftags", cfg.NumberOfTags)
ctx := context.Background()
srcRegistries := make(map[string]Registry)
var err error
for _, secret := range cfg.Secrets {
if secret.Registry == "quay.io" {
quaySecret, err := readBearerSecret(secret.SecretFile)
if err != nil {
return fmt.Errorf("error reading secret file: %w %s", err, secret.SecretFile)
}
qr := NewQuayRegistry(cfg, quaySecret.BearerToken)
srcRegistries[secret.Registry] = qr
} else {
if strings.HasSuffix(secret.Registry, "azurecr.io") ||
strings.HasSuffix(secret.Registry, "azurecr.cn") ||
strings.HasSuffix(secret.Registry, "azurecr.us") {
azureSecret, err := readAzureSecret(secret.SecretFile)
if err != nil {
return fmt.Errorf("error reading azure secret file: %w %s", err, secret.SecretFile)
}
bearerSecret, err := getACRBearerToken(ctx, *azureSecret, secret.Registry)
if err != nil {
return fmt.Errorf("error getting ACR bearer token: %w", err)
}
srcRegistries[secret.Registry] = NewACRWithTokenAuth(cfg, secret.Registry, bearerSecret)
} else {
s, err := readBearerSecret(secret.SecretFile)
bearerSecret := s.BearerToken
if err != nil {
return fmt.Errorf("error reading secret file: %w %s", err, secret.SecretFile)
}
srcRegistries[secret.Registry] = NewOCIRegistry(cfg, secret.Registry, bearerSecret)
}
}
}
targetACR := NewAzureContainerRegistry(cfg)
acrPullSecret, err := targetACR.GetPullSecret(ctx)
if err != nil {
return fmt.Errorf("error getting pull secret: %w", err)
}
targetACRAuth := types.DockerAuthConfig{Username: "00000000-0000-0000-0000-000000000000", Password: acrPullSecret.RefreshToken}
for _, repoName := range cfg.Repositories {
var srcTags, acrTags []string
baseURL := strings.Split(repoName, "/")[0]
repoName = strings.Join(strings.Split(repoName, "/")[1:], "/")
Log().Infow("Syncing repository", "repository", repoName, "baseurl", baseURL)
if client, ok := srcRegistries[baseURL]; ok {
srcTags, err = client.GetTags(ctx, repoName)
if err != nil {
return fmt.Errorf("error getting tags from %s: %w", baseURL, err)
}
Log().Debugw("Got tags from quay", "tags", srcTags)
} else {
// No secret defined, create a default client without auth
oci := NewOCIRegistry(cfg, baseURL, "")
srcTags, err = oci.GetTags(ctx, repoName)
if err != nil {
return fmt.Errorf("error getting oci tags: %w", err)
}
Log().Debugw(fmt.Sprintf("Got tags from %s", baseURL), "repo", repoName, "tags", srcTags)
}
exists, err := targetACR.RepositoryExists(ctx, repoName)
if err != nil {
return fmt.Errorf("error getting ACR repository information: %w", err)
}
if exists {
acrTags, err = targetACR.GetTags(ctx, repoName)
if err != nil {
return fmt.Errorf("error getting ACR tags: %w", err)
}
Log().Infow("Got tags from acr", "tags", acrTags)
} else {
Log().Infow("Repository does not exist", "repository", repoName)
}
tagsToSync := filterTagsToSync(srcTags, acrTags)
Log().Infow("Images to sync", "images", tagsToSync)
for _, tagToSync := range tagsToSync {
source := fmt.Sprintf("%s/%s:%s", baseURL, repoName, tagToSync)
target := fmt.Sprintf("%s/%s:%s", cfg.AcrTargetRegistry, repoName, tagToSync)
Log().Infow("Copying images", "images", tagToSync, "from", source, "to", target)
err = Copy(ctx, target, source, &targetACRAuth, nil)
if err != nil {
return fmt.Errorf("error copying image: %w", err)
}
}
}
return nil
}