pkg/cmd/cli/cmd/tag.go
166190a4
 package cmd
 
 import (
477300a4
 	"errors"
166190a4
 	"fmt"
 	"io"
 	"strings"
 
011e8314
 	"github.com/golang/glog"
166190a4
 	"github.com/spf13/cobra"
b9776fd4
 
83c702b4
 	kapi "k8s.io/kubernetes/pkg/api"
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
b9776fd4
 	"k8s.io/kubernetes/pkg/api/unversioned"
867e6e44
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
95ec120f
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
3dd75654
 	"k8s.io/kubernetes/pkg/util/sets"
166190a4
 
477300a4
 	"github.com/openshift/origin/pkg/client"
166190a4
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
011e8314
 	imageapi "github.com/openshift/origin/pkg/image/api"
166190a4
 )
 
477300a4
 // TagOptions contains all the necessary options for the cli tag command.
 type TagOptions struct {
 	out      io.Writer
 	osClient client.Interface
 
56d56293
 	deleteTag   bool
 	aliasTag    bool
 	scheduleTag bool
 	insecureTag bool
 	namespace   string
477300a4
 
 	ref            imageapi.DockerImageReference
 	sourceKind     string
 	destNamespace  []string
 	destNameAndTag []string
 }
 
166190a4
 const (
c124965f
 	tagLong = `
 Tag existing images into image streams
166190a4
 
 The tag command allows you to take an existing tag or image from an image
 stream, or a Docker image pull spec, and set it as the most recent image for a
 tag in 1 or more other image streams. It is similar to the 'docker tag'
1d958f08
 command, but it operates on image streams instead.
 
 Pass the --insecure flag if your external registry does not have a valid HTTPS
 certificate, or is only served over HTTP. Pass --scheduled to have the server
 regularly check the tag for updates and import the latest version (which can
 then trigger builds and deployments). Note that --scheduled is only allowed for
 Docker images.
 `
166190a4
 
477300a4
 	tagExample = `  # Tag the current image for the image stream 'openshift/ruby' and tag '2.0' into the image stream 'yourproject/ruby with tag 'tip'.
af2094c4
   %[1]s tag openshift/ruby:2.0 yourproject/ruby:tip
166190a4
 
477300a4
   # Tag a specific image.
af2094c4
   %[1]s tag openshift/ruby@sha256:6b646fa6bf5e5e4c7fa41056c27910e679c03ebe7f93e361e6515a9da7e258cc yourproject/ruby:tip
166190a4
 
477300a4
   # Tag an external Docker image.
af2094c4
   %[1]s tag --source=docker openshift/origin:latest yourproject/ruby:tip
477300a4
 
   # Remove the specified spec tag from an image stream.
af2094c4
   %[1]s tag openshift/origin:latest -d`
166190a4
 )
 
 // NewCmdTag implements the OpenShift cli tag command.
 func NewCmdTag(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
477300a4
 	opts := &TagOptions{}
166190a4
 
 	cmd := &cobra.Command{
 		Use:     "tag [--source=SOURCETYPE] SOURCE DEST [DEST ...]",
 		Short:   "Tag existing images into image streams",
 		Long:    tagLong,
 		Example: fmt.Sprintf(tagExample, fullName),
 		Run: func(cmd *cobra.Command, args []string) {
95ec120f
 			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
 			kcmdutil.CheckErr(opts.Validate())
 			kcmdutil.CheckErr(opts.RunTag())
166190a4
 		},
 	}
 
477300a4
 	cmd.Flags().StringVar(&opts.sourceKind, "source", opts.sourceKind, "Optional hint for the source type; valid values are 'imagestreamtag', 'istag', 'imagestreamimage', 'isimage', and 'docker'")
eb97291e
 	cmd.Flags().BoolVarP(&opts.deleteTag, "delete", "d", opts.deleteTag, "Delete the provided spec tags")
 	cmd.Flags().BoolVar(&opts.aliasTag, "alias", false, "Should the destination tag be updated whenever the source tag changes. Defaults to false.")
1d958f08
 	cmd.Flags().BoolVar(&opts.scheduleTag, "scheduled", false, "Set a Docker image to be periodically imported from a remote repository.")
56d56293
 	cmd.Flags().BoolVar(&opts.insecureTag, "insecure", false, "Set to true if importing the specified Docker image requires HTTP or has a self-signed certificate.")
166190a4
 
 	return cmd
 }
 
