package cmd import ( "fmt" "io" "strings" "time" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/fields" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/watch" "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" "github.com/openshift/origin/pkg/cmd/util/clientcmd" ) const ( importImageLong = ` Import tag and image information from an external Docker image repository Only image streams that have a value set for spec.dockerImageRepository and/or spec.Tags may have tag and image information imported.` 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 { opts := &ImportImageOptions{} cmd := &cobra.Command{ Use: "import-image IMAGESTREAM[:TAG]", Short: "Imports images from a Docker registry", Long: importImageLong, Example: fmt.Sprintf(importImageExample, fullName), SuggestFor: []string{"image"}, Run: func(cmd *cobra.Command, args []string) { kcmdutil.CheckErr(opts.Complete(f, args, out)) kcmdutil.CheckErr(opts.Validate(cmd)) kcmdutil.CheckErr(opts.Run()) }, } cmd.Flags().StringVar(&opts.From, "from", "", "A Docker image repository to import images from") 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") return cmd } // 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] } namespace, _, err := f.DefaultNamespace() if err != nil { return err } o.Namespace = namespace osClient, _, err := f.Clients() if err != nil { return err } o.osClient = osClient o.isClient = osClient.ImageStreams(namespace) o.out = out 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") } targetRef, err := imageapi.ParseDockerImageReference(o.Target) switch { case err != nil: 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)") case len(targetRef.ID) > 0: return fmt.Errorf("to import images by ID, use the 'tag' command") case len(targetRef.Tag) != 0 && o.All: // error out return fmt.Errorf("cannot specify a tag %q as well as --all", o.Target) case len(targetRef.Tag) == 0 && !o.All: // apply the default tag targetRef.Tag = imageapi.DefaultImageTag } o.Name = targetRef.Name o.Tag = targetRef.Tag return nil } // Run contains all the necessary functionality for the OpenShift cli import-image command. func (o *ImportImageOptions) Run() error { stream, isi, err := o.createImageImport() if err != nil { 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 d := describe.ImageStreamDescriber{Interface: o.osClient} info, err := d.Describe(o.Namespace, stream.Name) if err != nil { return err } 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") d := describe.ImageStreamDescriber{Interface: o.osClient} 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 } // the stream is new if !o.Confirm { return nil, nil, fmt.Errorf("no image stream named %q exists, pass --confirm to create and import", o.Name) } if len(from) == 0 { from = o.Target } if o.All { stream = &imageapi.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: o.Name}, Spec: imageapi.ImageStreamSpec{DockerImageRepository: from}, } } else { stream = &imageapi.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: o.Name}, Spec: imageapi.ImageStreamSpec{ Tags: map[string]imageapi.TagReference{ tag: { From: &kapi.ObjectReference{ Kind: "DockerImage", Name: from, }, }, }, }, } } } else { // the stream already exists if len(stream.Spec.DockerImageRepository) == 0 && len(stream.Spec.Tags) == 0 { return nil, nil, fmt.Errorf("image stream has not defined anything to import") } if o.All { // importing a whole repository // TODO soltysh: write tests to cover all the possible usecases!!! if len(from) == 0 { 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 } if from != stream.Spec.DockerImageRepository { if !o.Confirm { if len(stream.Spec.DockerImageRepository) == 0 { return nil, nil, fmt.Errorf("the image stream does not currently import an entire Docker repository, pass --confirm to update") } return nil, nil, fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) } stream.Spec.DockerImageRepository = from } } else { // importing a single tag // follow any referential tags to the destination finalTag, existing, ok, multiple := imageapi.FollowTagReference(stream, tag) if !ok && multiple { return nil, nil, fmt.Errorf("tag %q on the image stream is a reference to %q, which does not exist", tag, finalTag) } if ok { // disallow changing an existing tag if existing.From == nil || existing.From.Kind != "DockerImage" { 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) } if len(from) != 0 && from != existing.From.Name { if multiple { 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) } 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) } // 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 } else { // create a new tag if len(from) == 0 { from = stream.Spec.DockerImageRepository } // 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) } existing = &imageapi.TagReference{ From: &kapi.ObjectReference{ Kind: "DockerImage", Name: from, }, } } stream.Spec.Tags[tag] = *existing } } if len(from) == 0 { // catch programmer error return nil, nil, fmt.Errorf("unexpected error, from is empty") } // Attempt the new, direct import path isi := &imageapi.ImageStreamImport{ ObjectMeta: kapi.ObjectMeta{ Name: stream.Name, Namespace: o.Namespace, ResourceVersion: stream.ResourceVersion, }, Spec: imageapi.ImageStreamImportSpec{Import: true}, } insecureAnnotation := stream.Annotations[imageapi.InsecureRepositoryAnnotation] insecure := o.Insecure || insecureAnnotation == "true" if o.All { isi.Spec.Repository = &imageapi.RepositoryImportSpec{ From: kapi.ObjectReference{ Kind: "DockerImage", Name: from, }, ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure}, } } else { isi.Spec.Images = append(isi.Spec.Images, imageapi.ImageImportSpec{ From: kapi.ObjectReference{ Kind: "DockerImage", Name: from, }, To: &kapi.LocalObjectReference{Name: tag}, ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure}, }) } return stream, isi, nil }