pkg/cmd/cli/cmd/importimage.go
b8c6f167
 package cmd
 
 import (
 	"fmt"
 	"io"
be69ad81
 	"strings"
59a42f10
 	"time"
b8c6f167
 
011e8314
 	kapi "k8s.io/kubernetes/pkg/api"
 	"k8s.io/kubernetes/pkg/api/errors"
83c702b4
 	"k8s.io/kubernetes/pkg/fields"
95ec120f
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
83c702b4
 	"k8s.io/kubernetes/pkg/watch"
b8c6f167
 
011e8314
 	"github.com/openshift/origin/pkg/client"
 	"github.com/openshift/origin/pkg/cmd/cli/describe"
 	imageapi "github.com/openshift/origin/pkg/image/api"
 	"github.com/spf13/cobra"
 
b8c6f167
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
 )
 
 const (
c124965f
 	importImageLong = `
 Import tag and image information from an external Docker image repository
b8c6f167
 
6b424a62
 Only image streams that have a value set for spec.dockerImageRepository and/or
 spec.Tags may have tag and image information imported.`
b8c6f167
 
 	importImageExample = `  $ %[1]s import-image mystream`
 )
 
 // NewCmdImportImage implements the OpenShift cli import-image command.
 func NewCmdImportImage(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
0db7669d
 	opts := &ImportImageOptions{}
b8c6f167
 	cmd := &cobra.Command{
1316869b
 		Use:        "import-image IMAGESTREAM[:TAG]",
b3e77d8d
 		Short:      "Imports images from a Docker registry",
 		Long:       importImageLong,
 		Example:    fmt.Sprintf(importImageExample, fullName),
 		SuggestFor: []string{"image"},
b8c6f167
 		Run: func(cmd *cobra.Command, args []string) {
0db7669d
 			kcmdutil.CheckErr(opts.Complete(f, args, out))
 			kcmdutil.CheckErr(opts.Validate(cmd))
 			kcmdutil.CheckErr(opts.Run())
b8c6f167
 		},
 	}
1b8c4af9
 	cmd.Flags().StringVar(&opts.From, "from", "", "A Docker image repository to import images from")
0db7669d
 	cmd.Flags().BoolVar(&opts.Confirm, "confirm", false, "If true, allow the image stream import location to be set or changed")
 	cmd.Flags().BoolVar(&opts.All, "all", false, "If true, import all tags from the provided source on creation or if --from is specified")
 	cmd.Flags().BoolVar(&opts.Insecure, "insecure", false, "If true, allow importing from registries that have invalid HTTPS certificates or are hosted via HTTP")
b8c6f167
 
 	return cmd
 }
 