477300a4
 func parseStreamName(defaultNamespace, name string) (string, string, error) {
166190a4
 	if !strings.Contains(name, "/") {
 		return defaultNamespace, name, nil
 	}
 
 	parts := strings.Split(name, "/")
 	if len(parts) != 2 {
 		return "", "", fmt.Errorf("invalid image stream %q", name)
 	}
 
 	namespace := parts[0]
 	if len(namespace) == 0 {
 		return "", "", fmt.Errorf("invalid namespace %q for image stream %q", namespace, name)
 	}
 
 	streamName := parts[1]
 	if len(streamName) == 0 {
 		return "", "", fmt.Errorf("invalid name %q for image stream %q", streamName, name)
 	}
 
 	return namespace, streamName, nil
 }
 
 func determineSourceKind(f *clientcmd.Factory, input string) string {
59e6f9d2
 	mapper, _ := f.Object(false)
b9776fd4
 	gvks, err := mapper.KindsFor(unversioned.GroupVersionResource{Group: imageapi.GroupName, Resource: input})
166190a4
 	if err == nil {
b9776fd4
 		return gvks[0].Kind
166190a4
 	}
 
 	// DockerImage isn't in RESTMapper
 	switch strings.ToLower(input) {
 	case "docker", "dockerimage":
 		return "DockerImage"
 	}
 
 	return input
 }
 
477300a4
 // Complete completes all the required options for the tag command.
 func (o *TagOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
 	if len(args) < 2 && (len(args) < 1 && !o.deleteTag) {
95ec120f
 		return kcmdutil.UsageError(cmd, "you must specify a source and at least one destination or one or more tags to delete")
166190a4
 	}
 
477300a4
 	// Setup writer.
 	o.out = out
166190a4
 
477300a4
 	// Setup client.
 	var err error
 	o.osClient, _, err = f.Clients()
166190a4
 	if err != nil {
 		return err
 	}
 
477300a4
 	// Setup namespace.
867e6e44
 	if len(o.namespace) == 0 {
 		o.namespace, _, err = f.DefaultNamespace()
 		if err != nil {
 			return err
 		}
166190a4
 	}
477300a4
 
 	// Populate source.
 	if !o.deleteTag {
 		source := args[0]
 		glog.V(3).Infof("Using %q as a source tag", source)
 
eb97291e
 		sourceKind := o.sourceKind
 		if len(sourceKind) > 0 {
 			sourceKind = determineSourceKind(f, sourceKind)
011e8314
 		}
eb97291e
 		if len(sourceKind) > 0 {
477300a4
 			validSources := sets.NewString("imagestreamtag", "istag", "imagestreamimage", "isimage", "docker", "dockerimage")
eb97291e
 			if !validSources.Has(strings.ToLower(sourceKind)) {
95ec120f
 				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, "invalid source %q; valid values are %v", o.sourceKind, strings.Join(validSources.List(), ", ")))
011e8314
 			}
 		}
477300a4
 
eb97291e
 		ref, err := imageapi.ParseDockerImageReference(source)
477300a4
 		if err != nil {
 			return fmt.Errorf("invalid SOURCE: %v", err)
011e8314
 		}
eb97291e
 		switch sourceKind {
477300a4
 		case "ImageStreamTag", "ImageStreamImage":
eb97291e
 			if len(ref.Registry) > 0 {
477300a4
 				return fmt.Errorf("server in SOURCE is only allowed when providing a Docker image")
 			}
eb97291e
 			if ref.Namespace == imageapi.DockerDefaultNamespace {
867e6e44
 				ref.Namespace = o.namespace
477300a4
 			}
eb97291e
 			if sourceKind == "ImageStreamTag" {
 				if len(ref.Tag) == 0 {
477300a4
 					return fmt.Errorf("--source=ImageStreamTag requires a valid <name>:<tag> in SOURCE")
 				}
 			} else {
eb97291e
 				if len(ref.ID) == 0 {
477300a4
 					return fmt.Errorf("--source=ImageStreamImage requires a valid <name>@<id> in SOURCE")
 				}
 			}
 		case "":
eb97291e
 			if len(ref.Registry) > 0 {
 				sourceKind = "DockerImage"
477300a4
 				break
 			}
eb97291e
 			if len(ref.ID) > 0 {
 				sourceKind = "ImageStreamImage"
477300a4
 				break
 			}
eb97291e
 			if len(ref.Tag) > 0 {
 				sourceKind = "ImageStreamTag"
 				break
 			}
 			sourceKind = "DockerImage"
166190a4
 		}
 
