mirror of
https://github.com/containers/skopeo.git
synced 2025-09-17 15:30:38 +00:00
Introduce the sync command
The skopeo sync command can sync images between a SOURCE and a destination. The purpose of this command is to assist with the mirroring of container images from different docker registries to a single docker registry. Right now the following source/destination locations are implemented: * docker -> docker * docker-> dir * dir -> docker The dir location is supported to handle the use case of air-gapped environments. In this context users can perform an initial sync on a trusted machine connected to the internet; that would be a `docker` -> `dir` sync. The target directory can be copied to a removable drive that can then be plugged into a node of the air-gapped environment. From there a `dir` -> `docker` sync will import all the images into the registry serving the air-gapped environment. Notes when specifying the `--scoped` option: The image namespace is changed during the `docker` to `docker` or `dir` copy. The FQDN of the registry hosting the image will be added as new root namespace of the image. For example, the image `registry.example.com/busybox:latest` will be copied to `registry.local.lan/registry.example.com/busybox:latest`. The image namespace is not changed when doing a `dir:` -> `docker` sync operation. The alteration of the image namespace is used to nicely scope images coming from different registries (the Docker Hub, quay.io, gcr, other registries). That allows all of them to be hosted on the same registry without incurring in clashes and making their origin explicit. Signed-off-by: Flavio Castelli <fcastelli@suse.com> Co-authored-by: Marco Vedovati <mvedovati@suse.com>
This commit is contained in:
committed by
Valentin Rothberg
parent
8ccd7b994d
commit
9c402f3799
@@ -98,6 +98,7 @@ func createApp() (*cli.App, *globalOptions) {
|
|||||||
layersCmd(&opts),
|
layersCmd(&opts),
|
||||||
deleteCmd(&opts),
|
deleteCmd(&opts),
|
||||||
manifestDigestCmd(),
|
manifestDigestCmd(),
|
||||||
|
syncCmd(&opts),
|
||||||
standaloneSignCmd(),
|
standaloneSignCmd(),
|
||||||
standaloneVerifyCmd(),
|
standaloneVerifyCmd(),
|
||||||
untrustedSignatureDumpCmd(),
|
untrustedSignatureDumpCmd(),
|
||||||
|
549
cmd/skopeo/sync.go
Normal file
549
cmd/skopeo/sync.go
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/copy"
|
||||||
|
"github.com/containers/image/v5/directory"
|
||||||
|
"github.com/containers/image/v5/docker"
|
||||||
|
"github.com/containers/image/v5/docker/reference"
|
||||||
|
"github.com/containers/image/v5/transports"
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// syncOptions contains information retrieved from the skopeo sync command line.
|
||||||
|
type syncOptions struct {
|
||||||
|
global *globalOptions // Global (not command dependant) skopeo options
|
||||||
|
srcImage *imageOptions // Source image options
|
||||||
|
destImage *imageDestOptions // Destination image options
|
||||||
|
removeSignatures bool // Do not copy signatures from the source image
|
||||||
|
signByFingerprint string // Sign the image using a GPG key with the specified fingerprint
|
||||||
|
source string // Source repository name
|
||||||
|
destination string // Destination registry name
|
||||||
|
scoped bool // When true, namespace copied images at destination using the source repository name
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoDescriptor contains information of a single repository used as a sync source.
|
||||||
|
type repoDescriptor struct {
|
||||||
|
DirBasePath string // base path when source is 'dir'
|
||||||
|
TaggedImages []types.ImageReference // List of tagged image found for the repository
|
||||||
|
Context *types.SystemContext // SystemContext for the sync command
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsVerify is an implementation of the Unmarshaler interface, used to
|
||||||
|
// customize the unmarshaling behaviour of the tls-verify YAML key.
|
||||||
|
type tlsVerifyConfig struct {
|
||||||
|
skip types.OptionalBool // skip TLS verification check (false by default)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registrySyncConfig contains information about a single registry, read from
|
||||||
|
// the source YAML file
|
||||||
|
type registrySyncConfig struct {
|
||||||
|
Images map[string][]string // Images map images name to slices with the images' tags
|
||||||
|
Credentials types.DockerAuthConfig // Username and password used to authenticate with the registry
|
||||||
|
TLSVerify tlsVerifyConfig `yaml:"tls-verify"` // TLS verification mode (enabled by default)
|
||||||
|
CertDir string `yaml:"cert-dir"` // Path to the TLS certificates of the registry
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceConfig contains all registries information read from the source YAML file
|
||||||
|
type sourceConfig map[string]registrySyncConfig
|
||||||
|
|
||||||
|
func syncCmd(global *globalOptions) cli.Command {
|
||||||
|
sharedFlags, sharedOpts := sharedImageFlags()
|
||||||
|
srcFlags, srcOpts := dockerImageFlags(global, sharedOpts, "src-", "screds")
|
||||||
|
destFlags, destOpts := dockerImageFlags(global, sharedOpts, "dest-", "dcreds")
|
||||||
|
|
||||||
|
opts := syncOptions{
|
||||||
|
global: global,
|
||||||
|
srcImage: srcOpts,
|
||||||
|
destImage: &imageDestOptions{imageOptions: destOpts},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.Command{
|
||||||
|
Name: "sync",
|
||||||
|
Usage: "Synchronize one or more images from one location to another",
|
||||||
|
Description: fmt.Sprint(`
|
||||||
|
|
||||||
|
Copy all the images from a SOURCE to a DESTINATION.
|
||||||
|
|
||||||
|
Allowed SOURCE transports (specified with --src): docker, dir, yaml.
|
||||||
|
Allowed DESTINATION transports (specified with --dest): docker, dir.
|
||||||
|
|
||||||
|
See skopeo-sync(1) for details.
|
||||||
|
`),
|
||||||
|
ArgsUsage: "--src SOURCE-LOCATION --dest DESTINATION-LOCATION SOURCE DESTINATION",
|
||||||
|
Action: commandAction(opts.run),
|
||||||
|
// FIXME: Do we need to namespace the GPG aspect?
|
||||||
|
Flags: append(append(append([]cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "remove-signatures",
|
||||||
|
Usage: "Do not copy signatures from SOURCE images",
|
||||||
|
Destination: &opts.removeSignatures,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "sign-by",
|
||||||
|
Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`",
|
||||||
|
Destination: &opts.signByFingerprint,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "src, s",
|
||||||
|
Usage: "SOURCE transport type",
|
||||||
|
Destination: &opts.source,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "dest, d",
|
||||||
|
Usage: "DESTINATION transport type",
|
||||||
|
Destination: &opts.destination,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "scoped",
|
||||||
|
Usage: "Images at DESTINATION are prefix using the full source image path as scope",
|
||||||
|
Destination: &opts.scoped,
|
||||||
|
},
|
||||||
|
}, sharedFlags...), srcFlags...), destFlags...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalYAML is the implementation of the Unmarshaler interface method
|
||||||
|
// method for the tlsVerifyConfig type.
|
||||||
|
// It unmarshals the 'tls-verify' YAML key so that, when they key is not
|
||||||
|
// specified, tls verification is enforced.
|
||||||
|
func (tls *tlsVerifyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var verify bool
|
||||||
|
if err := unmarshal(&verify); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tls.skip = types.NewOptionalBool(!verify)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSourceConfig unmarshals the provided YAML file path to the sourceConfig type.
|
||||||
|
// It returns a new unmarshaled sourceConfig object and any error encountered.
|
||||||
|
func newSourceConfig(yamlFile string) (sourceConfig, error) {
|
||||||
|
var cfg sourceConfig
|
||||||
|
source, err := ioutil.ReadFile(yamlFile)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(source, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, errors.Wrapf(err, "Failed to unmarshal %q", yamlFile)
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// destinationReference creates an image reference using the provided transport.
|
||||||
|
// It returns a image reference to be used as destination of an image copy and
|
||||||
|
// any error encountered.
|
||||||
|
func destinationReference(destination string, transport string) (types.ImageReference, error) {
|
||||||
|
var imageTransport types.ImageTransport
|
||||||
|
|
||||||
|
switch transport {
|
||||||
|
case docker.Transport.Name():
|
||||||
|
destination = fmt.Sprintf("//%s", destination)
|
||||||
|
imageTransport = docker.Transport
|
||||||
|
case directory.Transport.Name():
|
||||||
|
_, err := os.Stat(destination)
|
||||||
|
if err == nil {
|
||||||
|
return nil, errors.Errorf(fmt.Sprintf("Refusing to overwrite destination directory %q", destination))
|
||||||
|
}
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, errors.Wrap(err, "Destination directory could not be used")
|
||||||
|
}
|
||||||
|
// the directory holding the image must be created here
|
||||||
|
if err = os.MkdirAll(destination, 0755); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, fmt.Sprintf("Error creating directory for image %s",
|
||||||
|
destination))
|
||||||
|
}
|
||||||
|
imageTransport = directory.Transport
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("%q is not a valid destination transport", transport)
|
||||||
|
}
|
||||||
|
logrus.Debugf("Destination for transport %q: %s", transport, destination)
|
||||||
|
|
||||||
|
destRef, err := imageTransport.ParseReference(destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, fmt.Sprintf("Cannot obtain a valid image reference for transport %q and reference %q", imageTransport.Name(), destination))
|
||||||
|
}
|
||||||
|
|
||||||
|
return destRef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getImageTags retrieves all the tags associated to an image hosted on a
|
||||||
|
// container registry.
|
||||||
|
// It returns a string slice of tags and any error encountered.
|
||||||
|
func getImageTags(ctx context.Context, sysCtx *types.SystemContext, imgRef types.ImageReference) ([]string, error) {
|
||||||
|
name := imgRef.DockerReference().Name()
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"image": name,
|
||||||
|
}).Info("Getting tags")
|
||||||
|
tags, err := docker.GetRepositoryTags(ctx, sysCtx, imgRef)
|
||||||
|
|
||||||
|
switch err := err.(type) {
|
||||||
|
case nil:
|
||||||
|
break
|
||||||
|
case docker.ErrUnauthorizedForCredentials:
|
||||||
|
// Some registries may decide to block the "list all tags" endpoint.
|
||||||
|
// Gracefully allow the sync to continue in this case.
|
||||||
|
logrus.Warnf("Registry disallows tag list retrieval: %s", err)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return tags, errors.Wrapf(err, fmt.Sprintf("Error determining repository tags for image %s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTagSpecified checks if an image name includes a tag and returns any errors
|
||||||
|
// encountered.
|
||||||
|
func isTagSpecified(imageName string) (bool, error) {
|
||||||
|
normNamed, err := reference.ParseNormalizedNamed(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tagged := !reference.IsNameOnly(normNamed)
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"imagename": imageName,
|
||||||
|
"tagged": tagged,
|
||||||
|
}).Info("Tag presence check")
|
||||||
|
return tagged, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagesTopCopyFromRepo builds a list of image references from the tags
|
||||||
|
// found in the source repository.
|
||||||
|
// It returns an image reference slice with as many elements as the tags found
|
||||||
|
// and any error encountered.
|
||||||
|
func imagesToCopyFromRepo(repoReference types.ImageReference, repoName string, sourceCtx *types.SystemContext) ([]types.ImageReference, error) {
|
||||||
|
var sourceReferences []types.ImageReference
|
||||||
|
tags, err := getImageTags(context.Background(), sourceCtx, repoReference)
|
||||||
|
if err != nil {
|
||||||
|
return sourceReferences, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
imageAndTag := fmt.Sprintf("%s:%s", repoName, tag)
|
||||||
|
ref, err := docker.ParseReference(imageAndTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, fmt.Sprintf("Cannot obtain a valid image reference for transport %q and reference %q", docker.Transport.Name(), imageAndTag))
|
||||||
|
}
|
||||||
|
sourceReferences = append(sourceReferences, ref)
|
||||||
|
}
|
||||||
|
return sourceReferences, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagesTopCopyFromDir builds a list of image references from the images found
|
||||||
|
// in the source directory.
|
||||||
|
// It returns an image reference slice with as many elements as the images found
|
||||||
|
// and any error encountered.
|
||||||
|
func imagesToCopyFromDir(dirPath string) ([]types.ImageReference, error) {
|
||||||
|
var sourceReferences []types.ImageReference
|
||||||
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && info.Name() == "manifest.json" {
|
||||||
|
dirname := filepath.Dir(path)
|
||||||
|
ref, err := directory.Transport.ParseReference(dirname)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, fmt.Sprintf("Cannot obtain a valid image reference for transport %q and reference %q", directory.Transport.Name(), dirname))
|
||||||
|
}
|
||||||
|
sourceReferences = append(sourceReferences, ref)
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return sourceReferences,
|
||||||
|
errors.Wrapf(err, fmt.Sprintf("Error walking the path %q", dirPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceReferences, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagesTopCopyFromDir builds a list of repository descriptors from the images
|
||||||
|
// in a registry configuration.
|
||||||
|
// It returns a repository descriptors slice with as many elements as the images
|
||||||
|
// found and any error encountered. Each element of the slice is a list of
|
||||||
|
// tagged image references, to be used as sync source.
|
||||||
|
func imagesToCopyFromRegistry(registryName string, cfg registrySyncConfig, sourceCtx types.SystemContext) ([]repoDescriptor, error) {
|
||||||
|
var repoDescList []repoDescriptor
|
||||||
|
for imageName, tags := range cfg.Images {
|
||||||
|
repoName := fmt.Sprintf("//%s", path.Join(registryName, imageName))
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"repo": imageName,
|
||||||
|
"registry": registryName,
|
||||||
|
}).Info("Processing repo")
|
||||||
|
|
||||||
|
serverCtx := &sourceCtx
|
||||||
|
// override ctx with per-registryName options
|
||||||
|
serverCtx.DockerCertPath = cfg.CertDir
|
||||||
|
serverCtx.DockerDaemonCertPath = cfg.CertDir
|
||||||
|
serverCtx.DockerDaemonInsecureSkipTLSVerify = (cfg.TLSVerify.skip == types.OptionalBoolTrue)
|
||||||
|
serverCtx.DockerInsecureSkipTLSVerify = cfg.TLSVerify.skip
|
||||||
|
serverCtx.DockerAuthConfig = &cfg.Credentials
|
||||||
|
|
||||||
|
var sourceReferences []types.ImageReference
|
||||||
|
for _, tag := range tags {
|
||||||
|
source := fmt.Sprintf("%s:%s", repoName, tag)
|
||||||
|
|
||||||
|
imageRef, err := docker.ParseReference(source)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"tag": source,
|
||||||
|
}).Error("Error processing tag, skipping")
|
||||||
|
logrus.Errorf("Error getting image reference: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sourceReferences = append(sourceReferences, imageRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"repo": imageName,
|
||||||
|
"registry": registryName,
|
||||||
|
}).Info("Querying registry for image tags")
|
||||||
|
|
||||||
|
imageRef, err := docker.ParseReference(repoName)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"repo": imageName,
|
||||||
|
"registry": registryName,
|
||||||
|
}).Error("Error processing repo, skipping")
|
||||||
|
logrus.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceReferences, err = imagesToCopyFromRepo(imageRef, repoName, serverCtx)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"repo": imageName,
|
||||||
|
"registry": registryName,
|
||||||
|
}).Error("Error processing repo, skipping")
|
||||||
|
logrus.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sourceReferences) == 0 {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"repo": imageName,
|
||||||
|
"registry": registryName,
|
||||||
|
}).Warnf("No tags to sync found")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
repoDescList = append(repoDescList, repoDescriptor{
|
||||||
|
TaggedImages: sourceReferences,
|
||||||
|
Context: serverCtx})
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoDescList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imagesToCopy retrieves all the images to copy from a specified sync source
|
||||||
|
// and transport.
|
||||||
|
// It returns a slice of repository descriptors, where each descriptor is a
|
||||||
|
// list of tagged image references to be used as sync source, and any error
|
||||||
|
// encountered.
|
||||||
|
func imagesToCopy(source string, transport string, sourceCtx *types.SystemContext) ([]repoDescriptor, error) {
|
||||||
|
var descriptors []repoDescriptor
|
||||||
|
|
||||||
|
switch transport {
|
||||||
|
case docker.Transport.Name():
|
||||||
|
desc := repoDescriptor{
|
||||||
|
Context: sourceCtx,
|
||||||
|
}
|
||||||
|
refName := fmt.Sprintf("//%s", source)
|
||||||
|
srcRef, err := docker.ParseReference(refName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, fmt.Sprintf("Cannot obtain a valid image reference for transport %q and reference %q", docker.Transport.Name(), refName))
|
||||||
|
}
|
||||||
|
imageTagged, err := isTagSpecified(source)
|
||||||
|
if err != nil {
|
||||||
|
return descriptors, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageTagged {
|
||||||
|
desc.TaggedImages = append(desc.TaggedImages, srcRef)
|
||||||
|
descriptors = append(descriptors, desc)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
desc.TaggedImages, err = imagesToCopyFromRepo(
|
||||||
|
srcRef,
|
||||||
|
fmt.Sprintf("//%s", source),
|
||||||
|
sourceCtx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return descriptors, err
|
||||||
|
}
|
||||||
|
if len(desc.TaggedImages) == 0 {
|
||||||
|
return descriptors, errors.Errorf("No images to sync found in %q", source)
|
||||||
|
}
|
||||||
|
descriptors = append(descriptors, desc)
|
||||||
|
|
||||||
|
case directory.Transport.Name():
|
||||||
|
desc := repoDescriptor{
|
||||||
|
Context: sourceCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
return descriptors, errors.Wrap(err, "Invalid source directory specified")
|
||||||
|
}
|
||||||
|
desc.DirBasePath = source
|
||||||
|
var err error
|
||||||
|
desc.TaggedImages, err = imagesToCopyFromDir(source)
|
||||||
|
if err != nil {
|
||||||
|
return descriptors, err
|
||||||
|
}
|
||||||
|
if len(desc.TaggedImages) == 0 {
|
||||||
|
return descriptors, errors.Errorf("No images to sync found in %q", source)
|
||||||
|
}
|
||||||
|
descriptors = append(descriptors, desc)
|
||||||
|
|
||||||
|
case "yaml":
|
||||||
|
cfg, err := newSourceConfig(source)
|
||||||
|
if err != nil {
|
||||||
|
return descriptors, err
|
||||||
|
}
|
||||||
|
for registryName, registryConfig := range cfg {
|
||||||
|
if len(registryConfig.Images) == 0 {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"registry": registryName,
|
||||||
|
}).Warn("No images specified for registry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
descs, err := imagesToCopyFromRegistry(registryName, registryConfig, *sourceCtx)
|
||||||
|
if err != nil {
|
||||||
|
return descriptors, errors.Wrapf(err, "Failed to retrieve list of images from registry %q", registryName)
|
||||||
|
}
|
||||||
|
descriptors = append(descriptors, descs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *syncOptions) run(args []string, stdout io.Writer) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")}
|
||||||
|
}
|
||||||
|
|
||||||
|
policyContext, err := opts.global.getPolicyContext()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error loading trust policy")
|
||||||
|
}
|
||||||
|
defer policyContext.Destroy()
|
||||||
|
|
||||||
|
// validate source and destination options
|
||||||
|
contains := func(val string, list []string) (_ bool) {
|
||||||
|
for _, l := range list {
|
||||||
|
if l == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.source) == 0 {
|
||||||
|
return errors.New("A source transport must be specified")
|
||||||
|
}
|
||||||
|
if !contains(opts.source, []string{docker.Transport.Name(), directory.Transport.Name(), "yaml"}) {
|
||||||
|
return errors.Errorf("%q is not a valid source transport", opts.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.destination) == 0 {
|
||||||
|
return errors.New("A destination transport must be specified")
|
||||||
|
}
|
||||||
|
if !contains(opts.destination, []string{docker.Transport.Name(), directory.Transport.Name()}) {
|
||||||
|
return errors.Errorf("%q is not a valid destination transport", opts.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.source == opts.destination && opts.source == directory.Transport.Name() {
|
||||||
|
return errors.New("sync from 'dir' to 'dir' not implemented, consider using rsync instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCtx, err := opts.srcImage.newSystemContext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceArg := args[0]
|
||||||
|
srcRepoList, err := imagesToCopy(sourceArg, opts.source, sourceCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
destination := args[1]
|
||||||
|
destinationCtx, err := opts.destImage.newSystemContext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := opts.global.commandTimeoutContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
imagesNumber := 0
|
||||||
|
options := copy.Options{
|
||||||
|
RemoveSignatures: opts.removeSignatures,
|
||||||
|
SignBy: opts.signByFingerprint,
|
||||||
|
ReportWriter: os.Stdout,
|
||||||
|
DestinationCtx: destinationCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, srcRepo := range srcRepoList {
|
||||||
|
options.SourceCtx = srcRepo.Context
|
||||||
|
for counter, ref := range srcRepo.TaggedImages {
|
||||||
|
var destSuffix string
|
||||||
|
switch ref.Transport() {
|
||||||
|
case docker.Transport:
|
||||||
|
// docker -> dir or docker -> docker
|
||||||
|
destSuffix = ref.DockerReference().String()
|
||||||
|
case directory.Transport:
|
||||||
|
// dir -> docker (we don't allow `dir` -> `dir` sync operations)
|
||||||
|
destSuffix = strings.TrimPrefix(ref.StringWithinTransport(), srcRepo.DirBasePath)
|
||||||
|
if destSuffix == "" {
|
||||||
|
// if source is a full path to an image, have destPath scoped to repo:tag
|
||||||
|
destSuffix = path.Base(srcRepo.DirBasePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.scoped {
|
||||||
|
destSuffix = path.Base(destSuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
destRef, err := destinationReference(path.Join(destination, destSuffix), opts.destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"from": transports.ImageName(ref),
|
||||||
|
"to": transports.ImageName(destRef),
|
||||||
|
}).Infof("Copying image tag %d/%d", counter+1, len(srcRepo.TaggedImages))
|
||||||
|
|
||||||
|
_, err = copy.Image(ctx, policyContext, destRef, ref, &options)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, fmt.Sprintf("Error copying tag %q", transports.ImageName(ref)))
|
||||||
|
}
|
||||||
|
imagesNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("Synced %d images from %d sources", imagesNumber, len(srcRepoList))
|
||||||
|
return nil
|
||||||
|
}
|
@@ -2,13 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/image/v5/pkg/compression"
|
"github.com/containers/image/v5/pkg/compression"
|
||||||
"github.com/containers/image/v5/transports/alltransports"
|
"github.com/containers/image/v5/transports/alltransports"
|
||||||
"github.com/containers/image/v5/types"
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,24 +50,34 @@ func sharedImageFlags() ([]cli.Flag, *sharedImageOptions) {
|
|||||||
}, &opts
|
}, &opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageOptions collects CLI flags specific to the "docker" transport, which are
|
||||||
|
// the same across subcommands, but may be different for each image
|
||||||
|
// (e.g. may differ between the source and destination of a copy)
|
||||||
|
type dockerImageOptions struct {
|
||||||
|
global *globalOptions // May be shared across several imageOptions instances.
|
||||||
|
shared *sharedImageOptions // May be shared across several imageOptions instances.
|
||||||
|
credsOption optionalString // username[:password] for accessing a registry
|
||||||
|
dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon
|
||||||
|
tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:)
|
||||||
|
noCreds bool // Access the registry anonymously
|
||||||
|
}
|
||||||
|
|
||||||
// imageOptions collects CLI flags which are the same across subcommands, but may be different for each image
|
// imageOptions collects CLI flags which are the same across subcommands, but may be different for each image
|
||||||
// (e.g. may differ between the source and destination of a copy)
|
// (e.g. may differ between the source and destination of a copy)
|
||||||
type imageOptions struct {
|
type imageOptions struct {
|
||||||
global *globalOptions // May be shared across several imageOptions instances.
|
dockerImageOptions
|
||||||
shared *sharedImageOptions // May be shared across several imageOptions instances.
|
sharedBlobDir string // A directory to use for OCI blobs, shared across repositories
|
||||||
credsOption optionalString // username[:password] for accessing a registry
|
dockerDaemonHost string // docker-daemon: host to connect to
|
||||||
dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon
|
|
||||||
tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:)
|
|
||||||
sharedBlobDir string // A directory to use for OCI blobs, shared across repositories
|
|
||||||
dockerDaemonHost string // docker-daemon: host to connect to
|
|
||||||
noCreds bool // Access the registry anonymously
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure.
|
// dockerImageFlags prepares a collection of docker-transport specific CLI flags
|
||||||
func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) {
|
// writing into imageOptions, and the managed imageOptions structure.
|
||||||
|
func dockerImageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) {
|
||||||
opts := imageOptions{
|
opts := imageOptions{
|
||||||
global: global,
|
dockerImageOptions: dockerImageOptions{
|
||||||
shared: shared,
|
global: global,
|
||||||
|
shared: shared,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility.
|
// This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility.
|
||||||
@@ -93,6 +103,19 @@ func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, c
|
|||||||
Usage: "require HTTPS and verify certificates when talking to the container registry or daemon (defaults to true)",
|
Usage: "require HTTPS and verify certificates when talking to the container registry or daemon (defaults to true)",
|
||||||
Value: newOptionalBoolValue(&opts.tlsVerify),
|
Value: newOptionalBoolValue(&opts.tlsVerify),
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: flagPrefix + "no-creds",
|
||||||
|
Usage: "Access the registry anonymously",
|
||||||
|
Destination: &opts.noCreds,
|
||||||
|
},
|
||||||
|
}, &opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure.
|
||||||
|
func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) {
|
||||||
|
dockerFlags, opts := dockerImageFlags(global, shared, flagPrefix, credsOptionAlias)
|
||||||
|
|
||||||
|
return append(dockerFlags, []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: flagPrefix + "shared-blob-dir",
|
Name: flagPrefix + "shared-blob-dir",
|
||||||
Usage: "`DIRECTORY` to use to share blobs across OCI repositories",
|
Usage: "`DIRECTORY` to use to share blobs across OCI repositories",
|
||||||
@@ -103,12 +126,7 @@ func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, c
|
|||||||
Usage: "use docker daemon host at `HOST` (docker-daemon: only)",
|
Usage: "use docker daemon host at `HOST` (docker-daemon: only)",
|
||||||
Destination: &opts.dockerDaemonHost,
|
Destination: &opts.dockerDaemonHost,
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
}...), opts
|
||||||
Name: flagPrefix + "no-creds",
|
|
||||||
Usage: "Access the registry anonymously",
|
|
||||||
Destination: &opts.noCreds,
|
|
||||||
},
|
|
||||||
}, &opts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
||||||
|
142
docs/skopeo-sync.1.md
Normal file
142
docs/skopeo-sync.1.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
% skopeo-sync(1)
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
skopeo\-sync - Synchronize images between container registries and local directories.
|
||||||
|
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**skopeo sync** --src _transport_ --dest _transport_ _source_ _destination_
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
Synchronize images between container registries and local directories.
|
||||||
|
The synchronization is achieved by copying all the images found at _source_ to _destination_.
|
||||||
|
|
||||||
|
Useful to synchronize a local container registry mirror, and to to populate registries running inside of air-gapped environments.
|
||||||
|
|
||||||
|
Differently from other skopeo commands, skopeo sync requires both source and destination transports to be specified separately from _source_ and _destination_.
|
||||||
|
One of the problems of prefixing a destination with its transport is that, the registry `docker://hostname:port` would be wrongly interpreted as an image reference at a non-fully qualified registry, with `hostname` and `port` the image name and tag.
|
||||||
|
|
||||||
|
Available _source_ transports:
|
||||||
|
- _docker_ (i.e. `--src docker`): _source_ is a repository hosted on a container registry (e.g.: `registry.example.com/busybox`).
|
||||||
|
If no image tag is specified, skopeo sync copies all the tags found in that repository.
|
||||||
|
- _dir_ (i.e. `--src dir`): _source_ is a local directory path (e.g.: `/media/usb/`). Refer to skopeo(1) **dir:**_path_ for the local image format.
|
||||||
|
- _yaml_ (i.e. `--src yaml`): _source_ is local YAML file path.
|
||||||
|
The YAML file should specify the list of images copied from different container registries (local directories are not supported). Refer to EXAMPLES for the file format.
|
||||||
|
|
||||||
|
Available _destination_ transports:
|
||||||
|
- _docker_ (i.e. `--dest docker`): _destination_ is a container registry (e.g.: `my-registry.local.lan`).
|
||||||
|
- _dir_ (i.e. `--dest dir`): _destination_ is a local directory path (e.g.: `/media/usb/`).
|
||||||
|
One directory per source 'image:tag' is created for each copied image.
|
||||||
|
|
||||||
|
When the `--scoped` option is specified, images are prefixed with the source image path so that multiple images with the same
|
||||||
|
name can be stored at _destination_.
|
||||||
|
|
||||||
|
## OPTIONS
|
||||||
|
**--authfile** _path_
|
||||||
|
|
||||||
|
Path of the authentication file. Default is ${XDG\_RUNTIME\_DIR}/containers/auth.json, which is set using `podman login`.
|
||||||
|
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
|
||||||
|
|
||||||
|
**--src** _transport_ Transport for the source repository.
|
||||||
|
|
||||||
|
**--dest** _transport_ Destination transport.
|
||||||
|
|
||||||
|
**--scoped** Prefix images with the source image path, so that multiple images with the same name can be stored at _destination_.
|
||||||
|
|
||||||
|
**--remove-signatures** Do not copy signatures, if any, from _source-image_. This is necessary when copying a signed image to a destination which does not support signatures.
|
||||||
|
|
||||||
|
**--sign-by=**_key-id_ Add a signature using that key ID for an image name corresponding to _destination-image_.
|
||||||
|
|
||||||
|
**--src-creds** _username[:password]_ for accessing the source registry.
|
||||||
|
|
||||||
|
**--dest-creds** _username[:password]_ for accessing the destination registry.
|
||||||
|
|
||||||
|
**--src-cert-dir** _path_ Use certificates (*.crt, *.cert, *.key) at _path_ to connect to the source registry or daemon.
|
||||||
|
|
||||||
|
**--src-no-creds** _bool-value_ Access the registry anonymously.
|
||||||
|
|
||||||
|
**--src-tls-verify** _bool-value_ Require HTTPS and verify certificates when talking to a container source registry or daemon (defaults to true).
|
||||||
|
|
||||||
|
**--dest-cert-dir** _path_ Use certificates (*.crt, *.cert, *.key) at _path_ to connect to the destination registry or daemon.
|
||||||
|
|
||||||
|
**--dest-no-creds** _bool-value_ Access the registry anonymously.
|
||||||
|
|
||||||
|
**--dest-tls-verify** _bool-value_ Require HTTPS and verify certificates when talking to a container destination registry or daemon (defaults to true).
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
### Synchronizing to a local directory
|
||||||
|
```
|
||||||
|
$ skopeo sync --src docker --dest dir registry.example.com/busybox /media/usb
|
||||||
|
```
|
||||||
|
Images are located at:
|
||||||
|
```
|
||||||
|
/media/usb/busybox:1-glibc
|
||||||
|
/media/usb/busybox:1-musl
|
||||||
|
/media/usb/busybox:1-ubuntu
|
||||||
|
...
|
||||||
|
/media/usb/busybox:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronizing to a local directory, scoped
|
||||||
|
```
|
||||||
|
$ skopeo sync --src docker --dest dir --scoped registry.example.com/busybox /media/usb
|
||||||
|
```
|
||||||
|
Images are located at:
|
||||||
|
```
|
||||||
|
/media/usb/registry.example.com/busybox:1-glibc
|
||||||
|
/media/usb/registry.example.com/busybox:1-musl
|
||||||
|
/media/usb/registry.example.com/busybox:1-ubuntu
|
||||||
|
...
|
||||||
|
/media/usb/registry.example.com/busybox:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronizing to a container registry
|
||||||
|
```
|
||||||
|
skopeo sync --src docker --dest docker registry.example.com/busybox my-registry.local.lan
|
||||||
|
```
|
||||||
|
Destination registry content:
|
||||||
|
```
|
||||||
|
REPO TAGS
|
||||||
|
registry.example.com/busybox 1-glibc, 1-musl, 1-ubuntu, ..., latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### YAML file content (used _source_ for `**--src yaml**`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
registry.example.com:
|
||||||
|
images:
|
||||||
|
busybox: []
|
||||||
|
redis:
|
||||||
|
- "1.0"
|
||||||
|
- "2.0"
|
||||||
|
credentials:
|
||||||
|
username: john
|
||||||
|
password: this is a secret
|
||||||
|
tls-verify: true
|
||||||
|
cert-dir: /home/john/certs
|
||||||
|
quay.io:
|
||||||
|
tls-verify: false
|
||||||
|
images:
|
||||||
|
coreos/etcd:
|
||||||
|
- latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will copy the following images:
|
||||||
|
- Repository `registry.example.com/busybox`: all images, as no tags are specified.
|
||||||
|
- Repository `registry.example.com/redis`: images tagged "1.0" and "2.0".
|
||||||
|
- Repository `quay.io/coreos/etcd`: images tagged "latest".
|
||||||
|
|
||||||
|
For the registry `registry.example.com`, the "john"/"this is a secret" credentials are used, with server TLS certificates located at `/home/john/certs`.
|
||||||
|
|
||||||
|
TLS verification is normally enabled, and it can be disabled setting `tls-verify` to `true`.
|
||||||
|
In the above example, TLS verification is enabled for `reigstry.example.com`, while is
|
||||||
|
disabled for `quay.io`.
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
skopeo(1), podman-login(1), docker-login(1)
|
||||||
|
|
||||||
|
## AUTHORS
|
||||||
|
|
||||||
|
Flavio Castelli <fcastelli@suse.com>, Marco Vedovati <mvedovati@suse.com>
|
||||||
|
|
@@ -74,6 +74,7 @@ Most commands refer to container images, using a _transport_`:`_details_ format.
|
|||||||
| [skopeo-manifest-digest(1)](skopeo-manifest-digest.1.md) | Compute a manifest digest of manifest-file and write it to standard output.|
|
| [skopeo-manifest-digest(1)](skopeo-manifest-digest.1.md) | Compute a manifest digest of manifest-file and write it to standard output.|
|
||||||
| [skopeo-standalone-sign(1)](skopeo-standalone-sign.1.md) | Sign an image. |
|
| [skopeo-standalone-sign(1)](skopeo-standalone-sign.1.md) | Sign an image. |
|
||||||
| [skopeo-standalone-verify(1)](skopeo-standalone-verify.1.md)| Verify an image. |
|
| [skopeo-standalone-verify(1)](skopeo-standalone-verify.1.md)| Verify an image. |
|
||||||
|
| [skopeo-sync(1)](skopeo-sync.1.md)| Copy images from one or more repositories to a user specified destination. |
|
||||||
|
|
||||||
## FILES
|
## FILES
|
||||||
**/etc/containers/policy.json**
|
**/etc/containers/policy.json**
|
||||||
|
1
go.mod
1
go.mod
@@ -21,4 +21,5 @@ require (
|
|||||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2
|
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2
|
||||||
github.com/urfave/cli v1.22.1
|
github.com/urfave/cli v1.22.1
|
||||||
go4.org v0.0.0-20190218023631-ce4c26f7be8e // indirect
|
go4.org v0.0.0-20190218023631-ce4c26f7be8e // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
)
|
)
|
||||||
|
514
integration/sync_test.go
Normal file
514
integration/sync_test.go
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/docker"
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/go-check/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
check.Suite(&SyncSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncSuite struct {
|
||||||
|
cluster *openshiftCluster
|
||||||
|
registry *testRegistryV2
|
||||||
|
gpgHome string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) SetUpSuite(c *check.C) {
|
||||||
|
const registryAuth = false
|
||||||
|
const registrySchema1 = false
|
||||||
|
|
||||||
|
if os.Getenv("SKOPEO_LOCAL_TESTS") == "1" {
|
||||||
|
c.Log("Running tests without a container")
|
||||||
|
fmt.Printf("NOTE: tests requires a V2 registry at url=%s, with auth=%t, schema1=%t \n", v2DockerRegistryURL, registryAuth, registrySchema1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SKOPEO_CONTAINER_TESTS") != "1" {
|
||||||
|
c.Skip("Not running in a container, refusing to affect user state")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.cluster = startOpenshiftCluster(c) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
|
||||||
|
|
||||||
|
for _, stream := range []string{"unsigned", "personal", "official", "naming", "cosigned", "compression", "schema1", "schema2"} {
|
||||||
|
isJSON := fmt.Sprintf(`{
|
||||||
|
"kind": "ImageStream",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": {
|
||||||
|
"name": "%s"
|
||||||
|
},
|
||||||
|
"spec": {}
|
||||||
|
}`, stream)
|
||||||
|
runCommandWithInput(c, isJSON, "oc", "create", "-f", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
|
||||||
|
s.registry = setupRegistryV2At(c, v2DockerRegistryURL, registryAuth, registrySchema1)
|
||||||
|
|
||||||
|
gpgHome, err := ioutil.TempDir("", "skopeo-gpg")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
s.gpgHome = gpgHome
|
||||||
|
os.Setenv("GNUPGHOME", s.gpgHome)
|
||||||
|
|
||||||
|
for _, key := range []string{"personal", "official"} {
|
||||||
|
batchInput := fmt.Sprintf("Key-Type: RSA\nName-Real: Test key - %s\nName-email: %s@example.com\n%%no-protection\n%%commit\n",
|
||||||
|
key, key)
|
||||||
|
runCommandWithInput(c, batchInput, gpgBinary, "--batch", "--gen-key")
|
||||||
|
|
||||||
|
out := combinedOutputOfCommand(c, gpgBinary, "--armor", "--export", fmt.Sprintf("%s@example.com", key))
|
||||||
|
err := ioutil.WriteFile(filepath.Join(s.gpgHome, fmt.Sprintf("%s-pubkey.gpg", key)),
|
||||||
|
[]byte(out), 0600)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TearDownSuite(c *check.C) {
|
||||||
|
if os.Getenv("SKOPEO_LOCAL_TESTS") == "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.gpgHome != "" {
|
||||||
|
os.RemoveAll(s.gpgHome)
|
||||||
|
}
|
||||||
|
if s.registry != nil {
|
||||||
|
s.registry.Close()
|
||||||
|
}
|
||||||
|
if s.cluster != nil {
|
||||||
|
s.cluster.tearDown(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestDocker2DirTagged(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "busybox:latest"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().String()
|
||||||
|
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
dir2 := path.Join(tmpDir, "dir2")
|
||||||
|
|
||||||
|
// sync docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// copy docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "docker://"+image, "dir:"+dir2)
|
||||||
|
_, err = os.Stat(path.Join(dir2, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
out := combinedOutputOfCommand(c, "diff", "-urN", path.Join(dir1, imagePath), dir2)
|
||||||
|
c.Assert(out, check.Equals, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestScoped(c *check.C) {
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "busybox:latest"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().String()
|
||||||
|
|
||||||
|
dir1, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--src", "docker", "--dest", "dir", image, dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, image, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
os.RemoveAll(dir1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestDirIsNotOverwritten(c *check.C) {
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "busybox:latest"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().String()
|
||||||
|
|
||||||
|
// make a copy of the image in the local registry
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "docker://"+image, "docker://"+path.Join(v2DockerRegistryURL, image))
|
||||||
|
|
||||||
|
//sync upstream image to dir, not scoped
|
||||||
|
dir1, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--src", "docker", "--dest", "dir", image, dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, image, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
//sync local registry image to dir, not scoped
|
||||||
|
assertSkopeoFails(c, ".*Refusing to overwrite destination directory.*", "sync", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", path.Join(v2DockerRegistryURL, image), dir1)
|
||||||
|
|
||||||
|
//sync local registry image to dir, scoped
|
||||||
|
imageRef, err = docker.ParseReference(fmt.Sprintf("//%s", path.Join(v2DockerRegistryURL, image)))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath = imageRef.DockerReference().String()
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", path.Join(v2DockerRegistryURL, image), dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, imagePath, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
os.RemoveAll(dir1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestDocker2DirUntagged(c *check.C) {
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "alpine"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().String()
|
||||||
|
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "docker", "--dest", "dir", image, dir1)
|
||||||
|
|
||||||
|
sysCtx := types.SystemContext{}
|
||||||
|
tags, err := docker.GetRepositoryTags(context.Background(), &sysCtx, imageRef)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Check(len(tags), check.Not(check.Equals), 0)
|
||||||
|
|
||||||
|
nManifests, err := filepath.Glob(path.Join(dir1, path.Dir(imagePath), "*", "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(len(nManifests), check.Equals, len(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestYamlUntagged(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
|
||||||
|
image := "alpine"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().Name()
|
||||||
|
|
||||||
|
sysCtx := types.SystemContext{}
|
||||||
|
tags, err := docker.GetRepositoryTags(context.Background(), &sysCtx, imageRef)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Check(len(tags), check.Not(check.Equals), 0)
|
||||||
|
|
||||||
|
yamlConfig := fmt.Sprintf(`
|
||||||
|
docker.io:
|
||||||
|
images:
|
||||||
|
%s:
|
||||||
|
`, image)
|
||||||
|
|
||||||
|
//sync to the local reg
|
||||||
|
yamlFile := path.Join(tmpDir, "registries.yaml")
|
||||||
|
ioutil.WriteFile(yamlFile, []byte(yamlConfig), 0644)
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "yaml", "--dest", "docker", "--dest-tls-verify=false", yamlFile, v2DockerRegistryURL)
|
||||||
|
// sync back from local reg to a folder
|
||||||
|
os.Remove(yamlFile)
|
||||||
|
yamlConfig = fmt.Sprintf(`
|
||||||
|
%s:
|
||||||
|
tls-verify: false
|
||||||
|
images:
|
||||||
|
%s:
|
||||||
|
|
||||||
|
`, v2DockerRegistryURL, imagePath)
|
||||||
|
|
||||||
|
ioutil.WriteFile(yamlFile, []byte(yamlConfig), 0644)
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
|
||||||
|
|
||||||
|
sysCtx = types.SystemContext{
|
||||||
|
DockerInsecureSkipTLSVerify: types.NewOptionalBool(true),
|
||||||
|
}
|
||||||
|
localTags, err := docker.GetRepositoryTags(context.Background(), &sysCtx, imageRef)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Check(len(localTags), check.Not(check.Equals), 0)
|
||||||
|
c.Assert(len(localTags), check.Equals, len(tags))
|
||||||
|
|
||||||
|
nManifests := 0
|
||||||
|
//count the number of manifest.json in dir1
|
||||||
|
err = filepath.Walk(dir1, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && info.Name() == "manifest.json" {
|
||||||
|
nManifests++
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(nManifests, check.Equals, len(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestYaml2Dir(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
|
||||||
|
yamlConfig := `
|
||||||
|
docker.io:
|
||||||
|
images:
|
||||||
|
busybox:
|
||||||
|
- latest
|
||||||
|
- musl
|
||||||
|
alpine:
|
||||||
|
- edge
|
||||||
|
- 3.8
|
||||||
|
|
||||||
|
opensuse/leap:
|
||||||
|
- latest
|
||||||
|
|
||||||
|
quay.io:
|
||||||
|
images:
|
||||||
|
quay/busybox:
|
||||||
|
- latest`
|
||||||
|
|
||||||
|
// get the number of tags
|
||||||
|
re := regexp.MustCompile(`^ +- +[^:/ ]+`)
|
||||||
|
var nTags int
|
||||||
|
for _, l := range strings.Split(yamlConfig, "\n") {
|
||||||
|
if re.MatchString(l) {
|
||||||
|
nTags++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Assert(nTags, check.Not(check.Equals), 0)
|
||||||
|
|
||||||
|
yamlFile := path.Join(tmpDir, "registries.yaml")
|
||||||
|
ioutil.WriteFile(yamlFile, []byte(yamlConfig), 0644)
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
|
||||||
|
|
||||||
|
nManifests := 0
|
||||||
|
err = filepath.Walk(dir1, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && info.Name() == "manifest.json" {
|
||||||
|
nManifests++
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(nManifests, check.Equals, nTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestYamlTLSVerify(c *check.C) {
|
||||||
|
const localRegURL = "docker://" + v2DockerRegistryURL + "/"
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
image := "busybox"
|
||||||
|
tag := "latest"
|
||||||
|
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
// copy docker => docker
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "docker://"+image+":"+tag, localRegURL+image+":"+tag)
|
||||||
|
|
||||||
|
yamlTemplate := `
|
||||||
|
%s:
|
||||||
|
%s
|
||||||
|
images:
|
||||||
|
%s:
|
||||||
|
- %s`
|
||||||
|
|
||||||
|
testCfg := []struct {
|
||||||
|
tlsVerify string
|
||||||
|
msg string
|
||||||
|
checker func(c *check.C, regexp string, args ...string)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
tlsVerify: "tls-verify: false",
|
||||||
|
msg: "",
|
||||||
|
checker: assertSkopeoSucceeds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tlsVerify: "tls-verify: true",
|
||||||
|
msg: ".*server gave HTTP response to HTTPS client.*",
|
||||||
|
checker: assertSkopeoFails,
|
||||||
|
},
|
||||||
|
// no "tls-verify" line means default TLS verify must be ON
|
||||||
|
{
|
||||||
|
tlsVerify: "",
|
||||||
|
msg: ".*server gave HTTP response to HTTPS client.*",
|
||||||
|
checker: assertSkopeoFails,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range testCfg {
|
||||||
|
yamlConfig := fmt.Sprintf(yamlTemplate, v2DockerRegistryURL, cfg.tlsVerify, image, tag)
|
||||||
|
yamlFile := path.Join(tmpDir, "registries.yaml")
|
||||||
|
ioutil.WriteFile(yamlFile, []byte(yamlConfig), 0644)
|
||||||
|
|
||||||
|
cfg.checker(c, cfg.msg, "sync", "--scoped", "--src", "yaml", "--dest", "dir", yamlFile, dir1)
|
||||||
|
os.Remove(yamlFile)
|
||||||
|
os.RemoveAll(dir1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestDocker2DockerTagged(c *check.C) {
|
||||||
|
const localRegURL = "docker://" + v2DockerRegistryURL + "/"
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "busybox:latest"
|
||||||
|
imageRef, err := docker.ParseReference(fmt.Sprintf("//%s", image))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
imagePath := imageRef.DockerReference().String()
|
||||||
|
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
dir2 := path.Join(tmpDir, "dir2")
|
||||||
|
|
||||||
|
// sync docker => docker
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--dest-tls-verify=false", "--src", "docker", "--dest", "docker", image, v2DockerRegistryURL)
|
||||||
|
|
||||||
|
// copy docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "docker://"+image, "dir:"+dir1)
|
||||||
|
_, err = os.Stat(path.Join(dir1, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// copy docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", localRegURL+imagePath, "dir:"+dir2)
|
||||||
|
_, err = os.Stat(path.Join(dir2, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2)
|
||||||
|
c.Assert(out, check.Equals, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestDir2DockerTagged(c *check.C) {
|
||||||
|
const localRegURL = "docker://" + v2DockerRegistryURL + "/"
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection.
|
||||||
|
image := "busybox:latest"
|
||||||
|
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
err = os.Mkdir(dir1, 0755)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
dir2 := path.Join(tmpDir, "dir2")
|
||||||
|
err = os.Mkdir(dir2, 0755)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// copy docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "docker://"+image, "dir:"+path.Join(dir1, image))
|
||||||
|
_, err = os.Stat(path.Join(dir1, image, "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// sync dir => docker
|
||||||
|
assertSkopeoSucceeds(c, "", "sync", "--scoped", "--dest-tls-verify=false", "--src", "dir", "--dest", "docker", dir1, v2DockerRegistryURL)
|
||||||
|
|
||||||
|
// copy docker => dir
|
||||||
|
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", localRegURL+image, "dir:"+path.Join(dir2, image))
|
||||||
|
_, err = os.Stat(path.Join(path.Join(dir2, image), "manifest.json"))
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2)
|
||||||
|
c.Assert(out, check.Equals, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsWithDir2Dir(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
dir1 := path.Join(tmpDir, "dir1")
|
||||||
|
dir2 := path.Join(tmpDir, "dir2")
|
||||||
|
|
||||||
|
// sync dir => dir is not allowed
|
||||||
|
assertSkopeoFails(c, ".*sync from 'dir' to 'dir' not implemented.*", "sync", "--scoped", "--src", "dir", "--dest", "dir", dir1, dir2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsNoSourceImages(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
assertSkopeoFails(c, ".*No images to sync found in .*",
|
||||||
|
"sync", "--scoped", "--dest-tls-verify=false", "--src", "dir", "--dest", "docker", tmpDir, v2DockerRegistryURL)
|
||||||
|
|
||||||
|
assertSkopeoFails(c, ".*No images to sync found in .*",
|
||||||
|
"sync", "--scoped", "--dest-tls-verify=false", "--src", "docker", "--dest", "docker", "hopefully_no_images_will_ever_be_called_like_this", v2DockerRegistryURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsWithDockerSourceNoRegistry(c *check.C) {
|
||||||
|
const regURL = "google.com/namespace/imagename"
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
//untagged
|
||||||
|
assertSkopeoFails(c, ".*invalid status code from registry 404.*",
|
||||||
|
"sync", "--scoped", "--src", "docker", "--dest", "dir", regURL, tmpDir)
|
||||||
|
|
||||||
|
//tagged
|
||||||
|
assertSkopeoFails(c, ".*invalid status code from registry 404.*",
|
||||||
|
"sync", "--scoped", "--src", "docker", "--dest", "dir", regURL+":thetag", tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsWithDockerSourceUnauthorized(c *check.C) {
|
||||||
|
const repo = "privateimagenamethatshouldnotbepublic"
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
//untagged
|
||||||
|
assertSkopeoFails(c, ".*Registry disallows tag list retrieval.*",
|
||||||
|
"sync", "--scoped", "--src", "docker", "--dest", "dir", repo, tmpDir)
|
||||||
|
|
||||||
|
//tagged
|
||||||
|
assertSkopeoFails(c, ".*unauthorized: authentication required.*",
|
||||||
|
"sync", "--scoped", "--src", "docker", "--dest", "dir", repo+":thetag", tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsWithDockerSourceNotExisting(c *check.C) {
|
||||||
|
repo := path.Join(v2DockerRegistryURL, "imagedoesdotexist")
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
//untagged
|
||||||
|
assertSkopeoFails(c, ".*invalid status code from registry 404.*",
|
||||||
|
"sync", "--scoped", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", repo, tmpDir)
|
||||||
|
|
||||||
|
//tagged
|
||||||
|
assertSkopeoFails(c, ".*Error reading manifest.*",
|
||||||
|
"sync", "--scoped", "--src-tls-verify=false", "--src", "docker", "--dest", "dir", repo+":thetag", tmpDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncSuite) TestFailsWithDirSourceNotExisting(c *check.C) {
|
||||||
|
// Make sure the dir does not exist!
|
||||||
|
tmpDir, err := ioutil.TempDir("", "skopeo-sync-test")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
err = os.RemoveAll(tmpDir)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
_, err = os.Stat(path.Join(tmpDir))
|
||||||
|
c.Check(os.IsNotExist(err), check.Equals, true)
|
||||||
|
|
||||||
|
assertSkopeoFails(c, ".*no such file or directory.*",
|
||||||
|
"sync", "--scoped", "--dest-tls-verify=false", "--src", "dir", "--dest", "docker", tmpDir, v2DockerRegistryURL)
|
||||||
|
}
|
Reference in New Issue
Block a user