b8c6f167 |
package cmd
import (
"fmt"
"io" |
be69ad81 |
"strings" |
59a42f10 |
"time" |
b8c6f167 |
|
d2e21f7a |
"github.com/spf13/cobra"
|
011e8314 |
kapi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" |
584e1cb5 |
"k8s.io/kubernetes/pkg/api/unversioned" |
83c702b4 |
"k8s.io/kubernetes/pkg/fields" |
37b6c5f1 |
kctl "k8s.io/kubernetes/pkg/kubectl" |
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" |
6267dded |
"github.com/openshift/origin/pkg/cmd/templates" |
b8c6f167 |
"github.com/openshift/origin/pkg/cmd/util/clientcmd" |
d2e21f7a |
imageapi "github.com/openshift/origin/pkg/image/api" |
b8c6f167 |
)
|
6267dded |
var (
importImageLong = templates.LongDesc(`
Import tag and image information from an external Docker image repository |
b8c6f167 |
|
6267dded |
Only image streams that have a value set for spec.dockerImageRepository and/or
spec.Tags may have tag and image information imported.`) |
b8c6f167 |
|
6267dded |
importImageExample = templates.Examples(` %[1]s import-image mystream`) |
b8c6f167 |
)
// NewCmdImportImage implements the OpenShift cli import-image command. |
584e1cb5 |
func NewCmdImportImage(fullName string, f *clientcmd.Factory, out, errout 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) { |
584e1cb5 |
kcmdutil.CheckErr(opts.Complete(f, cmd, args, fullName, out, errout)) |
0db7669d |
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") |
4d679ac8 |
opts.Insecure = cmd.Flags().Bool("insecure", false, "If true, allow importing from registries that have invalid HTTPS certificates or are hosted via HTTP. This flag will take precedence over the insecure annotation.") |
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 |
4d679ac8 |
Insecure *bool |
0db7669d |
// internal values
Namespace string
Name string
Tag string
Target string
|
5913cbe3 |
CommandName string
|
0db7669d |
// helpers
out io.Writer |
584e1cb5 |
errout io.Writer |
0db7669d |
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. |
584e1cb5 |
func (o *ImportImageOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, commandName string, out, errout io.Writer) error { |
5913cbe3 |
o.CommandName = commandName
|
0db7669d |
if len(args) > 0 {
o.Target = args[0] |
b8c6f167 |
}
|
4d679ac8 |
if !cmd.Flags().Lookup("insecure").Changed {
o.Insecure = nil
}
|
ab7d732c |
namespace, _, err := f.DefaultNamespace() |
b8c6f167 |
if err != nil {
return err
} |
0db7669d |
o.Namespace = namespace |
b8c6f167 |
|
97e6f1de |
osClient, _, _, err := f.Clients() |
b8c6f167 |
if err != nil {
return err
} |
0db7669d |
o.osClient = osClient
o.isClient = osClient.ImageStreams(namespace)
o.out = out |
584e1cb5 |
o.errout = errout |
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 { |
6fa0d7e2 |
// TODO: add dry-run |
0db7669d |
stream, isi, err := o.createImageImport() |
b8c6f167 |
if err != nil { |
0db7669d |
return err
}
|
6fa0d7e2 |
// Attempt the new, direct import path |
0db7669d |
result, err := o.isClient.Import(isi)
switch {
case err == client.ErrImageStreamImportUnsupported:
case err != nil:
return err
default: |
584e1cb5 |
if wasError(result) {
fmt.Fprintf(o.errout, "The import completed with errors.\n\n")
} else {
fmt.Fprint(o.out, "The import completed successfully.\n\n")
} |
0db7669d |
// optimization, use the image stream returned by the call |
90c2aef2 |
d := describe.ImageStreamDescriber{Interface: o.osClient} |
37b6c5f1 |
info, err := d.Describe(o.Namespace, stream.Name, kctl.DescriberSettings{}) |
0db7669d |
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) |
4d679ac8 |
if o.Insecure != nil && *o.Insecure { |
0db7669d |
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
} |
5913cbe3 |
return fmt.Errorf("unable to determine if the import completed successfully - please run '%s describe -n %s imagestream/%s' to see if the tags were updated as expected: %v", o.CommandName, stream.Namespace, stream.Name, err) |
0db7669d |
}
fmt.Fprint(o.out, "The import completed successfully.\n\n")
|
90c2aef2 |
d := describe.ImageStreamDescriber{Interface: o.osClient} |
37b6c5f1 |
info, err := d.Describe(updatedStream.Namespace, updatedStream.Name, kctl.DescriberSettings{}) |
0db7669d |
if err != nil {
return err
}
fmt.Fprintln(o.out, info)
return nil
}
|
584e1cb5 |
func wasError(isi *imageapi.ImageStreamImport) bool {
for _, image := range isi.Status.Images {
if image.Status.Status == unversioned.StatusFailure {
return true
}
}
if isi.Status.Repository != nil && isi.Status.Repository.Status.Status == unversioned.StatusFailure {
return true
}
return false
}
|
0db7669d |
// 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) { |
fa40c7dd |
var isi *imageapi.ImageStreamImport |
0db7669d |
stream, err := o.isClient.Get(o.Name) |
fa40c7dd |
// no stream, try creating one |
0db7669d |
if err != nil {
if !errors.IsNotFound(err) {
return nil, nil, err
}
if !o.Confirm {
return nil, nil, fmt.Errorf("no image stream named %q exists, pass --confirm to create and import", o.Name) |
011e8314 |
} |
fa40c7dd |
stream, isi = o.newImageStream()
return stream, isi, nil |
b8c6f167 |
}
|
fa40c7dd |
// the stream already exists
if len(stream.Spec.DockerImageRepository) == 0 && len(stream.Spec.Tags) == 0 {
return nil, nil, fmt.Errorf("image stream does not have valid docker images to be imported") |
730f1f52 |
}
|
0db7669d |
if o.All { |
fa40c7dd |
// importing the entire repository
isi, err = o.importAll(stream)
if err != nil {
return nil, nil, err |
730f1f52 |
} |
e6d5e470 |
} else { |
fa40c7dd |
// importing a single tag
isi, err = o.importTag(stream)
if err != nil {
return nil, nil, err
} |
b8c6f167 |
}
|
0db7669d |
return stream, isi, nil |
b8c6f167 |
} |
6fa0d7e2 |
|
fa40c7dd |
func (o *ImportImageOptions) importAll(stream *imageapi.ImageStream) (*imageapi.ImageStreamImport, error) { |
6fa0d7e2 |
from := o.From |
fa40c7dd |
// update ImageStream appropriately |
6fa0d7e2 |
if len(from) == 0 { |
fa40c7dd |
if len(stream.Spec.DockerImageRepository) != 0 {
from = stream.Spec.DockerImageRepository
} else {
tags := make(map[string]string)
for name, tag := range stream.Spec.Tags {
if tag.From != nil && tag.From.Kind == "DockerImage" {
tags[name] = tag.From.Name
}
}
if len(tags) == 0 {
return nil, fmt.Errorf("image stream does not have tags pointing to external docker images")
}
return o.newImageStreamImportTags(stream, tags), nil |
6fa0d7e2 |
}
}
if from != stream.Spec.DockerImageRepository {
if !o.Confirm {
if len(stream.Spec.DockerImageRepository) == 0 { |
fa40c7dd |
return nil, fmt.Errorf("the image stream does not currently import an entire Docker repository, pass --confirm to update") |
6fa0d7e2 |
} |
fa40c7dd |
return nil, fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) |
6fa0d7e2 |
}
stream.Spec.DockerImageRepository = from
}
|
fa40c7dd |
// and create accompanying ImageStreamImport
return o.newImageStreamImportAll(stream, from), nil |
6fa0d7e2 |
}
|
fa40c7dd |
func (o *ImportImageOptions) importTag(stream *imageapi.ImageStream) (*imageapi.ImageStreamImport, error) { |
6fa0d7e2 |
from := o.From
tag := o.Tag
// follow any referential tags to the destination
finalTag, existing, ok, multiple := imageapi.FollowTagReference(stream, tag)
if !ok && multiple { |
fa40c7dd |
return nil, fmt.Errorf("tag %q on the image stream is a reference to %q, which does not exist", tag, finalTag) |
6fa0d7e2 |
}
|
fa40c7dd |
// update ImageStream appropriately |
6fa0d7e2 |
if ok { |
4ab49cae |
// disallow re-importing anything other than DockerImage
if existing.From != nil && existing.From.Kind != "DockerImage" {
return nil, fmt.Errorf("tag %q points to existing %s %q, it cannot be re-imported", tag, existing.From.Kind, existing.From.Name)
} |
6fa0d7e2 |
// disallow changing an existing tag |
4ab49cae |
if existing.From == nil { |
fa40c7dd |
return nil, fmt.Errorf("tag %q already exists - you must use the 'tag' command if you want to change the source to %q", tag, from) |
6fa0d7e2 |
}
if len(from) != 0 && from != existing.From.Name {
if multiple { |
fa40c7dd |
return 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) |
6fa0d7e2 |
} |
fa40c7dd |
return 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) |
6fa0d7e2 |
}
// 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 |
d2e21f7a |
if len(from) == 0 && tag == imageapi.DefaultImageTag { |
6fa0d7e2 |
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 { |
fa40c7dd |
return 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) |
6fa0d7e2 |
}
existing = &imageapi.TagReference{
From: &kapi.ObjectReference{
Kind: "DockerImage",
Name: from,
},
}
}
stream.Spec.Tags[tag] = *existing
|
fa40c7dd |
// and create accompanying ImageStreamImport
return o.newImageStreamImportTags(stream, map[string]string{tag: from}), nil
}
func (o *ImportImageOptions) newImageStream() (*imageapi.ImageStream, *imageapi.ImageStreamImport) {
from := o.From
tag := o.Tag
if len(from) == 0 {
from = o.Target
}
var stream *imageapi.ImageStream
// create new ImageStream
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,
},
},
},
},
}
}
// and accompanying ImageStreamImport
var isi *imageapi.ImageStreamImport
if o.All {
isi = o.newImageStreamImportAll(stream, from)
} else {
isi = o.newImageStreamImportTags(stream, map[string]string{tag: from})
}
return stream, isi
}
func (o *ImportImageOptions) newImageStreamImport(stream *imageapi.ImageStream) (*imageapi.ImageStreamImport, bool) {
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 := insecureAnnotation == "true"
// --insecure flag (if provided) takes precedence over insecure annotation
if o.Insecure != nil {
insecure = *o.Insecure
}
return isi, insecure
}
func (o *ImportImageOptions) newImageStreamImportAll(stream *imageapi.ImageStream, from string) *imageapi.ImageStreamImport {
isi, insecure := o.newImageStreamImport(stream)
isi.Spec.Repository = &imageapi.RepositoryImportSpec{
From: kapi.ObjectReference{
Kind: "DockerImage",
Name: from,
},
ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure},
}
return isi
}
func (o *ImportImageOptions) newImageStreamImportTags(stream *imageapi.ImageStream, tags map[string]string) *imageapi.ImageStreamImport {
isi, insecure := o.newImageStreamImport(stream)
for tag, from := range tags {
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 isi |
6fa0d7e2 |
} |