eb97291e
 		// if we are not aliasing the tag, specify the exact value to copy
 		if sourceKind == "ImageStreamTag" && !o.aliasTag {
 			srcNamespace := ref.Namespace
 			if len(srcNamespace) == 0 {
867e6e44
 				srcNamespace = o.namespace
eb97291e
 			}
 			is, err := o.osClient.ImageStreams(srcNamespace).Get(ref.Name)
 			if err != nil {
 				return err
 			}
 			event := imageapi.LatestTaggedImage(is, ref.Tag)
 			if event == nil {
 				return fmt.Errorf("%q is not currently pointing to an image, cannot use it as the source of a tag", args[0])
 			}
 			if len(event.Image) == 0 {
 				imageRef, err := imageapi.ParseDockerImageReference(event.DockerImageReference)
 				if err != nil {
 					return fmt.Errorf("the image stream tag %q has an invalid pull spec and cannot be used to tag: %v", args[0], err)
 				}
 				ref = imageRef
 				sourceKind = "DockerImage"
 			} else {
 				ref.ID = event.Image
 				ref.Tag = ""
 				sourceKind = "ImageStreamImage"
 			}
 		}
166190a4
 
eb97291e
 		args = args[1:]
 		o.sourceKind = sourceKind
 		o.ref = ref
477300a4
 		glog.V(3).Infof("Source tag %s %#v", o.sourceKind, o.ref)
 	}
011e8314
 
477300a4
 	// Populate destinations.
 	for _, arg := range args {
867e6e44
 		destNamespace, destNameAndTag, err := parseStreamName(o.namespace, arg)
166190a4
 		if err != nil {
 			return err
 		}
477300a4
 		o.destNamespace = append(o.destNamespace, destNamespace)
 		o.destNameAndTag = append(o.destNameAndTag, destNameAndTag)
 		glog.V(3).Infof("Using \"%s/%s\" as a destination tag", destNamespace, destNameAndTag)
 	}
166190a4
 