0db7669d
 // ImageImportOptions contains all the necessary information to perform an import.
 type ImportImageOptions struct {
 	// user set values
 	From     string
 	Confirm  bool
 	All      bool
 	Insecure bool
 
 	// internal values
 	Namespace string
 	Name      string
 	Tag       string
 	Target    string
 
 	// helpers
 	out      io.Writer
 	osClient client.Interface
 	isClient client.ImageStreamInterface
 }
 
 // Complete turns a partially defined ImportImageOptions into a solvent structure
 // which can be validated and used for aa import.
 func (o *ImportImageOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error {
 	if len(args) > 0 {
 		o.Target = args[0]
b8c6f167
 	}
 
ab7d732c
 	namespace, _, err := f.DefaultNamespace()
b8c6f167
 	if err != nil {
 		return err
 	}
0db7669d
 	o.Namespace = namespace
b8c6f167
 
90c2aef2
 	osClient, _, err := f.Clients()
b8c6f167
 	if err != nil {
 		return err
 	}
0db7669d
 	o.osClient = osClient
 	o.isClient = osClient.ImageStreams(namespace)
 	o.out = out
b8c6f167
 
0db7669d
 	return nil
 }
 
 // Validate ensures that a ImportImageOptions is valid and can be used to execute
 // an import.
 func (o *ImportImageOptions) Validate(cmd *cobra.Command) error {
 	if len(o.Target) == 0 {
 		return kcmdutil.UsageError(cmd, "you must specify the name of an image stream")
 	}
1316869b
 
0db7669d
 	targetRef, err := imageapi.ParseDockerImageReference(o.Target)
730f1f52
 	switch {
 	case err != nil:
1316869b
 		return fmt.Errorf("the image name must be a valid Docker image pull spec or reference to an image stream (e.g. myregistry/myteam/image:tag)")
730f1f52
 	case len(targetRef.ID) > 0:
 		return fmt.Errorf("to import images by ID, use the 'tag' command")
0db7669d
 	case len(targetRef.Tag) != 0 && o.All:
730f1f52
 		// error out
0db7669d
 		return fmt.Errorf("cannot specify a tag %q as well as --all", o.Target)
 	case len(targetRef.Tag) == 0 && !o.All:
730f1f52
 		// apply the default tag
 		targetRef.Tag = imageapi.DefaultImageTag
1316869b
 	}
0db7669d
 	o.Name = targetRef.Name
 	o.Tag = targetRef.Tag
 
 	return nil
 }
1316869b
 
0db7669d
 // Run contains all the necessary functionality for the OpenShift cli import-image command.
 func (o *ImportImageOptions) Run() error {
 	stream, isi, err := o.createImageImport()
b8c6f167
 	if err != nil {
0db7669d
 		return err
 	}
 
 	// TODO: add dry-run
 	result, err := o.isClient.Import(isi)
 	switch {
 	case err == client.ErrImageStreamImportUnsupported:
 	case err != nil:
 		return err
 	default:
 		fmt.Fprint(o.out, "The import completed successfully.\n\n")
 
 		// optimization, use the image stream returned by the call
90c2aef2
 		d := describe.ImageStreamDescriber{Interface: o.osClient}
0db7669d
 		info, err := d.Describe(o.Namespace, stream.Name)
 		if err != nil {
011e8314
 			return err
 		}
730f1f52
 
0db7669d
 		fmt.Fprintln(o.out, info)
 
 		if r := result.Status.Repository; r != nil && len(r.AdditionalTags) > 0 {
 			fmt.Fprintf(o.out, "\ninfo: The remote repository contained %d additional tags which were not imported: %s\n", len(r.AdditionalTags), strings.Join(r.AdditionalTags, ", "))
 		}
 		return nil
 	}
 
 	// Legacy path, remove when support for older importers is removed
 	delete(stream.Annotations, imageapi.DockerImageRepositoryCheckAnnotation)
 	if o.Insecure {
 		if stream.Annotations == nil {
 			stream.Annotations = make(map[string]string)
 		}
 		stream.Annotations[imageapi.InsecureRepositoryAnnotation] = "true"
 	}
 
 	if stream.CreationTimestamp.IsZero() {
 		stream, err = o.isClient.Create(stream)
 	} else {
 		stream, err = o.isClient.Update(stream)
 	}
 	if err != nil {
 		return err
 	}
 
 	fmt.Fprintln(o.out, "Importing (ctrl+c to stop waiting) ...")
 
 	resourceVersion := stream.ResourceVersion
 	updatedStream, err := o.waitForImport(resourceVersion)
 	if err != nil {
 		if _, ok := err.(importError); ok {
 			return err
 		}
 		return fmt.Errorf("unable to determine if the import completed successfully - please run 'oc describe -n %s imagestream/%s' to see if the tags were updated as expected: %v", stream.Namespace, stream.Name, err)
 	}
 
 	fmt.Fprint(o.out, "The import completed successfully.\n\n")
 
90c2aef2
 	d := describe.ImageStreamDescriber{Interface: o.osClient}
0db7669d
 	info, err := d.Describe(updatedStream.Namespace, updatedStream.Name)
 	if err != nil {
 		return err
 	}
 
 	fmt.Fprintln(o.out, info)
 	return nil
 }
 
 // TODO: move to image/api as a helper
 type importError struct {
 	annotation string
 }
 
 func (e importError) Error() string {
 	return fmt.Sprintf("unable to import image: %s", e.annotation)
 }
 
 func (o *ImportImageOptions) waitForImport(resourceVersion string) (*imageapi.ImageStream, error) {
 	streamWatch, err := o.isClient.Watch(kapi.ListOptions{FieldSelector: fields.OneTermEqualSelector("metadata.name", o.Name), ResourceVersion: resourceVersion})
 	if err != nil {
 		return nil, err
 	}
 	defer streamWatch.Stop()
 
 	for {
 		select {
 		case event, ok := <-streamWatch.ResultChan():
 			if !ok {
 				return nil, fmt.Errorf("image stream watch ended prematurely")
 			}
 
 			switch event.Type {
 			case watch.Modified:
 				s, ok := event.Object.(*imageapi.ImageStream)
 				if !ok {
 					continue
 				}
 				annotation, ok := s.Annotations[imageapi.DockerImageRepositoryCheckAnnotation]
 				if !ok {
 					continue
 				}
 
 				if _, err := time.Parse(time.RFC3339, annotation); err == nil {
 					return s, nil
 				}
 				return nil, importError{annotation}
 
 			case watch.Deleted:
 				return nil, fmt.Errorf("the image stream was deleted")
 			case watch.Error:
 				return nil, fmt.Errorf("error watching image stream")
 			}
 		}
 	}
 }
 
 func (o *ImportImageOptions) createImageImport() (*imageapi.ImageStream, *imageapi.ImageStreamImport, error) {
 	stream, err := o.isClient.Get(o.Name)
 	from := o.From
 	tag := o.Tag
 	if err != nil {
 		if !errors.IsNotFound(err) {
 			return nil, nil, err
 		}
 
730f1f52
 		// the stream is new
0db7669d
 		if !o.Confirm {
 			return nil, nil, fmt.Errorf("no image stream named %q exists, pass --confirm to create and import", o.Name)
011e8314
 		}
730f1f52
 		if len(from) == 0 {
0db7669d
 			from = o.Target
730f1f52
 		}
0db7669d
 		if o.All {
1316869b
 			stream = &imageapi.ImageStream{
0db7669d
 				ObjectMeta: kapi.ObjectMeta{Name: o.Name},
1316869b
 				Spec:       imageapi.ImageStreamSpec{DockerImageRepository: from},
 			}
 		} else {
 			stream = &imageapi.ImageStream{
0db7669d
 				ObjectMeta: kapi.ObjectMeta{Name: o.Name},
1316869b
 				Spec: imageapi.ImageStreamSpec{
 					Tags: map[string]imageapi.TagReference{
730f1f52
 						tag: {
1316869b
 							From: &kapi.ObjectReference{
 								Kind: "DockerImage",
 								Name: from,
 							},
 						},
 					},
 				},
 			}
011e8314
 		}
1316869b
 
011e8314
 	} else {
730f1f52
 		// the stream already exists
7257b7c4
 		if len(stream.Spec.DockerImageRepository) == 0 && len(stream.Spec.Tags) == 0 {
0db7669d
 			return nil, nil, fmt.Errorf("image stream has not defined anything to import")
7257b7c4
 		}
730f1f52
 
0db7669d
 		if o.All {
730f1f52
 			// importing a whole repository
1b8c4af9
 			// TODO soltysh: write tests to cover all the possible usecases!!!
730f1f52
 			if len(from) == 0 {
1b8c4af9
 				if len(stream.Spec.DockerImageRepository) == 0 {
 					// FIXME soltysh:
 					return nil, nil, fmt.Errorf("flag --all is applicable only to images with spec.dockerImageRepository defined")
 				}
 				from = stream.Spec.DockerImageRepository
730f1f52
 			}
 			if from != stream.Spec.DockerImageRepository {
0db7669d
 				if !o.Confirm {
730f1f52
 					if len(stream.Spec.DockerImageRepository) == 0 {
0db7669d
 						return nil, nil, fmt.Errorf("the image stream does not currently import an entire Docker repository, pass --confirm to update")
1316869b
 					}
0db7669d
 					return nil, nil, fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository)
1316869b
 				}
730f1f52
 				stream.Spec.DockerImageRepository = from
1316869b
 			}
730f1f52
 
1316869b
 		} else {
730f1f52
 			// importing a single tag
 
 			// follow any referential tags to the destination
 			finalTag, existing, ok, multiple := imageapi.FollowTagReference(stream, tag)
 			if !ok && multiple {
0db7669d
 				return nil, nil, fmt.Errorf("tag %q on the image stream is a reference to %q, which does not exist", tag, finalTag)
730f1f52
 			}
 
 			if ok {
 				// disallow changing an existing tag
 				if existing.From == nil || existing.From.Kind != "DockerImage" {
0db7669d
 					return nil, nil, fmt.Errorf("tag %q already exists - you must use the 'tag' command if you want to change the source to %q", tag, from)
730f1f52
 				}
 				if len(from) != 0 && from != existing.From.Name {
 					if multiple {
0db7669d
 						return nil, nil, fmt.Errorf("the tag %q points to the tag %q which points to %q - use the 'tag' command if you want to change the source to %q", tag, finalTag, existing.From.Name, from)
730f1f52
 					}
0db7669d
 					return nil, nil, fmt.Errorf("the tag %q points to %q - use the 'tag' command if you want to change the source to %q", tag, existing.From.Name, from)
730f1f52
 				}
 
 				// set the target item to import
 				from = existing.From.Name
 				if multiple {
 					tag = finalTag
 				}
 
 				// clear the legacy annotation
 				delete(existing.Annotations, imageapi.DockerImageRepositoryCheckAnnotation)
 				// reset the generation
 				zero := int64(0)
 				existing.Generation = &zero
 
1316869b
 			} else {
730f1f52
 				// create a new tag
 				if len(from) == 0 {
1b8c4af9
 					from = stream.Spec.DockerImageRepository
730f1f52
 				}
6e84e1ff
 				// if the from is still empty this means there's no such tag defined
 				// nor we can't create any from .spec.dockerImageRepository
 				if len(from) == 0 {
 					return nil, nil, fmt.Errorf("the tag %q does not exist on the image stream - choose an existing tag to import or use the 'tag' command to create a new tag", tag)
 				}
730f1f52
 				existing = &imageapi.TagReference{
1316869b
 					From: &kapi.ObjectReference{
 						Kind: "DockerImage",
 						Name: from,
 					},
011e8314
 				}
 			}
730f1f52
 			stream.Spec.Tags[tag] = *existing
011e8314
 		}
b8c6f167
 	}
 
730f1f52
 	if len(from) == 0 {
 		// catch programmer error
0db7669d
 		return nil, nil, fmt.Errorf("unexpected error, from is empty")
730f1f52
 	}
 
 	// Attempt the new, direct import path
 	isi := &imageapi.ImageStreamImport{
 		ObjectMeta: kapi.ObjectMeta{
 			Name:            stream.Name,
0db7669d
 			Namespace:       o.Namespace,
730f1f52
 			ResourceVersion: stream.ResourceVersion,
 		},
 		Spec: imageapi.ImageStreamImportSpec{Import: true},
 	}
52cfb733
 	insecureAnnotation := stream.Annotations[imageapi.InsecureRepositoryAnnotation]
 	insecure := o.Insecure || insecureAnnotation == "true"
0db7669d
 	if o.All {
730f1f52
 		isi.Spec.Repository = &imageapi.RepositoryImportSpec{
 			From: kapi.ObjectReference{
 				Kind: "DockerImage",
 				Name: from,
 			},
52cfb733
 			ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure},
730f1f52
 		}
e6d5e470
 	} else {
730f1f52
 		isi.Spec.Images = append(isi.Spec.Images, imageapi.ImageImportSpec{
 			From: kapi.ObjectReference{
 				Kind: "DockerImage",
 				Name: from,
 			},
 			To:           &kapi.LocalObjectReference{Name: tag},
52cfb733
 			ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure},
730f1f52
 		})
b8c6f167
 	}
 
0db7669d
 	return stream, isi, nil
b8c6f167
 }