477300a4
 	return nil
 }
 
 // Validate validates all the required options for the tag command.
 func (o TagOptions) Validate() error {
 	// Validate client and writer
 	if o.osClient == nil {
 		return errors.New("a client is required")
 	}
 	if o.out == nil {
 		return errors.New("a writer interface is required")
 	}
 
eb97291e
 	if o.deleteTag && o.aliasTag {
0e6d8de2
 		return errors.New("--alias and --delete may not be both specified")
eb97291e
 	}
 
477300a4
 	// Validate source tag based on --delete usage.
 	if o.deleteTag {
 		if len(o.sourceKind) > 0 {
 			return errors.New("cannot specify a source kind when deleting")
 		}
 		if len(o.ref.String()) > 0 {
 			return errors.New("cannot specify a source when deleting")
 		}
56d56293
 		if o.scheduleTag || o.insecureTag {
 			return errors.New("cannot set flags for importing images when deleting a tag")
 		}
477300a4
 	} else {
 		if len(o.sourceKind) == 0 {
 			return errors.New("a source kind is required")
 		}
 		if len(o.ref.String()) == 0 {
 			return errors.New("a source is required")
 		}
 	}
 
 	// Validate destination tags.
 	if len(o.destNamespace) == 0 || len(o.destNameAndTag) == 0 {
 		return errors.New("at least a destination is required")
 	}
 	if len(o.destNamespace) != len(o.destNameAndTag) {
 		return errors.New("destination namespaces don't match with destination tags")
 	}
56d56293
 	if o.sourceKind != "DockerImage" && (o.scheduleTag || o.insecureTag) {
 		return errors.New("only Docker images can have importing flags set")
 	}
 	if o.aliasTag && (o.scheduleTag || o.insecureTag) {
 		return errors.New("cannot set a Docker image tag as an alias and also set import flags")
 	}
477300a4
 
 	return nil
 }
 
 // RunTag contains all the necessary functionality for the OpenShift cli tag command.
 func (o TagOptions) RunTag() error {
 	for i, destNameAndTag := range o.destNameAndTag {
166190a4
 		destName, destTag, ok := imageapi.SplitImageStreamTag(destNameAndTag)
 		if !ok {
0e6d8de2
 			return fmt.Errorf("%q must be of the form <stream_name>:<tag>", destNameAndTag)
166190a4
 		}
 
867e6e44
 		err := kclient.RetryOnConflict(kclient.DefaultRetry, func() error {
 			isc := o.osClient.ImageStreams(o.destNamespace[i])
 			target, err := isc.Get(destName)
 			if err != nil {
 				if !kerrors.IsNotFound(err) {
 					return err
 				}
97b2b5c6
 
867e6e44
 				if o.deleteTag {
 					// Nothing to do here, continue to the next dest tag
 					// if there is any.
0e6d8de2
 					fmt.Fprintf(o.out, "Image stream %q does not exist.\n", destName)
867e6e44
 					return nil
 				}
 
 				// try to create the target if it doesn't exist
 				target = &imageapi.ImageStream{
 					ObjectMeta: kapi.ObjectMeta{
 						Name: destName,
 					},
 				}
477300a4
 			}
 
867e6e44
 			if target.Spec.Tags == nil {
 				target.Spec.Tags = make(map[string]imageapi.TagReference)
97b2b5c6
 			}
166190a4
 
867e6e44
 			msg := ""
 			if o.deleteTag {
 				// The user wants to delete a spec tag.
 				if _, ok := target.Spec.Tags[destTag]; !ok {
 					return fmt.Errorf("destination tag %s/%s does not exist.\n", o.destNamespace[i], destNameAndTag)
 				}
 				delete(target.Spec.Tags, destTag)
 				msg = fmt.Sprintf("Deleted tag %s/%s.", o.destNamespace[i], destNameAndTag)
 			} else {
 				// The user wants to symlink a tag.
 				targetRef, ok := target.Spec.Tags[destTag]
 				if !ok {
 					targetRef = imageapi.TagReference{}
 				}
166190a4
 
56d56293
 				targetRef.ImportPolicy.Insecure = o.insecureTag
 				targetRef.ImportPolicy.Scheduled = o.scheduleTag
867e6e44
 				targetRef.From = &kapi.ObjectReference{
 					Kind: o.sourceKind,
 				}
 				localRef := o.ref
 				switch o.sourceKind {
 				case "DockerImage":
730f1f52
 					targetRef.From.Name = localRef.Exact()
 					if targetRef.Generation == nil {
 						// for servers that do not support tag generations, we need to force re-import to fetch its metadata
 						delete(target.Annotations, imageapi.DockerImageRepositoryCheckAnnotation)
 					} else {
 						// for newer servers we do not need to force re-import
 						gen := int64(0)
 						targetRef.Generation = &gen
 					}
56d56293
 
867e6e44
 				default:
 					targetRef.From.Name = localRef.NameString()
 					targetRef.From.Namespace = o.ref.Namespace
 				}
166190a4
 
867e6e44
 				sameNamespace := o.namespace == o.destNamespace[i]
 				target.Spec.Tags[destTag] = targetRef
 				if o.aliasTag {
 					if sameNamespace {
 						msg = fmt.Sprintf("Tag %s set up to track %s.", destNameAndTag, o.ref.Exact())
 					} else {
 						msg = fmt.Sprintf("Tag %s/%s set up to track %s.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
 					}
 				} else {
56d56293
 					if targetRef.ImportPolicy.Scheduled {
 						if sameNamespace {
 							msg = fmt.Sprintf("Tag %s set to import %s periodically.", destNameAndTag, o.ref.Exact())
 						} else {
 							msg = fmt.Sprintf("Tag %s/%s set to %s periodically.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
 						}
867e6e44
 					} else {
56d56293
 						if sameNamespace {
 							msg = fmt.Sprintf("Tag %s set to %s.", destNameAndTag, o.ref.Exact())
 						} else {
 							msg = fmt.Sprintf("Tag %s/%s set to %s.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
 						}
867e6e44
 					}
 				}
477300a4
 			}
166190a4
 
867e6e44
 			// Check the stream creation timestamp and make sure we will not
 			// create a new image stream while deleting.
 			if target.CreationTimestamp.IsZero() && !o.deleteTag {
 				_, err = isc.Create(target)
 			} else {
 				_, err = isc.Update(target)
 			}
 			if err != nil {
 				return err
477300a4
 			}
166190a4
 
867e6e44
 			fmt.Fprintln(o.out, msg)
 			return nil
 		})
011e8314
 		if err != nil {
166190a4
 			return err
 		}
 	}
 
 	return nil
 }