From ed5ca1d3d9b11db6f886ee755dfc9154852d54f1 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sun, 13 Dec 2015 17:09:00 -0500 Subject: [PATCH 1/5] UPSTREAM: : Allow client transport wrappers to support CancelRequest --- .../kubernetes/pkg/client/unversioned/debugging.go | 8 ++++++++ .../kubernetes/pkg/client/unversioned/transport.go | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/debugging.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/debugging.go index 76f8c2caa585..cbc3896dbbf1 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/debugging.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/debugging.go @@ -135,6 +135,14 @@ func (rt *DebuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, e return response, err } +func (rt *DebuggingRoundTripper) CancelRequest(req *http.Request) { + if canceler, ok := rt.delegatedRoundTripper.(requestCanceler); ok { + canceler.CancelRequest(req) + } else { + glog.Errorf("CancelRequest not implemented") + } +} + var _ = util.RoundTripperWrapper(&DebuggingRoundTripper{}) func (rt *DebuggingRoundTripper) WrappedRoundTripper() http.RoundTripper { diff --git a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/transport.go b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/transport.go index 405b495c21df..6d06b16a3d84 100644 --- a/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/transport.go +++ b/Godeps/_workspace/src/k8s.io/kubernetes/pkg/client/unversioned/transport.go @@ -26,6 +26,10 @@ import ( "k8s.io/kubernetes/pkg/util" ) +type requestCanceler interface { + CancelRequest(req *http.Request) +} + type userAgentRoundTripper struct { agent string rt http.RoundTripper @@ -50,6 +54,12 @@ func (rt *userAgentRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } +func (rt *userAgentRoundTripper) CancelRequest(req *http.Request) { + if canceler, ok := rt.rt.(requestCanceler); ok { + canceler.CancelRequest(req) + } +} + type basicAuthRoundTripper struct { username string password string From 1316869b14260fac78c2b762cd71c22181ea6d7d Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 14 Dec 2015 13:36:40 -0500 Subject: [PATCH 2/5] Make import-image a bit more flexible --- pkg/cmd/cli/cmd/importimage.go | 87 +++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/cli/cmd/importimage.go b/pkg/cmd/cli/cmd/importimage.go index 871daf170f8a..b74aef807bba 100644 --- a/pkg/cmd/cli/cmd/importimage.go +++ b/pkg/cmd/cli/cmd/importimage.go @@ -33,7 +33,7 @@ spec.Tags may have tag and image information imported.` // NewCmdImportImage implements the OpenShift cli import-image command. func NewCmdImportImage(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "import-image IMAGESTREAM", + Use: "import-image IMAGESTREAM[:TAG]", Short: "Imports images from a Docker registry", Long: importImageLong, Example: fmt.Sprintf(importImageExample, fullName), @@ -43,9 +43,10 @@ func NewCmdImportImage(fullName string, f *clientcmd.Factory, out io.Writer) *co cmdutil.CheckErr(err) }, } - cmd.Flags().String("from", "", "A Docker image repository to import images from") + cmd.Flags().String("from", "", "A Docker image repository or tag to import images from") cmd.Flags().Bool("confirm", false, "If true, allow the image stream import location to be set or changed") cmd.Flags().Bool("insecure-repository", false, "If true, allow the docker registry to be insecure") + cmd.Flags().Bool("all", false, "If true, import all tags from the provided source on creation or if --from is specified") return cmd } @@ -70,30 +71,84 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg from := cmdutil.GetFlagString(cmd, "from") confirm := cmdutil.GetFlagBool(cmd, "confirm") insecure := cmdutil.GetFlagBool(cmd, "insecure-repository") + all := cmdutil.GetFlagBool(cmd, "all") + + if len(from) == 0 { + from = streamName + } + + ref, err := imageapi.ParseDockerImageReference(streamName) + if 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)") + } + if len(ref.ID) > 0 { + return fmt.Errorf("if you want to import an image by ID, use --from=%s %s", from, ref.AsRepository().Exact()) + } + // apply the default tag + if !all && len(ref.Tag) == 0 { + ref.Tag = imageapi.DefaultImageTag + } + name := ref.Name + +>>>>>>> e650722... Make import-image a bit more flexible imageStreamClient := osClient.ImageStreams(namespace) - stream, err := imageStreamClient.Get(streamName) + stream, err := imageStreamClient.Get(name) if err != nil { - if len(from) == 0 || !errors.IsNotFound(err) { + if !errors.IsNotFound(err) { return err } - if !confirm { - return fmt.Errorf("the image stream does not exist, pass --confirm to create") + if len(from) == 0 && !confirm { + return fmt.Errorf("no image stream named %q exists, pass --confirm to create and import", name) } - stream = &imageapi.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: streamName}, - Spec: imageapi.ImageStreamSpec{DockerImageRepository: from}, + + if len(ref.Tag) == 0 { + stream = &imageapi.ImageStream{ + ObjectMeta: kapi.ObjectMeta{Name: name}, + Spec: imageapi.ImageStreamSpec{DockerImageRepository: from}, + } + } else { + stream = &imageapi.ImageStream{ + ObjectMeta: kapi.ObjectMeta{Name: name}, + Spec: imageapi.ImageStreamSpec{ + Tags: map[string]imageapi.TagReference{ + ref.Tag: { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: from, + }, + }, + }, + }, + } } + } else { if len(stream.Spec.DockerImageRepository) == 0 && len(stream.Spec.Tags) == 0 { return fmt.Errorf("image stream has not defined anything to import") } - if len(from) != 0 { - if from != stream.Spec.DockerImageRepository { - if !confirm { - return fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) + if len(ref.Tag) == 0 { + if len(from) != 0 { + if from != stream.Spec.DockerImageRepository { + if !confirm { + return fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) + } + stream.Spec.DockerImageRepository = from + } + } + } else { + var tag imageapi.TagReference + if existing, ok := stream.Spec.Tags[ref.Tag]; ok { + tag = existing + delete(tag.Annotations, imageapi.DockerImageRepositoryCheckAnnotation) + } else { + tag = imageapi.TagReference{ + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: from, + }, } - stream.Spec.DockerImageRepository = from } + stream.Spec.Tags[ref.Tag] = tag } } @@ -117,7 +172,7 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg resourceVersion := stream.ResourceVersion - fmt.Fprintln(cmd.Out(), "Waiting for the import to complete, CTRL+C to stop waiting.") + fmt.Fprintln(cmd.Out(), "Importing (ctrl+c to stop waiting) ...") updatedStream, err := waitForImport(imageStreamClient, stream.Name, resourceVersion) if err != nil { @@ -127,7 +182,7 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg 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(cmd.Out(), "The import completed successfully.", "\n\n") + fmt.Fprint(cmd.Out(), "The import completed successfully.\n\n") d := describe.ImageStreamDescriber{Interface: osClient} info, err := d.Describe(updatedStream.Namespace, updatedStream.Name) From cded0de508e11b11a886b141ce5b38efacc06da6 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 14 Dec 2015 13:36:56 -0500 Subject: [PATCH 3/5] Do not retry if the UID changes on import --- pkg/image/controller/controller.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/image/controller/controller.go b/pkg/image/controller/controller.go index 7f6f14379e14..6240e6116eed 100644 --- a/pkg/image/controller/controller.go +++ b/pkg/image/controller/controller.go @@ -267,8 +267,11 @@ func (c *ImportController) done(stream *api.ImageStream, reason string, retry in stream.Annotations[api.DockerImageRepositoryCheckAnnotation] = reason if _, err := c.streams.ImageStreams(stream.Namespace).Update(stream); err != nil && !errors.IsNotFound(err) { if errors.IsConflict(err) && retry > 0 { - if stream, err := c.streams.ImageStreams(stream.Namespace).Get(stream.Name); err == nil { - return c.done(stream, reason, retry-1) + if newStream, err := c.streams.ImageStreams(stream.Namespace).Get(stream.Name); err == nil { + if stream.UID != newStream.UID { + return nil + } + return c.done(newStream, reason, retry-1) } } return err From 0902573ec0cb550de232f02d722a743d576fbd4b Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 5 Jan 2016 00:10:10 -0500 Subject: [PATCH 4/5] bump(github.com/blang/semver):31b736133b98f26d5e078ec9eb591666edfd091f --- Godeps/Godeps.json | 7 +- .../src/github.com/blang/semver/json.go | 23 + .../src/github.com/blang/semver/semver.go | 395 ++++++++++++++++++ .../src/github.com/blang/semver/sort.go | 28 ++ .../src/github.com/blang/semver/sql.go | 30 ++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 Godeps/_workspace/src/github.com/blang/semver/json.go create mode 100644 Godeps/_workspace/src/github.com/blang/semver/semver.go create mode 100644 Godeps/_workspace/src/github.com/blang/semver/sort.go create mode 100644 Godeps/_workspace/src/github.com/blang/semver/sql.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1f257c343eb3..71371cad6bc2 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -125,12 +125,17 @@ "ImportPath": "github.com/beorn7/perks/quantile", "Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d" }, + { + "ImportPath": "github.com/blang/semver", + "Comment": "v3.0.b965b613227fddccbfffe13eae360ed3fa822f8d1", + "Rev": "31b736133b98f26d5e078ec9eb591666edfd091f" + }, { "ImportPath": "github.com/boltdb/bolt", "Comment": "v1.0-65-g3b44955", "Rev": "3b449559cf34cbcc74460b59041a4399d3226e5a" }, - { + { "ImportPath": "github.com/bradfitz/http2", "Rev": "f8202bc903bda493ebba4aa54922d78430c2c42f" }, diff --git a/Godeps/_workspace/src/github.com/blang/semver/json.go b/Godeps/_workspace/src/github.com/blang/semver/json.go new file mode 100644 index 000000000000..a74bf7c44940 --- /dev/null +++ b/Godeps/_workspace/src/github.com/blang/semver/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/Godeps/_workspace/src/github.com/blang/semver/semver.go b/Godeps/_workspace/src/github.com/blang/semver/semver.go new file mode 100644 index 000000000000..bbf85ce972d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/blang/semver/semver.go @@ -0,0 +1,395 @@ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers +) + +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +// Version represents a semver compatible version +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []PRVersion + Build []string //No Precendence +} + +// Version to string +func (v Version) String() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + + if len(v.Pre) > 0 { + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) + } + } + + if len(v.Build) > 0 { + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } + } + + return string(b) +} + +// Equals checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// EQ checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// NE checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + +// GT checks if v is greater than o. +func (v Version) GT(o Version) bool { + return (v.Compare(o) == 1) +} + +// GTE checks if v is greater than or equal to o. +func (v Version) GTE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// GE checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// LT checks if v is less than o. +func (v Version) LT(o Version) bool { + return (v.Compare(o) == -1) +} + +// LTE checks if v is less than or equal to o. +func (v Version) LTE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// LE checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// Compare compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v Version) Compare(o Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } + return -1 + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } + return -1 + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } + return -1 + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + +} + +// Validate validates v and returns error in case +func (v Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + + return nil +} + +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (vp *Version, err error) { + v, err := Parse(s) + vp = &v + return +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { + return Parse(s) +} + +// Parse parses version string and returns a validated Version or error +func Parse(s string) (Version, error) { + if len(s) == 0 { + return Version{}, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return Version{}, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return Version{}, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return Version{}, err + } + + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] + } + + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] + } + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) + } + patch, err := strconv.ParseUint(patchStr, 10, 64) + if err != nil { + return Version{}, err + } + + v.Patch = patch + + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err + } + v.Pre = append(v.Pre, parsedPR) + } + + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + + return v, nil +} + +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + +// PRVersion represents a PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// NewPRVersion creates a new valid prerelease version +func NewPRVersion(s string) (PRVersion, error) { + if len(s) == 0 { + return PRVersion{}, errors.New("Prerelease is empty") + } + v := PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return PRVersion{}, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// IsNumeric checks if prerelease-version is numeric +func (v PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compare compares two PreRelease Versions v and o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v PRVersion) Compare(o PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// NewBuildVersion creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} diff --git a/Godeps/_workspace/src/github.com/blang/semver/sort.go b/Godeps/_workspace/src/github.com/blang/semver/sort.go new file mode 100644 index 000000000000..e18f880826ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/blang/semver/sort.go @@ -0,0 +1,28 @@ +package semver + +import ( + "sort" +) + +// Versions represents multiple versions. +type Versions []Version + +// Len returns length of version collection +func (s Versions) Len() int { + return len(s) +} + +// Swap swaps two versions inside the collection by its indices +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less checks if version at index i is less than version at index j +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/Godeps/_workspace/src/github.com/blang/semver/sql.go b/Godeps/_workspace/src/github.com/blang/semver/sql.go new file mode 100644 index 000000000000..eb4d802666e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/blang/semver/sql.go @@ -0,0 +1,30 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var str string + switch src := src.(type) { + case string: + str = src + case []byte: + str = string(src) + default: + return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) + } + + if t, err := Parse(str); err == nil { + *v = t + } + + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} From 730f1f52a33feb979e5996fb52aae1f6ba2293a9 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 5 Jan 2016 00:20:52 -0500 Subject: [PATCH 5/5] Implement authenticated image import Add a new API which allows images to be imported through an image stream, with the ability to optionally tag those images into a new or existing image stream. Allow the image import controller to continue to work based on annotation. Add the concept of "generation" to image streams to concretely record the state at which a tag was changed, and allow importers to track when that state was effected (into status). Add a conditions field to each status tag to report failures to users incrementally. Preserves all fundamental behaviors for legacy clients. Omitting generation on an update results in the field being preserved. Clients must set generation to 0 to request a clear. Clients are prohibited from providing their own generation. Update oc tag to not clear the top level annotation when setting the direct field. Update oc import-image to prefer using the new method to import. Update oc new-app to use the new import method, and only set a single tag if the server is detected to support tag import. Update image creation and update to extract layer names and size information into the serialized image object. --- .../v1.imagestreamimport/description.adoc | 4 + api/swagger-spec/oapi-v1.json | 539 ++++++++++- contrib/completions/bash/oc | 3 +- contrib/completions/bash/openshift | 3 +- hack/api-description-whitelist.txt | 247 +++++ hack/test-integration.sh | 2 +- hack/verify-govet.sh | 3 +- pkg/api/deep_copy_generated.go | 218 +++++ pkg/api/serialization_test.go | 7 + pkg/api/v1/conversion_generated.go | 436 +++++++++ pkg/api/v1/deep_copy_generated.go | 218 +++++ pkg/api/v1beta3/conversion_generated.go | 20 + pkg/api/v1beta3/deep_copy_generated.go | 58 ++ pkg/api/validation/register.go | 1 + pkg/authorization/api/types.go | 6 +- pkg/client/client.go | 6 + pkg/client/imagestreams.go | 34 + pkg/client/imagestreams_test.go | 56 ++ pkg/client/imagestreamsecrets.go | 44 + pkg/client/testclient/fake.go | 5 + pkg/client/testclient/fake_imagestreams.go | 12 + .../testclient/fake_imagestreamsecrets.go | 26 + pkg/cmd/cli/cmd/importimage.go | 169 +++- pkg/cmd/cli/cmd/newapp.go | 4 +- pkg/cmd/cli/cmd/newbuild.go | 4 +- pkg/cmd/cli/cmd/tag.go | 12 +- pkg/cmd/cli/describe/describer.go | 28 +- pkg/cmd/cli/describe/describer_test.go | 1 + pkg/cmd/cli/describe/helpers.go | 92 +- pkg/cmd/cli/describe/printer_test.go | 1 + pkg/cmd/server/api/types.go | 10 + pkg/cmd/server/api/v1/conversions.go | 5 + pkg/cmd/server/api/v1/types.go | 10 + pkg/cmd/server/api/v1/types_test.go | 2 + pkg/cmd/server/origin/master.go | 27 +- pkg/cmd/server/origin/master_config.go | 10 + pkg/dockerregistry/client.go | 6 +- pkg/dockerregistry/client_test.go | 14 +- pkg/generate/app/cmd/newapp.go | 13 +- pkg/generate/app/cmd/newapp_test.go | 12 +- pkg/generate/app/dockerimagelookup.go | 72 ++ pkg/generate/app/imageref.go | 74 +- pkg/generate/app/imagestreamlookup.go | 101 +- pkg/image/api/dockertypes.go | 27 +- pkg/image/api/helper.go | 334 ++++++- pkg/image/api/helper_test.go | 37 +- pkg/image/api/register.go | 4 +- pkg/image/api/types.go | 140 ++- pkg/image/api/v1/conversion.go | 59 +- pkg/image/api/v1/conversion_test.go | 1 + pkg/image/api/v1/register.go | 2 + pkg/image/api/v1/types.go | 114 +++ pkg/image/api/v1beta3/conversion.go | 20 + pkg/image/api/v1beta3/types.go | 62 +- pkg/image/api/validation/validation.go | 47 +- pkg/image/controller/controller.go | 326 ++----- pkg/image/controller/controller_test.go | 862 ++++-------------- pkg/image/controller/factory.go | 3 +- pkg/image/importer/credentials.go | 112 +++ pkg/image/importer/credentials_test.go | 39 + pkg/image/importer/importer.go | 792 ++++++++++++++++ pkg/image/importer/importer_test.go | 354 +++++++ pkg/image/registry/image/etcd/etcd_test.go | 225 +++++ pkg/image/registry/image/strategy.go | 15 + pkg/image/registry/imagesecret/rest.go | 58 ++ pkg/image/registry/imagesecret/rest_test.go | 50 + pkg/image/registry/imagestream/etcd/etcd.go | 9 +- pkg/image/registry/imagestream/strategy.go | 177 +++- pkg/image/registry/imagestreamimage/rest.go | 6 +- pkg/image/registry/imagestreamimport/rest.go | 338 +++++++ .../registry/imagestreamimport/strategy.go | 35 + pkg/image/registry/imagestreammapping/rest.go | 18 +- pkg/image/registry/imagestreamtag/rest.go | 6 +- .../registry/imagestreamtag/rest_test.go | 23 +- pkg/image/registry/imagestreamtag/strategy.go | 1 + test/cmd/builds.sh | 5 +- test/cmd/images.sh | 19 +- .../bootstrap_cluster_roles.yaml | 4 + .../bootstrap_openshift_roles.yaml | 1 + test/fixtures/image-secrets.json | 68 ++ test/integration/dockerregistryclient_test.go | 14 +- test/integration/imageimporter_test.go | 300 ++++++ 82 files changed, 6097 insertions(+), 1225 deletions(-) create mode 100644 api/definitions/v1.imagestreamimport/description.adoc create mode 100644 hack/api-description-whitelist.txt create mode 100644 pkg/client/imagestreams_test.go create mode 100644 pkg/client/imagestreamsecrets.go create mode 100644 pkg/client/testclient/fake_imagestreamsecrets.go create mode 100644 pkg/image/importer/credentials.go create mode 100644 pkg/image/importer/credentials_test.go create mode 100644 pkg/image/importer/importer.go create mode 100644 pkg/image/importer/importer_test.go create mode 100644 pkg/image/registry/imagesecret/rest.go create mode 100644 pkg/image/registry/imagesecret/rest_test.go create mode 100644 pkg/image/registry/imagestreamimport/rest.go create mode 100644 pkg/image/registry/imagestreamimport/strategy.go create mode 100644 test/fixtures/image-secrets.json create mode 100644 test/integration/imageimporter_test.go diff --git a/api/definitions/v1.imagestreamimport/description.adoc b/api/definitions/v1.imagestreamimport/description.adoc new file mode 100644 index 000000000000..4671c5a85bd9 --- /dev/null +++ b/api/definitions/v1.imagestreamimport/description.adoc @@ -0,0 +1,4 @@ +The image stream import resource provides an easy way for a user to find and import Docker images from other Docker registries into the server. Individual images or an entire image repository may be imported, and users may choose to see the results of the import prior to tagging the resulting images into the specified image stream. + +This API is intended for end-user tools that need to see the metadata of the image prior to import (for instance, to generate an application from it). Clients that know the desired image can continue to create spec.tags directly into their image streams. + diff --git a/api/swagger-spec/oapi-v1.json b/api/swagger-spec/oapi-v1.json index d717d89fa4d9..4f6e3f305d15 100644 --- a/api/swagger-spec/oapi-v1.json +++ b/api/swagger-spec/oapi-v1.json @@ -6523,6 +6523,100 @@ } ] }, + { + "path": "/oapi/v1/namespaces/{namespace}/imagestreamimports", + "description": "OpenShift REST API, version v1", + "operations": [ + { + "type": "v1.ImageStreamImport", + "method": "POST", + "summary": "create a ImageStreamImport", + "nickname": "createNamespacedImageStreamImport", + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "pretty", + "description": "If 'true', then the output is pretty printed.", + "required": false, + "allowMultiple": false + }, + { + "type": "v1.ImageStreamImport", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespace", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1.ImageStreamImport" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/oapi/v1/imagestreamimports", + "description": "OpenShift REST API, version v1", + "operations": [ + { + "type": "v1.ImageStreamImport", + "method": "POST", + "summary": "create a ImageStreamImport", + "nickname": "createImageStreamImport", + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "pretty", + "description": "If 'true', then the output is pretty printed.", + "required": false, + "allowMultiple": false + }, + { + "type": "v1.ImageStreamImport", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1.ImageStreamImport" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/oapi/v1/namespaces/{namespace}/imagestreammappings", "description": "OpenShift REST API, version v1", @@ -7278,6 +7372,89 @@ } ] }, + { + "path": "/oapi/v1/namespaces/{namespace}/imagestreams/{name}/secrets", + "description": "OpenShift REST API, version v1", + "operations": [ + { + "type": "v1.SecretList", + "method": "GET", + "summary": "read secrets of the specified SecretList", + "nickname": "readNamespacedSecretListSecrets", + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "pretty", + "description": "If 'true', then the output is pretty printed.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "required": false, + "allowMultiple": false + }, + { + "type": "boolean", + "paramType": "query", + "name": "watch", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history.", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespace", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the SecretList", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1.SecretList" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/oapi/v1/namespaces/{namespace}/imagestreams/{name}/status", "description": "OpenShift REST API, version v1", @@ -17435,6 +17612,9 @@ }, "v1.Image": { "id": "v1.Image", + "required": [ + "dockerImageLayers" + ], "properties": { "kind": { "type": "string", @@ -17462,6 +17642,31 @@ "dockerImageManifest": { "type": "string", "description": "raw JSON of the manifest" + }, + "dockerImageLayers": { + "type": "array", + "items": { + "$ref": "v1.ImageLayer" + }, + "description": "a list of the image layers from lowest to highest" + } + } + }, + "v1.ImageLayer": { + "id": "v1.ImageLayer", + "required": [ + "name", + "size" + ], + "properties": { + "name": { + "type": "string", + "description": "the name of the layer (blob, in Docker parlance)" + }, + "size": { + "type": "integer", + "format": "int64", + "description": "size of the layer in bytes" } } }, @@ -17488,11 +17693,11 @@ } } }, - "v1.ImageStreamMapping": { - "id": "v1.ImageStreamMapping", + "v1.ImageStreamImport": { + "id": "v1.ImageStreamImport", "required": [ - "image", - "tag" + "spec", + "status" ], "properties": { "kind": { @@ -17504,41 +17709,112 @@ "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" }, "metadata": { - "$ref": "v1.ObjectMeta" + "$ref": "v1.ObjectMeta", + "description": "metadata about the image stream, name is required" }, - "image": { - "$ref": "v1.Image", - "description": "a Docker image" + "spec": { + "$ref": "v1.ImageStreamImportSpec", + "description": "description of the images that the user wishes to import" }, - "tag": { - "type": "string", - "description": "string value this image can be located with inside the stream" + "status": { + "$ref": "v1.ImageStreamImportStatus", + "description": "the result of importing the image" } } }, - "v1.ImageStreamList": { - "id": "v1.ImageStreamList", + "v1.ImageStreamImportSpec": { + "id": "v1.ImageStreamImportSpec", "required": [ - "items" + "import" ], "properties": { - "kind": { - "type": "string", - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + "import": { + "type": "boolean", + "description": "if true, the images will be imported to the server and the resulting image stream will be returned in status.import" }, - "apiVersion": { - "type": "string", - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" + "repository": { + "$ref": "v1.RepositoryImportSpec", + "description": "if specified, import a single Docker repository's tags to this image stream" }, - "metadata": { - "$ref": "unversioned.ListMeta" + "images": { + "type": "array", + "items": { + "$ref": "v1.ImageImportSpec" + }, + "description": "a list of images to import into this image stream" + } + } + }, + "v1.RepositoryImportSpec": { + "id": "v1.RepositoryImportSpec", + "required": [ + "from" + ], + "properties": { + "from": { + "$ref": "v1.ObjectReference", + "description": "the source for the image repository to import; only kind DockerImage and a name of a Docker image repository is allowed" }, - "items": { + "importPolicy": { + "$ref": "v1.TagImportPolicy", + "description": "policy controlling how the image is imported" + }, + "includeManifest": { + "type": "boolean", + "description": "if true, return the manifest for each image in the response" + } + } + }, + "v1.TagImportPolicy": { + "id": "v1.TagImportPolicy", + "properties": { + "insecure": { + "type": "boolean", + "description": "if true, the server may bypass certificate verification or connect directly over HTTP during image import" + } + } + }, + "v1.ImageImportSpec": { + "id": "v1.ImageImportSpec", + "required": [ + "from" + ], + "properties": { + "from": { + "$ref": "v1.ObjectReference", + "description": "the source of an image to import; only kind DockerImage is allowed" + }, + "to": { + "$ref": "v1.LocalObjectReference", + "description": "a tag in the current image stream to assign the imported image to, if name is not specified the default tag from from.name will be used" + }, + "importPolicy": { + "$ref": "v1.TagImportPolicy", + "description": "policy controlling how the image is imported" + }, + "includeManifest": { + "type": "boolean", + "description": "if true, return the manifest for this image in the response" + } + } + }, + "v1.ImageStreamImportStatus": { + "id": "v1.ImageStreamImportStatus", + "properties": { + "import": { + "$ref": "v1.ImageStream", + "description": "if the user requested any images be imported, this field will be set with the successful image stream create or update" + }, + "repository": { + "$ref": "v1.RepositoryImportStatus", + "description": "status of the attempt to import a repository" + }, + "images": { "type": "array", "items": { - "$ref": "v1.ImageStream" + "$ref": "v1.ImageImportStatus" }, - "description": "list of image stream objects" + "description": "status of the attempt to import images" } } }, @@ -17588,7 +17864,8 @@ "v1.NamedTagReference": { "id": "v1.NamedTagReference", "required": [ - "name" + "name", + "generation" ], "properties": { "name": { @@ -17606,6 +17883,15 @@ "reference": { "type": "boolean", "description": "if true consider this tag a reference only and do not attempt to import metadata about the image" + }, + "generation": { + "type": "integer", + "format": "int64", + "description": "the generation of the image stream this was updated to" + }, + "importPolicy": { + "$ref": "v1.TagImportPolicy", + "description": "attributes controlling how this reference is imported" } } }, @@ -17645,6 +17931,13 @@ "$ref": "v1.TagEvent" }, "description": "list of tag events related to the tag" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "v1.TagEventCondition" + }, + "description": "the set of conditions that apply to this tag" } } }, @@ -17653,7 +17946,8 @@ "required": [ "created", "dockerImageReference", - "image" + "image", + "generation" ], "properties": { "created": { @@ -17667,6 +17961,197 @@ "image": { "type": "string", "description": "the image" + }, + "generation": { + "type": "integer", + "format": "int64", + "description": "the generation of the image stream spec tag this tag event represents" + } + } + }, + "v1.TagEventCondition": { + "id": "v1.TagEventCondition", + "required": [ + "type", + "status", + "generation" + ], + "properties": { + "type": { + "type": "string", + "description": "type of tag event condition, currently only ImportSuccess" + }, + "status": { + "type": "string", + "description": "status of the condition, one of True, False, Unknown" + }, + "lastTransitionTime": { + "type": "string", + "description": "last time the condition transitioned from one status to another" + }, + "reason": { + "type": "string", + "description": "machine-readable reason for the last condition transition" + }, + "message": { + "type": "string", + "description": "human-readable message indicating details of the last transition" + }, + "generation": { + "type": "integer", + "format": "int64", + "description": "the generation of the image stream spec tag this condition represents" + } + } + }, + "v1.RepositoryImportStatus": { + "id": "v1.RepositoryImportStatus", + "properties": { + "status": { + "$ref": "unversioned.Status", + "description": "the result of the import attempt, will include a reason and message if the repository could not be imported" + }, + "images": { + "type": "array", + "items": { + "$ref": "v1.ImageImportStatus" + }, + "description": "a list of the images retrieved by the import of the repository" + }, + "additionalTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "a list of additional tags on the repository that were not retrieved" + } + } + }, + "v1.ImageImportStatus": { + "id": "v1.ImageImportStatus", + "required": [ + "status" + ], + "properties": { + "status": { + "$ref": "unversioned.Status", + "description": "the status of the image import, including errors encountered while retrieving the image" + }, + "image": { + "$ref": "v1.Image", + "description": "if the image was located, the metadata of that image" + }, + "tag": { + "type": "string", + "description": "the tag this image was located under, if any" + } + } + }, + "v1.ImageStreamMapping": { + "id": "v1.ImageStreamMapping", + "required": [ + "image", + "tag" + ], + "properties": { + "kind": { + "type": "string", + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + }, + "apiVersion": { + "type": "string", + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" + }, + "metadata": { + "$ref": "v1.ObjectMeta" + }, + "image": { + "$ref": "v1.Image", + "description": "a Docker image" + }, + "tag": { + "type": "string", + "description": "string value this image can be located with inside the stream" + } + } + }, + "v1.ImageStreamList": { + "id": "v1.ImageStreamList", + "required": [ + "items" + ], + "properties": { + "kind": { + "type": "string", + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + }, + "apiVersion": { + "type": "string", + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" + }, + "metadata": { + "$ref": "unversioned.ListMeta" + }, + "items": { + "type": "array", + "items": { + "$ref": "v1.ImageStream" + }, + "description": "list of image stream objects" + } + } + }, + "v1.SecretList": { + "id": "v1.SecretList", + "description": "SecretList is a list of Secret.", + "required": [ + "items" + ], + "properties": { + "kind": { + "type": "string", + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + }, + "apiVersion": { + "type": "string", + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" + }, + "metadata": { + "$ref": "unversioned.ListMeta", + "description": "Standard list metadata. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + }, + "items": { + "type": "array", + "items": { + "$ref": "v1.Secret" + }, + "description": "Items is a list of secret objects. More info: http://releases.k8s.io/HEAD/docs/user-guide/secrets.md" + } + } + }, + "v1.Secret": { + "id": "v1.Secret", + "description": "Secret holds secret data of a certain type. The total bytes of the values in the Data field must be less than MaxSecretSize bytes.", + "properties": { + "kind": { + "type": "string", + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + }, + "apiVersion": { + "type": "string", + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources" + }, + "metadata": { + "$ref": "v1.ObjectMeta", + "description": "Standard object's metadata. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata" + }, + "data": { + "type": "any", + "description": "Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. The serialized form of the secret data is a base64 encoded string, representing the arbitrary (possibly non-string) data value here. Described in https://tools.ietf.org/html/rfc4648#section-4" + }, + "type": { + "type": "string", + "description": "Used to facilitate programmatic handling of secret data." } } }, diff --git a/contrib/completions/bash/oc b/contrib/completions/bash/oc index 6d88a23251b4..2c34eb7ad0c8 100644 --- a/contrib/completions/bash/oc +++ b/contrib/completions/bash/oc @@ -1142,9 +1142,10 @@ _oc_import-image() flags_with_completion=() flags_completion=() + flags+=("--all") flags+=("--confirm") flags+=("--from=") - flags+=("--insecure-repository") + flags+=("--insecure") flags+=("--alsologtostderr") flags+=("--api-version=") flags+=("--boot-id-file=") diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index cb7ef0dd184e..694c2f112ba5 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -6137,9 +6137,10 @@ _openshift_cli_import-image() flags_with_completion=() flags_completion=() + flags+=("--all") flags+=("--confirm") flags+=("--from=") - flags+=("--insecure-repository") + flags+=("--insecure") flags+=("--alsologtostderr") flags+=("--api-version=") flags+=("--boot-id-file=") diff --git a/hack/api-description-whitelist.txt b/hack/api-description-whitelist.txt new file mode 100644 index 000000000000..8a921cc2a881 --- /dev/null +++ b/hack/api-description-whitelist.txt @@ -0,0 +1,247 @@ +## ---------------------------------------------- ## +## THIS WHITELIST ALLOWS FOR THE VERIFY SCRIPT TO ## +## SUCCEED WHILE DESCRIPTIONS ARE ADDED. IF A NEW ## +## DESCRIPTION IS ADDED, THE CORRESPONDING ENTRY ## +## IN THIS LIST SHOULD BE REMOVED. ## +## ADDING NEW ENTRIES TO THIS LIST IS DISALLOWED ## +## ---------------------------------------------- ## +api.patch +integer +json.watchevent +patch.object +runtime.rawextension +v1.awselasticblockstorevolumesource +v1.binarybuildsource +v1.binding +v1.build +v1.buildconfiglist +v1.buildconfigspec +v1.buildconfigstatus +v1.buildlist +v1.buildlog +v1.buildoutput +v1.buildrequest +v1.buildsource +v1.buildspec +v1.buildstatus +v1.buildstrategy +v1.buildtriggerpolicy +v1.capabilities +v1.capability +v1.cephfsvolumesource +v1.clusternetwork +v1.clusternetworklist +v1.clusterpolicy +v1.clusterpolicybinding +v1.clusterpolicybindinglist +v1.clusterpolicylist +v1.clusterrole +v1.clusterrolebinding +v1.clusterrolebindinglist +v1.clusterrolelist +v1.componentcondition +v1.componentstatus +v1.componentstatuslist +v1.container +v1.containerport +v1.containerstate +v1.containerstaterunning +v1.containerstateterminated +v1.containerstatewaiting +v1.containerstatus +v1.custombuildstrategy +v1.customdeploymentstrategyparams +v1.daemonendpoint +v1.deleteoptions +v1.deploymentcause +v1.deploymentcauseimagetrigger +v1.deploymentconfiglist +v1.deploymentconfigrollback +v1.deploymentconfigrollbackspec +v1.deploymentconfigspec +v1.deploymentconfigstatus +v1.deploymentdetails +v1.deploymentstrategy +v1.deploymenttriggerimagechangeparams +v1.deploymenttriggerpolicy +v1.dockerbuildstrategy +v1.emptydirvolumesource +v1.endpointaddress +v1.endpointport +v1.endpoints +v1.endpointslist +v1.endpointsubset +v1.envvar +v1.envvarsource +v1.event +v1.eventlist +v1.eventsource +v1.execaction +v1.execnewpodhook +v1.fcvolumesource +v1.finalizername +v1.flockervolumesource +v1.gcepersistentdiskvolumesource +v1.gitbuildsource +v1.gitrepovolumesource +v1.gitsourcerevision +v1.glusterfsvolumesource +v1.group +v1.grouplist +v1.handler +v1.hostpathvolumesource +v1.hostsubnet +v1.hostsubnetlist +v1.httpgetaction +v1.identity +v1.identitylist +v1.image +v1.imagechangetrigger +v1.imagelayer +v1.imagelist +v1.imagestream +v1.imagestreamimage +v1.imagestreamlist +v1.imagestreammapping +v1.imagestreamspec +v1.imagestreamstatus +v1.imagestreamtag +v1.imagestreamtaglist +v1.iscsivolumesource +v1.lifecycle +v1.lifecyclehook +v1.limitrange +v1.limitrangeitem +v1.limitrangelist +v1.limitrangespec +v1.listmeta +v1.loadbalanceringress +v1.loadbalancerstatus +v1.localobjectreference +v1.metadatafile +v1.metadatavolumesource +v1.namedclusterrole +v1.namedclusterrolebinding +v1.namedrole +v1.namedrolebinding +v1.namedtageventlist +v1.namedtagreference +v1.namespace +v1.namespacelist +v1.namespacespec +v1.namespacestatus +v1.nfsvolumesource +v1.nodeaddress +v1.nodecondition +v1.nodedaemonendpoints +v1.nodelist +v1.nodespec +v1.nodestatus +v1.nodesysteminfo +v1.oauthaccesstoken +v1.oauthaccesstokenlist +v1.oauthauthorizetoken +v1.oauthauthorizetokenlist +v1.oauthclient +v1.oauthclientauthorization +v1.oauthclientauthorizationlist +v1.oauthclientlist +v1.objectfieldselector +v1.objectmeta +v1.objectreference +v1.parameter +v1.persistentvolumeaccessmode +v1.persistentvolumeclaimlist +v1.persistentvolumeclaimspec +v1.persistentvolumeclaimstatus +v1.persistentvolumeclaimvolumesource +v1.persistentvolumelist +v1.persistentvolumespec +v1.persistentvolumestatus +v1.podcondition +v1.podlist +v1.podspec +v1.podsecuritycontext +v1.podstatus +v1.podtemplate +v1.podtemplatelist +v1.podtemplatespec +v1.policy +v1.policybinding +v1.policybindinglist +v1.policylist +v1.policyrule +v1.probe +v1.projectlist +v1.projectrequest +v1.projectspec +v1.projectstatus +v1.rbdvolumesource +v1.recreatedeploymentstrategyparams +v1.replicationcontroller +v1.replicationcontrollerlist +v1.replicationcontrollerspec +v1.replicationcontrollerstatus +v1.resourceaccessreview +v1.resourcequota +v1.resourcequotalist +v1.resourcequotaspec +v1.resourcequotastatus +v1.resourcerequirements +v1.role +v1.rolebinding +v1.rolebindinglist +v1.rolelist +v1.rollingdeploymentstrategyparams +v1.routelist +v1.routeport +v1.routespec +v1.routestatus +v1.runasuserstrategyoptions +v1.secret +v1.secretlist +v1.secretvolumesource +v1.securitycontext +v1.securitycontextconstraints +v1.securitycontextconstraintslist +v1.selinuxcontextstrategyoptions +v1.selinuxoptions +v1.serviceaccount +v1.serviceaccountlist +v1.servicelist +v1.serviceport +v1.servicespec +v1.servicestatus +v1.sourcebuildstrategy +v1.sourcecontroluser +v1.sourcerevision +v1.status +v1.statuscause +v1.statusdetails +v1.subjectaccessreview +v1.tagevent +v1.tcpsocketaction +v1.template +v1.templatelist +v1.tlsconfig +v1.useridentitymapping +v1.userlist +v1.volume +v1.volumemount +v1.webhooktrigger +unversioned.status +unversioned.statuscause +unversioned.statusdetails +unversioned.patch +unversioned.listmeta +v1beta1.scalestatus +v1beta1.scalespec +v1.repositoryimportstatus +v1.imageimportstatus +v1.tageventcondition +v1.imagestreamimportspec +v1.tagimportpolicy +v1.imagestreamimportstatus +v1.imageimportspec +v1.repositoryimportspec + diff --git a/hack/test-integration.sh b/hack/test-integration.sh index 658cf5e6cf4b..40ce160ff81f 100755 --- a/hack/test-integration.sh +++ b/hack/test-integration.sh @@ -71,7 +71,7 @@ function exectest() { result=1 if [ -n "${VERBOSE-}" ]; then - ETCD_PORT=${ETCD_PORT} "${testexec}" -test.v -test.run="^$1$" "${@:2}" 2>&1 + ETCD_PORT=${ETCD_PORT} "${testexec}" -vmodule=*=5 -test.v -test.run="^$1$" "${@:2}" 2>&1 result=$? else out=$(ETCD_PORT=${ETCD_PORT} "${testexec}" -test.run="^$1$" "${@:2}" 2>&1) diff --git a/hack/verify-govet.sh b/hack/verify-govet.sh index bb09e88afd12..4c4261c32c62 100755 --- a/hack/verify-govet.sh +++ b/hack/verify-govet.sh @@ -23,7 +23,8 @@ FAILURE=false test_dirs=$(find_files | cut -d '/' -f 1-2 | sort -u) for test_dir in $test_dirs do - go tool vet -shadow=false $test_dir + # composites are disabled because fielderrors.ValidationErrorList{...} is incorrectly flagged + go tool vet -shadow=false -composites=false $test_dir if [ "$?" -ne 0 ] then FAILURE=true diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 8e80ad5dc380..3473cf3ec960 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -1947,6 +1947,62 @@ func deepCopy_api_Image(in imageapi.Image, out *imageapi.Image, c *conversion.Cl } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapi.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := deepCopy_api_ImageLayer(in.DockerImageLayers[i], &out.DockerImageLayers[i], c); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } + return nil +} + +func deepCopy_api_ImageImportSpec(in imageapi.ImageImportSpec, out *imageapi.ImageImportSpec, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.From); err != nil { + return err + } else { + out.From = newVal.(pkgapi.ObjectReference) + } + if in.To != nil { + if newVal, err := c.DeepCopy(in.To); err != nil { + return err + } else { + out.To = newVal.(*pkgapi.LocalObjectReference) + } + } else { + out.To = nil + } + if err := deepCopy_api_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func deepCopy_api_ImageImportStatus(in imageapi.ImageImportStatus, out *imageapi.ImageImportStatus, c *conversion.Cloner) error { + out.Tag = in.Tag + if newVal, err := c.DeepCopy(in.Status); err != nil { + return err + } else { + out.Status = newVal.(unversioned.Status) + } + if in.Image != nil { + out.Image = new(imageapi.Image) + if err := deepCopy_api_Image(*in.Image, out.Image, c); err != nil { + return err + } + } else { + out.Image = nil + } + return nil +} + +func deepCopy_api_ImageLayer(in imageapi.ImageLayer, out *imageapi.ImageLayer, c *conversion.Cloner) error { + out.Name = in.Name + out.Size = in.Size return nil } @@ -2011,6 +2067,79 @@ func deepCopy_api_ImageStreamImage(in imageapi.ImageStreamImage, out *imageapi.I return nil } +func deepCopy_api_ImageStreamImport(in imageapi.ImageStreamImport, out *imageapi.ImageStreamImport, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { + return err + } else { + out.TypeMeta = newVal.(unversioned.TypeMeta) + } + if newVal, err := c.DeepCopy(in.ObjectMeta); err != nil { + return err + } else { + out.ObjectMeta = newVal.(pkgapi.ObjectMeta) + } + if err := deepCopy_api_ImageStreamImportSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_api_ImageStreamImportStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_api_ImageStreamImportSpec(in imageapi.ImageStreamImportSpec, out *imageapi.ImageStreamImportSpec, c *conversion.Cloner) error { + out.Import = in.Import + if in.Repository != nil { + out.Repository = new(imageapi.RepositoryImportSpec) + if err := deepCopy_api_RepositoryImportSpec(*in.Repository, out.Repository, c); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportSpec, len(in.Images)) + for i := range in.Images { + if err := deepCopy_api_ImageImportSpec(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func deepCopy_api_ImageStreamImportStatus(in imageapi.ImageStreamImportStatus, out *imageapi.ImageStreamImportStatus, c *conversion.Cloner) error { + if in.Import != nil { + out.Import = new(imageapi.ImageStream) + if err := deepCopy_api_ImageStream(*in.Import, out.Import, c); err != nil { + return err + } + } else { + out.Import = nil + } + if in.Repository != nil { + out.Repository = new(imageapi.RepositoryImportStatus) + if err := deepCopy_api_RepositoryImportStatus(*in.Repository, out.Repository, c); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := deepCopy_api_ImageImportStatus(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + func deepCopy_api_ImageStreamList(in imageapi.ImageStreamList, out *imageapi.ImageStreamList, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { return err @@ -2129,6 +2258,46 @@ func deepCopy_api_ImageStreamTagList(in imageapi.ImageStreamTagList, out *imagea return nil } +func deepCopy_api_RepositoryImportSpec(in imageapi.RepositoryImportSpec, out *imageapi.RepositoryImportSpec, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.From); err != nil { + return err + } else { + out.From = newVal.(pkgapi.ObjectReference) + } + if err := deepCopy_api_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func deepCopy_api_RepositoryImportStatus(in imageapi.RepositoryImportStatus, out *imageapi.RepositoryImportStatus, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.Status); err != nil { + return err + } else { + out.Status = newVal.(unversioned.Status) + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := deepCopy_api_ImageImportStatus(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + if in.AdditionalTags != nil { + out.AdditionalTags = make([]string, len(in.AdditionalTags)) + for i := range in.AdditionalTags { + out.AdditionalTags[i] = in.AdditionalTags[i] + } + } else { + out.AdditionalTags = nil + } + return nil +} + func deepCopy_api_TagEvent(in imageapi.TagEvent, out *imageapi.TagEvent, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.Created); err != nil { return err @@ -2137,6 +2306,21 @@ func deepCopy_api_TagEvent(in imageapi.TagEvent, out *imageapi.TagEvent, c *conv } out.DockerImageReference = in.DockerImageReference out.Image = in.Image + out.Generation = in.Generation + return nil +} + +func deepCopy_api_TagEventCondition(in imageapi.TagEventCondition, out *imageapi.TagEventCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil { + return err + } else { + out.LastTransitionTime = newVal.(unversioned.Time) + } + out.Reason = in.Reason + out.Message = in.Message + out.Generation = in.Generation return nil } @@ -2151,6 +2335,21 @@ func deepCopy_api_TagEventList(in imageapi.TagEventList, out *imageapi.TagEventL } else { out.Items = nil } + if in.Conditions != nil { + out.Conditions = make([]imageapi.TagEventCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := deepCopy_api_TagEventCondition(in.Conditions[i], &out.Conditions[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + return nil +} + +func deepCopy_api_TagImportPolicy(in imageapi.TagImportPolicy, out *imageapi.TagImportPolicy, c *conversion.Cloner) error { + out.Insecure = in.Insecure return nil } @@ -2173,6 +2372,15 @@ func deepCopy_api_TagReference(in imageapi.TagReference, out *imageapi.TagRefere out.From = nil } out.Reference = in.Reference + if in.Generation != nil { + out.Generation = new(int64) + *out.Generation = *in.Generation + } else { + out.Generation = nil + } + if err := deepCopy_api_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } return nil } @@ -3009,17 +3217,27 @@ func init() { deepCopy_api_DockerConfig, deepCopy_api_DockerImage, deepCopy_api_Image, + deepCopy_api_ImageImportSpec, + deepCopy_api_ImageImportStatus, + deepCopy_api_ImageLayer, deepCopy_api_ImageList, deepCopy_api_ImageStream, deepCopy_api_ImageStreamImage, + deepCopy_api_ImageStreamImport, + deepCopy_api_ImageStreamImportSpec, + deepCopy_api_ImageStreamImportStatus, deepCopy_api_ImageStreamList, deepCopy_api_ImageStreamMapping, deepCopy_api_ImageStreamSpec, deepCopy_api_ImageStreamStatus, deepCopy_api_ImageStreamTag, deepCopy_api_ImageStreamTagList, + deepCopy_api_RepositoryImportSpec, + deepCopy_api_RepositoryImportStatus, deepCopy_api_TagEvent, + deepCopy_api_TagEventCondition, deepCopy_api_TagEventList, + deepCopy_api_TagImportPolicy, deepCopy_api_TagReference, deepCopy_api_OAuthAccessToken, deepCopy_api_OAuthAccessTokenList, diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 90dafc137580..3eae93574111 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -142,6 +142,13 @@ func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, se c.FuzzNoCustom(j) j.DockerImageRepository = "" }, + func(j *image.ImageImportSpec, c fuzz.Continue) { + c.FuzzNoCustom(j) + if j.To == nil { + // To is defaulted to be not nil + j.To = &api.LocalObjectReference{} + } + }, func(j *image.ImageStreamImage, c fuzz.Continue) { c.Fuzz(&j.Image) // because we de-embedded Image from ImageStreamImage, in order to round trip diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 459c25492a95..348be2d0cbc2 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -3720,9 +3720,67 @@ func autoconvert_api_Image_To_v1_Image(in *imageapi.Image, out *imageapiv1.Image } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapiv1.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } return nil } +func autoconvert_api_ImageImportSpec_To_v1_ImageImportSpec(in *imageapi.ImageImportSpec, out *imageapiv1.ImageImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.ImageImportSpec))(in) + } + if err := convert_api_ObjectReference_To_v1_ObjectReference(&in.From, &out.From, s); err != nil { + return err + } + if in.To != nil { + out.To = new(pkgapiv1.LocalObjectReference) + if err := convert_api_LocalObjectReference_To_v1_LocalObjectReference(in.To, out.To, s); err != nil { + return err + } + } else { + out.To = nil + } + if err := convert_api_TagImportPolicy_To_v1_TagImportPolicy(&in.ImportPolicy, &out.ImportPolicy, s); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func convert_api_ImageImportSpec_To_v1_ImageImportSpec(in *imageapi.ImageImportSpec, out *imageapiv1.ImageImportSpec, s conversion.Scope) error { + return autoconvert_api_ImageImportSpec_To_v1_ImageImportSpec(in, out, s) +} + +func autoconvert_api_ImageImportStatus_To_v1_ImageImportStatus(in *imageapi.ImageImportStatus, out *imageapiv1.ImageImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.ImageImportStatus))(in) + } + out.Tag = in.Tag + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if in.Image != nil { + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + } else { + out.Image = nil + } + return nil +} + +func convert_api_ImageImportStatus_To_v1_ImageImportStatus(in *imageapi.ImageImportStatus, out *imageapiv1.ImageImportStatus, s conversion.Scope) error { + return autoconvert_api_ImageImportStatus_To_v1_ImageImportStatus(in, out, s) +} + func autoconvert_api_ImageList_To_v1_ImageList(in *imageapi.ImageList, out *imageapiv1.ImageList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*imageapi.ImageList))(in) @@ -3793,6 +3851,96 @@ func convert_api_ImageStreamImage_To_v1_ImageStreamImage(in *imageapi.ImageStrea return autoconvert_api_ImageStreamImage_To_v1_ImageStreamImage(in, out, s) } +func autoconvert_api_ImageStreamImport_To_v1_ImageStreamImport(in *imageapi.ImageStreamImport, out *imageapiv1.ImageStreamImport, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.ImageStreamImport))(in) + } + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_api_ImageStreamImportSpec_To_v1_ImageStreamImportSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_api_ImageStreamImportStatus_To_v1_ImageStreamImportStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_api_ImageStreamImport_To_v1_ImageStreamImport(in *imageapi.ImageStreamImport, out *imageapiv1.ImageStreamImport, s conversion.Scope) error { + return autoconvert_api_ImageStreamImport_To_v1_ImageStreamImport(in, out, s) +} + +func autoconvert_api_ImageStreamImportSpec_To_v1_ImageStreamImportSpec(in *imageapi.ImageStreamImportSpec, out *imageapiv1.ImageStreamImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.ImageStreamImportSpec))(in) + } + out.Import = in.Import + if in.Repository != nil { + out.Repository = new(imageapiv1.RepositoryImportSpec) + if err := convert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec(in.Repository, out.Repository, s); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportSpec, len(in.Images)) + for i := range in.Images { + if err := convert_api_ImageImportSpec_To_v1_ImageImportSpec(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func convert_api_ImageStreamImportSpec_To_v1_ImageStreamImportSpec(in *imageapi.ImageStreamImportSpec, out *imageapiv1.ImageStreamImportSpec, s conversion.Scope) error { + return autoconvert_api_ImageStreamImportSpec_To_v1_ImageStreamImportSpec(in, out, s) +} + +func autoconvert_api_ImageStreamImportStatus_To_v1_ImageStreamImportStatus(in *imageapi.ImageStreamImportStatus, out *imageapiv1.ImageStreamImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.ImageStreamImportStatus))(in) + } + if in.Import != nil { + out.Import = new(imageapiv1.ImageStream) + if err := convert_api_ImageStream_To_v1_ImageStream(in.Import, out.Import, s); err != nil { + return err + } + } else { + out.Import = nil + } + if in.Repository != nil { + out.Repository = new(imageapiv1.RepositoryImportStatus) + if err := convert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in.Repository, out.Repository, s); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := convert_api_ImageImportStatus_To_v1_ImageImportStatus(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func convert_api_ImageStreamImportStatus_To_v1_ImageStreamImportStatus(in *imageapi.ImageStreamImportStatus, out *imageapiv1.ImageStreamImportStatus, s conversion.Scope) error { + return autoconvert_api_ImageStreamImportStatus_To_v1_ImageStreamImportStatus(in, out, s) +} + func autoconvert_api_ImageStreamList_To_v1_ImageStreamList(in *imageapi.ImageStreamList, out *imageapiv1.ImageStreamList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*imageapi.ImageStreamList))(in) @@ -3907,6 +4055,68 @@ func convert_api_ImageStreamTagList_To_v1_ImageStreamTagList(in *imageapi.ImageS return autoconvert_api_ImageStreamTagList_To_v1_ImageStreamTagList(in, out, s) } +func autoconvert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec(in *imageapi.RepositoryImportSpec, out *imageapiv1.RepositoryImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.RepositoryImportSpec))(in) + } + if err := convert_api_ObjectReference_To_v1_ObjectReference(&in.From, &out.From, s); err != nil { + return err + } + if err := convert_api_TagImportPolicy_To_v1_TagImportPolicy(&in.ImportPolicy, &out.ImportPolicy, s); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func convert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec(in *imageapi.RepositoryImportSpec, out *imageapiv1.RepositoryImportSpec, s conversion.Scope) error { + return autoconvert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec(in, out, s) +} + +func autoconvert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in *imageapi.RepositoryImportStatus, out *imageapiv1.RepositoryImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.RepositoryImportStatus))(in) + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := convert_api_ImageImportStatus_To_v1_ImageImportStatus(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + if in.AdditionalTags != nil { + out.AdditionalTags = make([]string, len(in.AdditionalTags)) + for i := range in.AdditionalTags { + out.AdditionalTags[i] = in.AdditionalTags[i] + } + } else { + out.AdditionalTags = nil + } + return nil +} + +func convert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in *imageapi.RepositoryImportStatus, out *imageapiv1.RepositoryImportStatus, s conversion.Scope) error { + return autoconvert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus(in, out, s) +} + +func autoconvert_api_TagImportPolicy_To_v1_TagImportPolicy(in *imageapi.TagImportPolicy, out *imageapiv1.TagImportPolicy, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapi.TagImportPolicy))(in) + } + out.Insecure = in.Insecure + return nil +} + +func convert_api_TagImportPolicy_To_v1_TagImportPolicy(in *imageapi.TagImportPolicy, out *imageapiv1.TagImportPolicy, s conversion.Scope) error { + return autoconvert_api_TagImportPolicy_To_v1_TagImportPolicy(in, out, s) +} + func autoconvert_v1_Image_To_api_Image(in *imageapiv1.Image, out *imageapi.Image, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*imageapiv1.Image))(in) @@ -3923,9 +4133,67 @@ func autoconvert_v1_Image_To_api_Image(in *imageapiv1.Image, out *imageapi.Image } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapi.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } return nil } +func autoconvert_v1_ImageImportSpec_To_api_ImageImportSpec(in *imageapiv1.ImageImportSpec, out *imageapi.ImageImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.ImageImportSpec))(in) + } + if err := convert_v1_ObjectReference_To_api_ObjectReference(&in.From, &out.From, s); err != nil { + return err + } + if in.To != nil { + out.To = new(pkgapi.LocalObjectReference) + if err := convert_v1_LocalObjectReference_To_api_LocalObjectReference(in.To, out.To, s); err != nil { + return err + } + } else { + out.To = nil + } + if err := convert_v1_TagImportPolicy_To_api_TagImportPolicy(&in.ImportPolicy, &out.ImportPolicy, s); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func convert_v1_ImageImportSpec_To_api_ImageImportSpec(in *imageapiv1.ImageImportSpec, out *imageapi.ImageImportSpec, s conversion.Scope) error { + return autoconvert_v1_ImageImportSpec_To_api_ImageImportSpec(in, out, s) +} + +func autoconvert_v1_ImageImportStatus_To_api_ImageImportStatus(in *imageapiv1.ImageImportStatus, out *imageapi.ImageImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.ImageImportStatus))(in) + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if in.Image != nil { + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + } else { + out.Image = nil + } + out.Tag = in.Tag + return nil +} + +func convert_v1_ImageImportStatus_To_api_ImageImportStatus(in *imageapiv1.ImageImportStatus, out *imageapi.ImageImportStatus, s conversion.Scope) error { + return autoconvert_v1_ImageImportStatus_To_api_ImageImportStatus(in, out, s) +} + func autoconvert_v1_ImageList_To_api_ImageList(in *imageapiv1.ImageList, out *imageapi.ImageList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*imageapiv1.ImageList))(in) @@ -3996,6 +4264,96 @@ func convert_v1_ImageStreamImage_To_api_ImageStreamImage(in *imageapiv1.ImageStr return autoconvert_v1_ImageStreamImage_To_api_ImageStreamImage(in, out, s) } +func autoconvert_v1_ImageStreamImport_To_api_ImageStreamImport(in *imageapiv1.ImageStreamImport, out *imageapi.ImageStreamImport, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.ImageStreamImport))(in) + } + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_v1_ImageStreamImportSpec_To_api_ImageStreamImportSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_v1_ImageStreamImportStatus_To_api_ImageStreamImportStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_v1_ImageStreamImport_To_api_ImageStreamImport(in *imageapiv1.ImageStreamImport, out *imageapi.ImageStreamImport, s conversion.Scope) error { + return autoconvert_v1_ImageStreamImport_To_api_ImageStreamImport(in, out, s) +} + +func autoconvert_v1_ImageStreamImportSpec_To_api_ImageStreamImportSpec(in *imageapiv1.ImageStreamImportSpec, out *imageapi.ImageStreamImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.ImageStreamImportSpec))(in) + } + out.Import = in.Import + if in.Repository != nil { + out.Repository = new(imageapi.RepositoryImportSpec) + if err := convert_v1_RepositoryImportSpec_To_api_RepositoryImportSpec(in.Repository, out.Repository, s); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportSpec, len(in.Images)) + for i := range in.Images { + if err := convert_v1_ImageImportSpec_To_api_ImageImportSpec(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func convert_v1_ImageStreamImportSpec_To_api_ImageStreamImportSpec(in *imageapiv1.ImageStreamImportSpec, out *imageapi.ImageStreamImportSpec, s conversion.Scope) error { + return autoconvert_v1_ImageStreamImportSpec_To_api_ImageStreamImportSpec(in, out, s) +} + +func autoconvert_v1_ImageStreamImportStatus_To_api_ImageStreamImportStatus(in *imageapiv1.ImageStreamImportStatus, out *imageapi.ImageStreamImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.ImageStreamImportStatus))(in) + } + if in.Import != nil { + out.Import = new(imageapi.ImageStream) + if err := convert_v1_ImageStream_To_api_ImageStream(in.Import, out.Import, s); err != nil { + return err + } + } else { + out.Import = nil + } + if in.Repository != nil { + out.Repository = new(imageapi.RepositoryImportStatus) + if err := convert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus(in.Repository, out.Repository, s); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := convert_v1_ImageImportStatus_To_api_ImageImportStatus(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func convert_v1_ImageStreamImportStatus_To_api_ImageStreamImportStatus(in *imageapiv1.ImageStreamImportStatus, out *imageapi.ImageStreamImportStatus, s conversion.Scope) error { + return autoconvert_v1_ImageStreamImportStatus_To_api_ImageStreamImportStatus(in, out, s) +} + func autoconvert_v1_ImageStreamList_To_api_ImageStreamList(in *imageapiv1.ImageStreamList, out *imageapi.ImageStreamList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*imageapiv1.ImageStreamList))(in) @@ -4109,6 +4467,68 @@ func convert_v1_ImageStreamTagList_To_api_ImageStreamTagList(in *imageapiv1.Imag return autoconvert_v1_ImageStreamTagList_To_api_ImageStreamTagList(in, out, s) } +func autoconvert_v1_RepositoryImportSpec_To_api_RepositoryImportSpec(in *imageapiv1.RepositoryImportSpec, out *imageapi.RepositoryImportSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.RepositoryImportSpec))(in) + } + if err := convert_v1_ObjectReference_To_api_ObjectReference(&in.From, &out.From, s); err != nil { + return err + } + if err := convert_v1_TagImportPolicy_To_api_TagImportPolicy(&in.ImportPolicy, &out.ImportPolicy, s); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func convert_v1_RepositoryImportSpec_To_api_RepositoryImportSpec(in *imageapiv1.RepositoryImportSpec, out *imageapi.RepositoryImportSpec, s conversion.Scope) error { + return autoconvert_v1_RepositoryImportSpec_To_api_RepositoryImportSpec(in, out, s) +} + +func autoconvert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus(in *imageapiv1.RepositoryImportStatus, out *imageapi.RepositoryImportStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.RepositoryImportStatus))(in) + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if in.Images != nil { + out.Images = make([]imageapi.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := convert_v1_ImageImportStatus_To_api_ImageImportStatus(&in.Images[i], &out.Images[i], s); err != nil { + return err + } + } + } else { + out.Images = nil + } + if in.AdditionalTags != nil { + out.AdditionalTags = make([]string, len(in.AdditionalTags)) + for i := range in.AdditionalTags { + out.AdditionalTags[i] = in.AdditionalTags[i] + } + } else { + out.AdditionalTags = nil + } + return nil +} + +func convert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus(in *imageapiv1.RepositoryImportStatus, out *imageapi.RepositoryImportStatus, s conversion.Scope) error { + return autoconvert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus(in, out, s) +} + +func autoconvert_v1_TagImportPolicy_To_api_TagImportPolicy(in *imageapiv1.TagImportPolicy, out *imageapi.TagImportPolicy, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*imageapiv1.TagImportPolicy))(in) + } + out.Insecure = in.Insecure + return nil +} + +func convert_v1_TagImportPolicy_To_api_TagImportPolicy(in *imageapiv1.TagImportPolicy, out *imageapi.TagImportPolicy, s conversion.Scope) error { + return autoconvert_v1_TagImportPolicy_To_api_TagImportPolicy(in, out, s) +} + func autoconvert_api_OAuthAccessToken_To_v1_OAuthAccessToken(in *oauthapi.OAuthAccessToken, out *oauthapiv1.OAuthAccessToken, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*oauthapi.OAuthAccessToken))(in) @@ -7981,10 +8401,15 @@ func init() { autoconvert_api_IdentityList_To_v1_IdentityList, autoconvert_api_Identity_To_v1_Identity, autoconvert_api_ImageChangeTrigger_To_v1_ImageChangeTrigger, + autoconvert_api_ImageImportSpec_To_v1_ImageImportSpec, + autoconvert_api_ImageImportStatus_To_v1_ImageImportStatus, autoconvert_api_ImageList_To_v1_ImageList, autoconvert_api_ImageSourcePath_To_v1_ImageSourcePath, autoconvert_api_ImageSource_To_v1_ImageSource, autoconvert_api_ImageStreamImage_To_v1_ImageStreamImage, + autoconvert_api_ImageStreamImportSpec_To_v1_ImageStreamImportSpec, + autoconvert_api_ImageStreamImportStatus_To_v1_ImageStreamImportStatus, + autoconvert_api_ImageStreamImport_To_v1_ImageStreamImport, autoconvert_api_ImageStreamList_To_v1_ImageStreamList, autoconvert_api_ImageStreamMapping_To_v1_ImageStreamMapping, autoconvert_api_ImageStreamSpec_To_v1_ImageStreamSpec, @@ -8030,6 +8455,8 @@ func init() { autoconvert_api_Project_To_v1_Project, autoconvert_api_RBDVolumeSource_To_v1_RBDVolumeSource, autoconvert_api_RecreateDeploymentStrategyParams_To_v1_RecreateDeploymentStrategyParams, + autoconvert_api_RepositoryImportSpec_To_v1_RepositoryImportSpec, + autoconvert_api_RepositoryImportStatus_To_v1_RepositoryImportStatus, autoconvert_api_ResourceAccessReviewResponse_To_v1_ResourceAccessReviewResponse, autoconvert_api_ResourceAccessReview_To_v1_ResourceAccessReview, autoconvert_api_ResourceRequirements_To_v1_ResourceRequirements, @@ -8055,6 +8482,7 @@ func init() { autoconvert_api_SubjectAccessReview_To_v1_SubjectAccessReview, autoconvert_api_TCPSocketAction_To_v1_TCPSocketAction, autoconvert_api_TLSConfig_To_v1_TLSConfig, + autoconvert_api_TagImportPolicy_To_v1_TagImportPolicy, autoconvert_api_TemplateList_To_v1_TemplateList, autoconvert_api_Template_To_v1_Template, autoconvert_api_UserIdentityMapping_To_v1_UserIdentityMapping, @@ -8139,10 +8567,15 @@ func init() { autoconvert_v1_IdentityList_To_api_IdentityList, autoconvert_v1_Identity_To_api_Identity, autoconvert_v1_ImageChangeTrigger_To_api_ImageChangeTrigger, + autoconvert_v1_ImageImportSpec_To_api_ImageImportSpec, + autoconvert_v1_ImageImportStatus_To_api_ImageImportStatus, autoconvert_v1_ImageList_To_api_ImageList, autoconvert_v1_ImageSourcePath_To_api_ImageSourcePath, autoconvert_v1_ImageSource_To_api_ImageSource, autoconvert_v1_ImageStreamImage_To_api_ImageStreamImage, + autoconvert_v1_ImageStreamImportSpec_To_api_ImageStreamImportSpec, + autoconvert_v1_ImageStreamImportStatus_To_api_ImageStreamImportStatus, + autoconvert_v1_ImageStreamImport_To_api_ImageStreamImport, autoconvert_v1_ImageStreamList_To_api_ImageStreamList, autoconvert_v1_ImageStreamMapping_To_api_ImageStreamMapping, autoconvert_v1_ImageStreamSpec_To_api_ImageStreamSpec, @@ -8188,6 +8621,8 @@ func init() { autoconvert_v1_Project_To_api_Project, autoconvert_v1_RBDVolumeSource_To_api_RBDVolumeSource, autoconvert_v1_RecreateDeploymentStrategyParams_To_api_RecreateDeploymentStrategyParams, + autoconvert_v1_RepositoryImportSpec_To_api_RepositoryImportSpec, + autoconvert_v1_RepositoryImportStatus_To_api_RepositoryImportStatus, autoconvert_v1_ResourceAccessReviewResponse_To_api_ResourceAccessReviewResponse, autoconvert_v1_ResourceAccessReview_To_api_ResourceAccessReview, autoconvert_v1_ResourceRequirements_To_api_ResourceRequirements, @@ -8213,6 +8648,7 @@ func init() { autoconvert_v1_SubjectAccessReview_To_api_SubjectAccessReview, autoconvert_v1_TCPSocketAction_To_api_TCPSocketAction, autoconvert_v1_TLSConfig_To_api_TLSConfig, + autoconvert_v1_TagImportPolicy_To_api_TagImportPolicy, autoconvert_v1_TemplateList_To_api_TemplateList, autoconvert_v1_Template_To_api_Template, autoconvert_v1_UserIdentityMapping_To_api_UserIdentityMapping, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 0a0a0971232b..07f9364233c8 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -1842,6 +1842,62 @@ func deepCopy_v1_Image(in imageapiv1.Image, out *imageapiv1.Image, c *conversion } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapiv1.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := deepCopy_v1_ImageLayer(in.DockerImageLayers[i], &out.DockerImageLayers[i], c); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } + return nil +} + +func deepCopy_v1_ImageImportSpec(in imageapiv1.ImageImportSpec, out *imageapiv1.ImageImportSpec, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.From); err != nil { + return err + } else { + out.From = newVal.(pkgapiv1.ObjectReference) + } + if in.To != nil { + if newVal, err := c.DeepCopy(in.To); err != nil { + return err + } else { + out.To = newVal.(*pkgapiv1.LocalObjectReference) + } + } else { + out.To = nil + } + if err := deepCopy_v1_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func deepCopy_v1_ImageImportStatus(in imageapiv1.ImageImportStatus, out *imageapiv1.ImageImportStatus, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.Status); err != nil { + return err + } else { + out.Status = newVal.(unversioned.Status) + } + if in.Image != nil { + out.Image = new(imageapiv1.Image) + if err := deepCopy_v1_Image(*in.Image, out.Image, c); err != nil { + return err + } + } else { + out.Image = nil + } + out.Tag = in.Tag + return nil +} + +func deepCopy_v1_ImageLayer(in imageapiv1.ImageLayer, out *imageapiv1.ImageLayer, c *conversion.Cloner) error { + out.Name = in.Name + out.Size = in.Size return nil } @@ -1906,6 +1962,79 @@ func deepCopy_v1_ImageStreamImage(in imageapiv1.ImageStreamImage, out *imageapiv return nil } +func deepCopy_v1_ImageStreamImport(in imageapiv1.ImageStreamImport, out *imageapiv1.ImageStreamImport, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { + return err + } else { + out.TypeMeta = newVal.(unversioned.TypeMeta) + } + if newVal, err := c.DeepCopy(in.ObjectMeta); err != nil { + return err + } else { + out.ObjectMeta = newVal.(pkgapiv1.ObjectMeta) + } + if err := deepCopy_v1_ImageStreamImportSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_v1_ImageStreamImportStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_v1_ImageStreamImportSpec(in imageapiv1.ImageStreamImportSpec, out *imageapiv1.ImageStreamImportSpec, c *conversion.Cloner) error { + out.Import = in.Import + if in.Repository != nil { + out.Repository = new(imageapiv1.RepositoryImportSpec) + if err := deepCopy_v1_RepositoryImportSpec(*in.Repository, out.Repository, c); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportSpec, len(in.Images)) + for i := range in.Images { + if err := deepCopy_v1_ImageImportSpec(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + +func deepCopy_v1_ImageStreamImportStatus(in imageapiv1.ImageStreamImportStatus, out *imageapiv1.ImageStreamImportStatus, c *conversion.Cloner) error { + if in.Import != nil { + out.Import = new(imageapiv1.ImageStream) + if err := deepCopy_v1_ImageStream(*in.Import, out.Import, c); err != nil { + return err + } + } else { + out.Import = nil + } + if in.Repository != nil { + out.Repository = new(imageapiv1.RepositoryImportStatus) + if err := deepCopy_v1_RepositoryImportStatus(*in.Repository, out.Repository, c); err != nil { + return err + } + } else { + out.Repository = nil + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := deepCopy_v1_ImageImportStatus(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + return nil +} + func deepCopy_v1_ImageStreamList(in imageapiv1.ImageStreamList, out *imageapiv1.ImageStreamList, c *conversion.Cloner) error { if newVal, err := c.DeepCopy(in.TypeMeta); err != nil { return err @@ -2031,6 +2160,16 @@ func deepCopy_v1_NamedTagEventList(in imageapiv1.NamedTagEventList, out *imageap } else { out.Items = nil } + if in.Conditions != nil { + out.Conditions = make([]imageapiv1.TagEventCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := deepCopy_v1_TagEventCondition(in.Conditions[i], &out.Conditions[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } return nil } @@ -2054,6 +2193,55 @@ func deepCopy_v1_NamedTagReference(in imageapiv1.NamedTagReference, out *imageap out.From = nil } out.Reference = in.Reference + if in.Generation != nil { + out.Generation = new(int64) + *out.Generation = *in.Generation + } else { + out.Generation = nil + } + if err := deepCopy_v1_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } + return nil +} + +func deepCopy_v1_RepositoryImportSpec(in imageapiv1.RepositoryImportSpec, out *imageapiv1.RepositoryImportSpec, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.From); err != nil { + return err + } else { + out.From = newVal.(pkgapiv1.ObjectReference) + } + if err := deepCopy_v1_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } + out.IncludeManifest = in.IncludeManifest + return nil +} + +func deepCopy_v1_RepositoryImportStatus(in imageapiv1.RepositoryImportStatus, out *imageapiv1.RepositoryImportStatus, c *conversion.Cloner) error { + if newVal, err := c.DeepCopy(in.Status); err != nil { + return err + } else { + out.Status = newVal.(unversioned.Status) + } + if in.Images != nil { + out.Images = make([]imageapiv1.ImageImportStatus, len(in.Images)) + for i := range in.Images { + if err := deepCopy_v1_ImageImportStatus(in.Images[i], &out.Images[i], c); err != nil { + return err + } + } + } else { + out.Images = nil + } + if in.AdditionalTags != nil { + out.AdditionalTags = make([]string, len(in.AdditionalTags)) + for i := range in.AdditionalTags { + out.AdditionalTags[i] = in.AdditionalTags[i] + } + } else { + out.AdditionalTags = nil + } return nil } @@ -2065,6 +2253,26 @@ func deepCopy_v1_TagEvent(in imageapiv1.TagEvent, out *imageapiv1.TagEvent, c *c } out.DockerImageReference = in.DockerImageReference out.Image = in.Image + out.Generation = in.Generation + return nil +} + +func deepCopy_v1_TagEventCondition(in imageapiv1.TagEventCondition, out *imageapiv1.TagEventCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil { + return err + } else { + out.LastTransitionTime = newVal.(unversioned.Time) + } + out.Reason = in.Reason + out.Message = in.Message + out.Generation = in.Generation + return nil +} + +func deepCopy_v1_TagImportPolicy(in imageapiv1.TagImportPolicy, out *imageapiv1.TagImportPolicy, c *conversion.Cloner) error { + out.Insecure = in.Insecure return nil } @@ -2901,9 +3109,15 @@ func init() { deepCopy_v1_RecreateDeploymentStrategyParams, deepCopy_v1_RollingDeploymentStrategyParams, deepCopy_v1_Image, + deepCopy_v1_ImageImportSpec, + deepCopy_v1_ImageImportStatus, + deepCopy_v1_ImageLayer, deepCopy_v1_ImageList, deepCopy_v1_ImageStream, deepCopy_v1_ImageStreamImage, + deepCopy_v1_ImageStreamImport, + deepCopy_v1_ImageStreamImportSpec, + deepCopy_v1_ImageStreamImportStatus, deepCopy_v1_ImageStreamList, deepCopy_v1_ImageStreamMapping, deepCopy_v1_ImageStreamSpec, @@ -2912,7 +3126,11 @@ func init() { deepCopy_v1_ImageStreamTagList, deepCopy_v1_NamedTagEventList, deepCopy_v1_NamedTagReference, + deepCopy_v1_RepositoryImportSpec, + deepCopy_v1_RepositoryImportStatus, deepCopy_v1_TagEvent, + deepCopy_v1_TagEventCondition, + deepCopy_v1_TagImportPolicy, deepCopy_v1_OAuthAccessToken, deepCopy_v1_OAuthAccessTokenList, deepCopy_v1_OAuthAuthorizeToken, diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index 11f890d3fb2b..10b81064d3a4 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -3729,6 +3729,16 @@ func autoconvert_api_Image_To_v1beta3_Image(in *imageapi.Image, out *imageapiv1b } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapiv1beta3.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } return nil } @@ -3920,6 +3930,16 @@ func autoconvert_v1beta3_Image_To_api_Image(in *imageapiv1beta3.Image, out *imag } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapi.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } return nil } diff --git a/pkg/api/v1beta3/deep_copy_generated.go b/pkg/api/v1beta3/deep_copy_generated.go index fd6a8463054e..b7c61fbb7afe 100644 --- a/pkg/api/v1beta3/deep_copy_generated.go +++ b/pkg/api/v1beta3/deep_copy_generated.go @@ -1850,6 +1850,22 @@ func deepCopy_v1beta3_Image(in imageapiv1beta3.Image, out *imageapiv1beta3.Image } out.DockerImageMetadataVersion = in.DockerImageMetadataVersion out.DockerImageManifest = in.DockerImageManifest + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]imageapiv1beta3.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := deepCopy_v1beta3_ImageLayer(in.DockerImageLayers[i], &out.DockerImageLayers[i], c); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } + return nil +} + +func deepCopy_v1beta3_ImageLayer(in imageapiv1beta3.ImageLayer, out *imageapiv1beta3.ImageLayer, c *conversion.Cloner) error { + out.Name = in.Name + out.Size = in.Size return nil } @@ -2021,6 +2037,16 @@ func deepCopy_v1beta3_NamedTagEventList(in imageapiv1beta3.NamedTagEventList, ou } else { out.Items = nil } + if in.Conditions != nil { + out.Conditions = make([]imageapiv1beta3.TagEventCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := deepCopy_v1beta3_TagEventCondition(in.Conditions[i], &out.Conditions[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } return nil } @@ -2044,6 +2070,15 @@ func deepCopy_v1beta3_NamedTagReference(in imageapiv1beta3.NamedTagReference, ou out.From = nil } out.Reference = in.Reference + if in.Generation != nil { + out.Generation = new(int64) + *out.Generation = *in.Generation + } else { + out.Generation = nil + } + if err := deepCopy_v1beta3_TagImportPolicy(in.ImportPolicy, &out.ImportPolicy, c); err != nil { + return err + } return nil } @@ -2055,6 +2090,26 @@ func deepCopy_v1beta3_TagEvent(in imageapiv1beta3.TagEvent, out *imageapiv1beta3 } out.DockerImageReference = in.DockerImageReference out.Image = in.Image + out.Generation = in.Generation + return nil +} + +func deepCopy_v1beta3_TagEventCondition(in imageapiv1beta3.TagEventCondition, out *imageapiv1beta3.TagEventCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil { + return err + } else { + out.LastTransitionTime = newVal.(unversioned.Time) + } + out.Reason = in.Reason + out.Message = in.Message + out.Generation = in.Generation + return nil +} + +func deepCopy_v1beta3_TagImportPolicy(in imageapiv1beta3.TagImportPolicy, out *imageapiv1beta3.TagImportPolicy, c *conversion.Cloner) error { + out.Insecure = in.Insecure return nil } @@ -2891,6 +2946,7 @@ func init() { deepCopy_v1beta3_RecreateDeploymentStrategyParams, deepCopy_v1beta3_RollingDeploymentStrategyParams, deepCopy_v1beta3_Image, + deepCopy_v1beta3_ImageLayer, deepCopy_v1beta3_ImageList, deepCopy_v1beta3_ImageStream, deepCopy_v1beta3_ImageStreamImage, @@ -2903,6 +2959,8 @@ func init() { deepCopy_v1beta3_NamedTagEventList, deepCopy_v1beta3_NamedTagReference, deepCopy_v1beta3_TagEvent, + deepCopy_v1beta3_TagEventCondition, + deepCopy_v1beta3_TagImportPolicy, deepCopy_v1beta3_OAuthAccessToken, deepCopy_v1beta3_OAuthAccessTokenList, deepCopy_v1beta3_OAuthAuthorizeToken, diff --git a/pkg/api/validation/register.go b/pkg/api/validation/register.go index e3e24ca6bc21..17b41a0fc64c 100644 --- a/pkg/api/validation/register.go +++ b/pkg/api/validation/register.go @@ -54,6 +54,7 @@ func init() { Validator.Register(&imageapi.Image{}, imagevalidation.ValidateImage, imagevalidation.ValidateImageUpdate) Validator.Register(&imageapi.ImageStream{}, imagevalidation.ValidateImageStream, imagevalidation.ValidateImageStreamUpdate) + Validator.Register(&imageapi.ImageStreamImport{}, imagevalidation.ValidateImageStreamImport, nil) Validator.Register(&imageapi.ImageStreamMapping{}, imagevalidation.ValidateImageStreamMapping, nil) Validator.Register(&imageapi.ImageStreamTag{}, imagevalidation.ValidateImageStreamTag, imagevalidation.ValidateImageStreamTagUpdate) diff --git a/pkg/authorization/api/types.go b/pkg/authorization/api/types.go index e555c665a829..c46d038e25e3 100644 --- a/pkg/authorization/api/types.go +++ b/pkg/authorization/api/types.go @@ -75,7 +75,7 @@ const ( var ( GroupsToResources = map[string][]string{ BuildGroupName: {"builds", "buildconfigs", "buildlogs", "buildconfigs/instantiate", "buildconfigs/instantiatebinary", "builds/log", "builds/clone", "buildconfigs/webhooks"}, - ImageGroupName: {"imagestreams", "imagestreammappings", "imagestreamtags", "imagestreamimages"}, + ImageGroupName: {"imagestreams", "imagestreammappings", "imagestreamtags", "imagestreamimages", "imagestreamimports"}, DeploymentGroupName: {"deployments", "deploymentconfigs", "generatedeploymentconfigs", "deploymentconfigrollbacks", "deploymentconfigs/log", "deploymentconfigs/scale"}, SDNGroupName: {"clusternetworks", "hostsubnets", "netnamespaces"}, TemplateGroupName: {"templates", "templateconfigs", "processedtemplates"}, @@ -88,7 +88,7 @@ var ( PermissionGrantingGroupName: {"roles", "rolebindings", "resourceaccessreviews" /* cluster scoped*/, "subjectaccessreviews" /* cluster scoped*/, "localresourceaccessreviews", "localsubjectaccessreviews"}, OpenshiftExposedGroupName: {BuildGroupName, ImageGroupName, DeploymentGroupName, TemplateGroupName, "routes"}, OpenshiftAllGroupName: {OpenshiftExposedGroupName, UserGroupName, OAuthGroupName, PolicyOwnerGroupName, SDNGroupName, PermissionGrantingGroupName, OpenshiftStatusGroupName, "projects", - "clusterroles", "clusterrolebindings", "clusterpolicies", "clusterpolicybindings", "images" /* cluster scoped*/, "projectrequests", "builds/details"}, + "clusterroles", "clusterrolebindings", "clusterpolicies", "clusterpolicybindings", "images" /* cluster scoped*/, "projectrequests", "builds/details", "imagestreams/secrets"}, OpenshiftStatusGroupName: {"imagestreams/status", "routes/status"}, QuotaGroupName: {"limitranges", "resourcequotas", "resourcequotausages"}, @@ -97,7 +97,7 @@ var ( KubeAllGroupName: {KubeInternalsGroupName, KubeExposedGroupName, QuotaGroupName}, KubeStatusGroupName: {"pods/status", "resourcequotas/status", "namespaces/status", "replicationcontrollers/status"}, - OpenshiftEscalatingViewableGroupName: {"oauthauthorizetokens", "oauthaccesstokens"}, + OpenshiftEscalatingViewableGroupName: {"oauthauthorizetokens", "oauthaccesstokens", "imagestreams/secrets"}, KubeEscalatingViewableGroupName: {"secrets"}, EscalatingResourcesGroupName: {OpenshiftEscalatingViewableGroupName, KubeEscalatingViewableGroupName}, diff --git a/pkg/client/client.go b/pkg/client/client.go index c8ec2e334838..22ff0ff28aed 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -23,6 +23,7 @@ type Interface interface { ImageStreamMappingsNamespacer ImageStreamTagsNamespacer ImageStreamImagesNamespacer + ImageStreamSecretsNamespacer DeploymentConfigsNamespacer DeploymentLogsNamespacer RoutesNamespacer @@ -74,6 +75,11 @@ func (c *Client) Images() ImageInterface { return newImages(c) } +// ImageStreamImages provides a REST client for retrieving image secrets in a namespace +func (c *Client) ImageStreamSecrets(namespace string) ImageStreamSecretInterface { + return newImageStreamSecrets(c, namespace) +} + // ImageStreams provides a REST client for ImageStream func (c *Client) ImageStreams(namespace string) ImageStreamInterface { return newImageStreams(c, namespace) diff --git a/pkg/client/imagestreams.go b/pkg/client/imagestreams.go index f6a45e55fe82..af0cc6200300 100644 --- a/pkg/client/imagestreams.go +++ b/pkg/client/imagestreams.go @@ -1,6 +1,10 @@ package client import ( + "errors" + + apierrs "k8s.io/kubernetes/pkg/api/errors" + kclient "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/watch" @@ -8,6 +12,8 @@ import ( imageapi "github.com/openshift/origin/pkg/image/api" ) +var ErrImageStreamImportUnsupported = errors.New("the server does not support directly importing images - create an image stream with tags or the dockerImageRepository field set") + // ImageStreamsNamespacer has methods to work with ImageStream resources in a namespace type ImageStreamsNamespacer interface { ImageStreams(namespace string) ImageStreamInterface @@ -22,6 +28,7 @@ type ImageStreamInterface interface { Delete(name string) error Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) UpdateStatus(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) + Import(isi *imageapi.ImageStreamImport) (*imageapi.ImageStreamImport, error) } // ImageStreamNamespaceGetter exposes methods to get ImageStreams by Namespace @@ -108,3 +115,30 @@ func (c *imageStreams) UpdateStatus(stream *imageapi.ImageStream) (result *image err = c.r.Put().Namespace(c.ns).Resource("imageStreams").Name(stream.Name).SubResource("status").Body(stream).Do().Into(result) return } + +// Import makes a call to the server to retrieve information about the requested images or to perform an import. ImageStreamImport +// will be returned if no actual import was requested (the to fields were not set), or an ImageStream if import was requested. +func (c *imageStreams) Import(isi *imageapi.ImageStreamImport) (*imageapi.ImageStreamImport, error) { + result := &imageapi.ImageStreamImport{} + if err := c.r.Post().Namespace(c.ns).Resource("imageStreamImports").Body(isi).Do().Into(result); err != nil { + return nil, transformUnsupported(err) + } + return result, nil +} + +// transformUnsupported converts specific error conditions to unsupported +func transformUnsupported(err error) error { + if err == nil { + return nil + } + if apierrs.IsNotFound(err) { + status, ok := err.(kclient.APIStatus) + if !ok { + return ErrImageStreamImportUnsupported + } + if status.Status().Details == nil || status.Status().Details.Kind == "" { + return ErrImageStreamImportUnsupported + } + } + return err +} diff --git a/pkg/client/imagestreams_test.go b/pkg/client/imagestreams_test.go new file mode 100644 index 000000000000..8a7e33db7b0e --- /dev/null +++ b/pkg/client/imagestreams_test.go @@ -0,0 +1,56 @@ +package client + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/latest" + "k8s.io/kubernetes/pkg/api/unversioned" + kclient "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + + "github.com/openshift/origin/pkg/image/api" +) + +type roundTripFunc func(req *http.Request) (*http.Response, error) + +func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +func TestImageStreamImportUnsupported(t *testing.T) { + testCases := []struct { + status unversioned.Status + errFn func(err error) bool + }{ + { + status: errors.NewNotFound("", "").(kclient.APIStatus).Status(), + errFn: func(err error) bool { return err == ErrImageStreamImportUnsupported }, + }, + { + status: errors.NewNotFound("Other", "").(kclient.APIStatus).Status(), + errFn: func(err error) bool { return err != ErrImageStreamImportUnsupported && errors.IsNotFound(err) }, + }, + { + status: errors.NewConflict("Other", "", nil).(kclient.APIStatus).Status(), + errFn: func(err error) bool { return err != ErrImageStreamImportUnsupported && errors.IsConflict(err) }, + }, + } + for i, test := range testCases { + c, err := New(&kclient.Config{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + buf := bytes.NewBuffer([]byte(runtime.EncodeOrDie(latest.GroupOrDie("").Codec, &test.status))) + return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(buf)}, nil + }), + }) + if err != nil { + t.Fatal(err) + } + if _, err := c.ImageStreams("test").Import(&api.ImageStreamImport{}); !test.errFn(err) { + t.Errorf("%d: error: %v", i, err) + } + } +} diff --git a/pkg/client/imagestreamsecrets.go b/pkg/client/imagestreamsecrets.go new file mode 100644 index 000000000000..6dc86855cedc --- /dev/null +++ b/pkg/client/imagestreamsecrets.go @@ -0,0 +1,44 @@ +package client + +import ( + kapi "k8s.io/kubernetes/pkg/api" +) + +// ImageStreamSecretsNamespacer has methods to work with ImageStreamSecret resources in a namespace +type ImageStreamSecretsNamespacer interface { + ImageStreamSecrets(namespace string) ImageStreamSecretInterface +} + +// ImageStreamSecretInterface exposes methods on ImageStreamSecret resources. +type ImageStreamSecretInterface interface { + // Secrets retrieves the secrets for a named image stream with the provided list options. + Secrets(name string, options kapi.ListOptions) (*kapi.SecretList, error) +} + +// imageStreamSecrets implements ImageStreamSecretsNamespacer interface +type imageStreamSecrets struct { + r *Client + ns string +} + +// newImageStreamSecrets returns an imageStreamSecrets +func newImageStreamSecrets(c *Client, namespace string) *imageStreamSecrets { + return &imageStreamSecrets{ + r: c, + ns: namespace, + } +} + +// GetSecrets returns a list of secrets for the named image stream +func (c *imageStreamSecrets) Secrets(name string, options kapi.ListOptions) (result *kapi.SecretList, err error) { + result = &kapi.SecretList{} + err = c.r.Get(). + Namespace(c.ns). + Resource("imageStreams"). + Name(name). + SubResource("secrets"). + VersionedParams(&options, kapi.Scheme). + Do(). + Into(result) + return +} diff --git a/pkg/client/testclient/fake.go b/pkg/client/testclient/fake.go index a6db7f6443b0..9b96414fc068 100644 --- a/pkg/client/testclient/fake.go +++ b/pkg/client/testclient/fake.go @@ -140,6 +140,11 @@ func (c *Fake) Images() client.ImageInterface { return &FakeImages{Fake: c} } +// ImageStreams provides a fake REST client for ImageStreams +func (c *Fake) ImageStreamSecrets(namespace string) client.ImageStreamSecretInterface { + return &FakeImageStreamSecrets{Fake: c, Namespace: namespace} +} + // ImageStreams provides a fake REST client for ImageStreams func (c *Fake) ImageStreams(namespace string) client.ImageStreamInterface { return &FakeImageStreams{Fake: c, Namespace: namespace} diff --git a/pkg/client/testclient/fake_imagestreams.go b/pkg/client/testclient/fake_imagestreams.go index 76e6ea8f7e76..50f1a48a91a8 100644 --- a/pkg/client/testclient/fake_imagestreams.go +++ b/pkg/client/testclient/fake_imagestreams.go @@ -79,3 +79,15 @@ func (c *FakeImageStreams) UpdateStatus(inObj *imageapi.ImageStream) (result *im return obj.(*imageapi.ImageStream), err } + +func (c *FakeImageStreams) Import(inObj *imageapi.ImageStreamImport) (*imageapi.ImageStreamImport, error) { + action := ktestclient.CreateActionImpl{} + action.Verb = "create" + action.Resource = "imagestreamimports" + action.Object = inObj + obj, err := c.Fake.Invokes(action, inObj) + if obj == nil { + return nil, err + } + return obj.(*imageapi.ImageStreamImport), nil +} diff --git a/pkg/client/testclient/fake_imagestreamsecrets.go b/pkg/client/testclient/fake_imagestreamsecrets.go new file mode 100644 index 000000000000..05a089eda8f4 --- /dev/null +++ b/pkg/client/testclient/fake_imagestreamsecrets.go @@ -0,0 +1,26 @@ +package testclient + +import ( + kapi "k8s.io/kubernetes/pkg/api" + ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient" + + "github.com/openshift/origin/pkg/client" +) + +// FakeImageStreamSecrets implements ImageStreamSecretInterface. Meant to be +// embedded into a struct to get a default implementation. This makes faking +// out just the methods you want to test easier. +type FakeImageStreamSecrets struct { + Fake *Fake + Namespace string +} + +var _ client.ImageStreamSecretInterface = &FakeImageStreamSecrets{} + +func (c *FakeImageStreamSecrets) Secrets(name string, options kapi.ListOptions) (*kapi.SecretList, error) { + obj, err := c.Fake.Invokes(ktestclient.NewGetAction("imagestreams/secrets", c.Namespace, name), &kapi.SecretList{}) + if obj == nil { + return nil, err + } + return obj.(*kapi.SecretList), err +} diff --git a/pkg/cmd/cli/cmd/importimage.go b/pkg/cmd/cli/cmd/importimage.go index b74aef807bba..d61cec08d36a 100644 --- a/pkg/cmd/cli/cmd/importimage.go +++ b/pkg/cmd/cli/cmd/importimage.go @@ -45,8 +45,8 @@ func NewCmdImportImage(fullName string, f *clientcmd.Factory, out io.Writer) *co } cmd.Flags().String("from", "", "A Docker image repository or tag to import images from") cmd.Flags().Bool("confirm", false, "If true, allow the image stream import location to be set or changed") - cmd.Flags().Bool("insecure-repository", false, "If true, allow the docker registry to be insecure") cmd.Flags().Bool("all", false, "If true, import all tags from the provided source on creation or if --from is specified") + cmd.Flags().Bool("insecure", false, "If true, allow importing from registries that have invalid HTTPS certificates or are hosted via HTTP") return cmd } @@ -57,7 +57,7 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg return cmdutil.UsageError(cmd, "you must specify the name of an image stream") } - streamName := args[0] + target := args[0] namespace, _, err := f.DefaultNamespace() if err != nil { return err @@ -68,40 +68,42 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg return err } + insecure := cmdutil.GetFlagBool(cmd, "insecure") from := cmdutil.GetFlagString(cmd, "from") confirm := cmdutil.GetFlagBool(cmd, "confirm") - insecure := cmdutil.GetFlagBool(cmd, "insecure-repository") all := cmdutil.GetFlagBool(cmd, "all") - if len(from) == 0 { - from = streamName - } - - ref, err := imageapi.ParseDockerImageReference(streamName) - if err != nil { + targetRef, err := imageapi.ParseDockerImageReference(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 && all: + // error out + return fmt.Errorf("cannot specify a tag %q as well as --all", target) + case len(targetRef.Tag) == 0 && !all: + // apply the default tag + targetRef.Tag = imageapi.DefaultImageTag } - if len(ref.ID) > 0 { - return fmt.Errorf("if you want to import an image by ID, use --from=%s %s", from, ref.AsRepository().Exact()) - } - // apply the default tag - if !all && len(ref.Tag) == 0 { - ref.Tag = imageapi.DefaultImageTag - } - name := ref.Name + name := targetRef.Name + tag := targetRef.Tag ->>>>>>> e650722... Make import-image a bit more flexible imageStreamClient := osClient.ImageStreams(namespace) stream, err := imageStreamClient.Get(name) if err != nil { if !errors.IsNotFound(err) { return err } - if len(from) == 0 && !confirm { + + // the stream is new + if !confirm { return fmt.Errorf("no image stream named %q exists, pass --confirm to create and import", name) } - - if len(ref.Tag) == 0 { + if len(from) == 0 { + from = target + } + if all { stream = &imageapi.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: name}, Spec: imageapi.ImageStreamSpec{DockerImageRepository: from}, @@ -111,7 +113,7 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg ObjectMeta: kapi.ObjectMeta{Name: name}, Spec: imageapi.ImageStreamSpec{ Tags: map[string]imageapi.TagReference{ - ref.Tag: { + tag: { From: &kapi.ObjectReference{ Kind: "DockerImage", Name: from, @@ -123,44 +125,137 @@ func RunImportImage(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, arg } } else { + // the stream already exists if len(stream.Spec.DockerImageRepository) == 0 && len(stream.Spec.Tags) == 0 { return fmt.Errorf("image stream has not defined anything to import") } - if len(ref.Tag) == 0 { - if len(from) != 0 { - if from != stream.Spec.DockerImageRepository { - if !confirm { - return fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) + + if all { + // importing a whole repository + if len(from) == 0 { + from = target + } + if from != stream.Spec.DockerImageRepository { + if !confirm { + if len(stream.Spec.DockerImageRepository) == 0 { + return fmt.Errorf("the image stream does not currently import an entire Docker repository, pass --confirm to update") } - stream.Spec.DockerImageRepository = from + return fmt.Errorf("the image stream has a different import spec %q, pass --confirm to update", stream.Spec.DockerImageRepository) } + stream.Spec.DockerImageRepository = from } + } else { - var tag imageapi.TagReference - if existing, ok := stream.Spec.Tags[ref.Tag]; ok { - tag = existing - delete(tag.Annotations, imageapi.DockerImageRepositoryCheckAnnotation) + // importing a single tag + + // follow any referential tags to the destination + finalTag, existing, ok, multiple := imageapi.FollowTagReference(stream, tag) + if !ok && multiple { + return 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 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 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 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 { - tag = imageapi.TagReference{ + // create a new tag + if len(from) == 0 { + from = target + } + existing = &imageapi.TagReference{ From: &kapi.ObjectReference{ Kind: "DockerImage", Name: from, }, } } - stream.Spec.Tags[ref.Tag] = tag + stream.Spec.Tags[tag] = *existing } } - if stream.Annotations != nil { - delete(stream.Annotations, imageapi.DockerImageRepositoryCheckAnnotation) + if len(from) == 0 { + // catch programmer error + return fmt.Errorf("unexpected error, from is empty") + } + + // Attempt the new, direct import path + isi := &imageapi.ImageStreamImport{ + ObjectMeta: kapi.ObjectMeta{ + Name: stream.Name, + Namespace: namespace, + ResourceVersion: stream.ResourceVersion, + }, + Spec: imageapi.ImageStreamImportSpec{Import: true}, + } + if all { + isi.Spec.Repository = &imageapi.RepositoryImportSpec{ + From: kapi.ObjectReference{ + Kind: "DockerImage", + Name: from, + }, + ImportPolicy: imageapi.TagImportPolicy{Insecure: insecure}, + } } else { - stream.Annotations = make(map[string]string) + 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}, + }) } + // TODO: add dry-run + _, err = imageStreamClient.Import(isi) + switch { + case err == client.ErrImageStreamImportUnsupported: + case err != nil: + return err + default: + fmt.Fprint(cmd.Out(), "The import completed successfully.\n\n") + + // optimization, use the image stream returned by the call + d := describe.ImageStreamDescriber{Interface: osClient} + info, err := d.Describe(namespace, stream.Name) + if err != nil { + return err + } + + fmt.Fprintln(out, info) + return nil + } + + // Legacy path, remove when support for older importers is removed + delete(stream.Annotations, imageapi.DockerImageRepositoryCheckAnnotation) if insecure { + if stream.Annotations == nil { + stream.Annotations = make(map[string]string) + } stream.Annotations[imageapi.InsecureRepositoryAnnotation] = "true" } + if stream.CreationTimestamp.IsZero() { stream, err = imageStreamClient.Create(stream) } else { diff --git a/pkg/cmd/cli/cmd/newapp.go b/pkg/cmd/cli/cmd/newapp.go index 90223e1bf960..fd70382531d1 100644 --- a/pkg/cmd/cli/cmd/newapp.go +++ b/pkg/cmd/cli/cmd/newapp.go @@ -578,8 +578,8 @@ The '%[2]s' command will match arguments to the following types: --allow-missing-images can be used to point to an image that does not exist yet. -See '%[2]s' for examples. -`, t, c.Name()) +See '%[2]s -h' for examples. +`, t, c.CommandPath()) case newapp.ErrMultipleMatches: return fmt.Errorf(err.(newapp.ErrMultipleMatches).UsageError("")) case newapp.ErrPartialMatch: diff --git a/pkg/cmd/cli/cmd/newbuild.go b/pkg/cmd/cli/cmd/newbuild.go index cac950258db0..492c45693b25 100644 --- a/pkg/cmd/cli/cmd/newbuild.go +++ b/pkg/cmd/cli/cmd/newbuild.go @@ -218,8 +218,8 @@ The '%[2]s' command will match arguments to the following types: --allow-missing-images can be used to point to an image that does not exist yet or is only on the local system. -See '%[2]s' for examples. -`, t, c.Name()) +See '%[2]s -h' for examples. +`, t, c.CommandPath()) } switch err { case newcmd.ErrNoInputs: diff --git a/pkg/cmd/cli/cmd/tag.go b/pkg/cmd/cli/cmd/tag.go index 4e35ed3bc03d..c958d2c856a7 100644 --- a/pkg/cmd/cli/cmd/tag.go +++ b/pkg/cmd/cli/cmd/tag.go @@ -340,9 +340,15 @@ func (o TagOptions) RunTag() error { localRef := o.ref switch o.sourceKind { case "DockerImage": - targetRef.From.Name = localRef.String() - // for external image we need to force re-import to fetch its metadata - delete(target.Annotations, imageapi.DockerImageRepositoryCheckAnnotation) + 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 + } default: targetRef.From.Name = localRef.NameString() targetRef.From.Namespace = o.ref.Namespace diff --git a/pkg/cmd/cli/describe/describer.go b/pkg/cmd/cli/describe/describer.go index d04f1e94612a..a0136eac99ab 100644 --- a/pkg/cmd/cli/describe/describer.go +++ b/pkg/cmd/cli/describe/describer.go @@ -440,8 +440,31 @@ func describeImage(image *imageapi.Image, imageName string) (string, error) { if len(imageName) > 0 { formatString(out, "Image Name", imageName) } - formatString(out, "Parent Image", image.DockerImageMetadata.Parent) - formatString(out, "Layer Size", units.HumanSize(float64(image.DockerImageMetadata.Size))) + switch l := len(image.DockerImageLayers); l { + case 0: + // legacy case, server does not know individual layers + formatString(out, "Layer Size", units.HumanSize(float64(image.DockerImageMetadata.Size))) + case 1: + formatString(out, "Image Size", units.HumanSize(float64(image.DockerImageMetadata.Size))) + default: + info := []string{} + if image.DockerImageLayers[0].Size > 0 { + info = append(info, fmt.Sprintf("first layer %s", units.HumanSize(float64(image.DockerImageLayers[0].Size)))) + } + for i := l - 1; i > 0; i-- { + if image.DockerImageLayers[i].Size == 0 { + continue + } + info = append(info, fmt.Sprintf("last binary layer %s", units.HumanSize(float64(image.DockerImageLayers[i].Size)))) + break + } + if len(info) > 0 { + formatString(out, "Image Size", fmt.Sprintf("%s (%s)", units.HumanSize(float64(image.DockerImageMetadata.Size)), strings.Join(info, ", "))) + } else { + formatString(out, "Image Size", units.HumanSize(float64(image.DockerImageMetadata.Size))) + } + } + //formatString(out, "Parent Image", image.DockerImageMetadata.Parent) formatString(out, "Image Created", fmt.Sprintf("%s ago", formatRelativeTime(image.DockerImageMetadata.Created.Time))) formatString(out, "Author", image.DockerImageMetadata.Author) formatString(out, "Arch", image.DockerImageMetadata.Architecture) @@ -473,6 +496,7 @@ func describeDockerImage(out *tabwriter.Writer, image *imageapi.DockerConfig) { ports.Insert(k) } formatString(out, "Exposes Ports", strings.Join(ports.List(), ", ")) + formatMapStringString(out, "Docker Labels", image.Labels) for i, env := range image.Env { if i == 0 { formatString(out, "Environment", env) diff --git a/pkg/cmd/cli/describe/describer_test.go b/pkg/cmd/cli/describe/describer_test.go index 68bcd1845122..d28449a782e3 100644 --- a/pkg/cmd/cli/describe/describer_test.go +++ b/pkg/cmd/cli/describe/describer_test.go @@ -44,6 +44,7 @@ var DescriberCoverageExceptions = []reflect.Type{ reflect.TypeOf(&deployapi.DeploymentLog{}), // normal users don't ever look at these reflect.TypeOf(&deployapi.DeploymentLogOptions{}), // normal users don't ever look at these reflect.TypeOf(&imageapi.DockerImage{}), // not a top level resource + reflect.TypeOf(&imageapi.ImageStreamImport{}), // normal users don't ever look at these reflect.TypeOf(&oauthapi.OAuthAccessToken{}), // normal users don't ever look at these reflect.TypeOf(&oauthapi.OAuthAuthorizeToken{}), // normal users don't ever look at these reflect.TypeOf(&oauthapi.OAuthClientAuthorization{}), // normal users don't ever look at these diff --git a/pkg/cmd/cli/describe/helpers.go b/pkg/cmd/cli/describe/helpers.go index 45dfe29d4dc5..603130ce925a 100644 --- a/pkg/cmd/cli/describe/helpers.go +++ b/pkg/cmd/cli/describe/helpers.go @@ -3,7 +3,8 @@ package describe import ( "bytes" "fmt" - "sort" + "regexp" + "strings" "text/tabwriter" "time" @@ -87,24 +88,32 @@ func extractAnnotations(annotations map[string]string, keys ...string) ([]string return extracted, remaining } -func formatAnnotations(out *tabwriter.Writer, m api.ObjectMeta, prefix string) { - values, annotations := extractAnnotations(m.Annotations, "description") - if len(values[0]) > 0 { - formatString(out, prefix+"Description", values[0]) - } +func formatMapStringString(out *tabwriter.Writer, label string, items map[string]string) { keys := sets.NewString() - for k := range annotations { + for k := range items { keys.Insert(k) } + if keys.Len() == 0 { + formatString(out, label, "") + return + } for i, key := range keys.List() { if i == 0 { - formatString(out, prefix+"Annotations", fmt.Sprintf("%s=%s", key, annotations[key])) + formatString(out, label, fmt.Sprintf("%s=%s", key, items[key])) } else { - fmt.Fprintf(out, "%s\t%s=%s\n", prefix, key, annotations[key]) + fmt.Fprintf(out, "%s\t%s=%s\n", "", key, items[key]) } } } +func formatAnnotations(out *tabwriter.Writer, m api.ObjectMeta, prefix string) { + values, annotations := extractAnnotations(m.Annotations, "description") + if len(values[0]) > 0 { + formatString(out, prefix+"Description", values[0]) + } + formatMapStringString(out, prefix+"Annotations", annotations) +} + var timeNowFn = func() time.Time { return time.Now() } @@ -153,6 +162,17 @@ func webhookURL(c *buildapi.BuildConfig, cli client.BuildConfigsNamespacer) map[ return result } +var reLongImageID = regexp.MustCompile(`[a-f0-9]{60,}$`) + +// shortenImagePullSpec returns a version of the pull spec intended for display, which may +// result in the image not being usable via cut-and-paste for users. +func shortenImagePullSpec(spec string) string { + if reLongImageID.MatchString(spec) { + return spec[:len(spec)-50] + "..." + } + return spec +} + func formatImageStreamTags(out *tabwriter.Writer, stream *imageapi.ImageStream) { if len(stream.Status.Tags) == 0 && len(stream.Spec.Tags) == 0 { fmt.Fprintf(out, "Tags:\t\n") @@ -168,7 +188,7 @@ func formatImageStreamTags(out *tabwriter.Writer, stream *imageapi.ImageStream) sortedTags = append(sortedTags, k) } } - sort.Strings(sortedTags) + imageapi.PrioritizeTags(sortedTags) for _, tag := range sortedTags { tagRef, ok := stream.Spec.Tags[tag] specTag := "" @@ -194,27 +214,61 @@ func formatImageStreamTags(out *tabwriter.Writer, stream *imageapi.ImageStream) specTag = "" } if taglist, ok := stream.Status.Tags[tag]; ok { - for _, event := range taglist.Items { + if len(taglist.Conditions) > 0 { + var lastTime time.Time + summary := []string{} + for _, condition := range taglist.Conditions { + if condition.LastTransitionTime.After(lastTime) { + lastTime = condition.LastTransitionTime.Time + } + switch condition.Type { + case imageapi.ImportSuccess: + if condition.Status == api.ConditionFalse { + summary = append(summary, fmt.Sprintf("import failed: %s", condition.Message)) + } + default: + summary = append(summary, string(condition.Type)) + } + } + if len(summary) > 0 { + description := strings.Join(summary, ", ") + if len(description) > 70 { + description = strings.TrimSpace(description[:70-3]) + "..." + } + d := timeNowFn().Sub(lastTime) + fmt.Fprintf(out, "%s\t%s\t%s ago\t%s\t%v\n", + tag, + shortenImagePullSpec(specTag), + units.HumanDuration(d), + "", + description) + } + } + for i, event := range taglist.Items { d := timeNowFn().Sub(event.Created.Time) image := event.Image ref, err := imageapi.ParseDockerImageReference(event.DockerImageReference) if err == nil { if ref.ID == image { - image = "" + image = "" } } + pullSpec := event.DockerImageReference + if pullSpec == specTag { + pullSpec = "" + } else { + pullSpec = shortenImagePullSpec(pullSpec) + } + specTag = shortenImagePullSpec(specTag) + if i != 0 { + tag, specTag = "", "" + } fmt.Fprintf(out, "%s\t%s\t%s ago\t%s\t%v\n", tag, specTag, units.HumanDuration(d), - event.DockerImageReference, + pullSpec, image) - if tag != "" { - tag = "" - } - if specTag != "" { - specTag = "" - } } } else { fmt.Fprintf(out, "%s\t%s\t\t\t\n", tag, specTag) diff --git a/pkg/cmd/cli/describe/printer_test.go b/pkg/cmd/cli/describe/printer_test.go index 90a71d336fde..6744d070b4b2 100644 --- a/pkg/cmd/cli/describe/printer_test.go +++ b/pkg/cmd/cli/describe/printer_test.go @@ -24,6 +24,7 @@ import ( // reason. var PrinterCoverageExceptions = []reflect.Type{ reflect.TypeOf(&imageapi.DockerImage{}), // not a top level resource + reflect.TypeOf(&imageapi.ImageStreamImport{}), // normal users don't ever look at these reflect.TypeOf(&buildapi.BuildLog{}), // just a marker type reflect.TypeOf(&buildapi.BuildLogOptions{}), // just a marker type reflect.TypeOf(&deployapi.DeploymentLog{}), // just a marker type diff --git a/pkg/cmd/server/api/types.go b/pkg/cmd/server/api/types.go index 53562970efee..266ab680df05 100644 --- a/pkg/cmd/server/api/types.go +++ b/pkg/cmd/server/api/types.go @@ -233,6 +233,9 @@ type MasterConfig struct { // ImageConfig holds options that describe how to build image names for system components ImageConfig ImageConfig + // ImagePolicyConfig controls limits and behavior for importing images + ImagePolicyConfig ImagePolicyConfig + // PolicyConfig holds information about where to locate critical pieces of bootstrapping policy PolicyConfig PolicyConfig @@ -246,6 +249,13 @@ type MasterConfig struct { NetworkConfig MasterNetworkConfig } +type ImagePolicyConfig struct { + // MaxImagesBulkImportedPerRepository controls the number of images that are imported when a user + // does a bulk import of a Docker repository. This number is set low to prevent users from + // importing large numbers of images accidentally. + MaxImagesBulkImportedPerRepository int +} + type ProjectConfig struct { // DefaultNodeSelector holds default project node label selector DefaultNodeSelector string diff --git a/pkg/cmd/server/api/v1/conversions.go b/pkg/cmd/server/api/v1/conversions.go index f825a38bec77..7403913e99c6 100644 --- a/pkg/cmd/server/api/v1/conversions.go +++ b/pkg/cmd/server/api/v1/conversions.go @@ -113,6 +113,11 @@ func init() { obj.BindNetwork = "tcp4" } }, + func(obj *ImagePolicyConfig) { + if obj.MaxImagesBulkImportedPerRepository == 0 { + obj.MaxImagesBulkImportedPerRepository = 5 + } + }, func(obj *DNSConfig) { if len(obj.BindNetwork) == 0 { obj.BindNetwork = "tcp4" diff --git a/pkg/cmd/server/api/v1/types.go b/pkg/cmd/server/api/v1/types.go index be3c473fb758..43a2ea57c488 100644 --- a/pkg/cmd/server/api/v1/types.go +++ b/pkg/cmd/server/api/v1/types.go @@ -181,6 +181,9 @@ type MasterConfig struct { // ImageConfig holds options that describe how to build image names for system components ImageConfig ImageConfig `json:"imageConfig"` + // ImagePolicyConfig controls limits and behavior for importing images + ImagePolicyConfig ImagePolicyConfig `json:"imagePolicyConfig"` + // PolicyConfig holds information about where to locate critical pieces of bootstrapping policy PolicyConfig PolicyConfig `json:"policyConfig"` @@ -194,6 +197,13 @@ type MasterConfig struct { NetworkConfig MasterNetworkConfig `json:"networkConfig"` } +type ImagePolicyConfig struct { + // MaxImagesBulkImportedPerRepository controls the number of images that are imported when a user + // does a bulk import of a Docker repository. This number is set low to prevent users from + // importing large numbers of images accidentally. + MaxImagesBulkImportedPerRepository int `json:"maxImagesBulkImportedPerRepository"` +} + type ProjectConfig struct { // DefaultNodeSelector holds default project node label selector DefaultNodeSelector string `json:"defaultNodeSelector"` diff --git a/pkg/cmd/server/api/v1/types_test.go b/pkg/cmd/server/api/v1/types_test.go index 41522f7110c8..a4c526448bac 100644 --- a/pkg/cmd/server/api/v1/types_test.go +++ b/pkg/cmd/server/api/v1/types_test.go @@ -128,6 +128,8 @@ etcdStorageConfig: imageConfig: format: "" latest: false +imagePolicyConfig: + maxImagesBulkImportedPerRepository: 0 kind: MasterConfig kubeletClientInfo: ca: "" diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index fdbce73d8eb6..7958aa8c0dc0 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -42,11 +42,16 @@ import ( deployconfigetcd "github.com/openshift/origin/pkg/deploy/registry/deployconfig/etcd" deploylogregistry "github.com/openshift/origin/pkg/deploy/registry/deploylog" deployrollback "github.com/openshift/origin/pkg/deploy/registry/rollback" + "github.com/openshift/origin/pkg/dockerregistry" + "github.com/openshift/origin/pkg/image/importer" + imageimporter "github.com/openshift/origin/pkg/image/importer" "github.com/openshift/origin/pkg/image/registry/image" imageetcd "github.com/openshift/origin/pkg/image/registry/image/etcd" + "github.com/openshift/origin/pkg/image/registry/imagesecret" "github.com/openshift/origin/pkg/image/registry/imagestream" imagestreametcd "github.com/openshift/origin/pkg/image/registry/imagestream/etcd" "github.com/openshift/origin/pkg/image/registry/imagestreamimage" + "github.com/openshift/origin/pkg/image/registry/imagestreamimport" "github.com/openshift/origin/pkg/image/registry/imagestreammapping" "github.com/openshift/origin/pkg/image/registry/imagestreamtag" accesstokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken/etcd" @@ -377,11 +382,19 @@ func (c *MasterConfig) GetRestStorage() map[string]rest.Storage { imageStorage := imageetcd.NewREST(c.EtcdHelper) imageRegistry := image.NewRegistry(imageStorage) + imageStreamSecretsStorage := imagesecret.NewREST(c.ImageStreamSecretClient()) imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage := imagestreametcd.NewREST(c.EtcdHelper, imagestream.DefaultRegistryFunc(defaultRegistryFunc), subjectAccessReviewRegistry) imageStreamRegistry := imagestream.NewRegistry(imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage) imageStreamMappingStorage := imagestreammapping.NewREST(imageRegistry, imageStreamRegistry) imageStreamTagStorage := imagestreamtag.NewREST(imageRegistry, imageStreamRegistry) imageStreamTagRegistry := imagestreamtag.NewRegistry(imageStreamTagStorage) + importerFn := func(r importer.RepositoryRetriever) imageimporter.Interface { + return imageimporter.NewImageStreamImporter(r, c.Options.ImagePolicyConfig.MaxImagesBulkImportedPerRepository, util.NewTokenBucketRateLimiter(2.0, 3)) + } + importerDockerClientFn := func() dockerregistry.Client { + return dockerregistry.NewClient(20*time.Second, false) + } + imageStreamImportStorage := imagestreamimport.NewREST(importerFn, imageStreamRegistry, internalImageStreamStorage, imageStorage, c.ImageStreamImportSecretClient(), importerDockerClientFn) imageStreamImageStorage := imagestreamimage.NewREST(imageRegistry, imageStreamRegistry) imageStreamImageRegistry := imagestreamimage.NewRegistry(imageStreamImageStorage) @@ -435,12 +448,14 @@ func (c *MasterConfig) GetRestStorage() map[string]rest.Storage { ) storage := map[string]rest.Storage{ - "images": imageStorage, - "imageStreams": imageStreamStorage, - "imageStreams/status": imageStreamStatusStorage, - "imageStreamImages": imageStreamImageStorage, - "imageStreamMappings": imageStreamMappingStorage, - "imageStreamTags": imageStreamTagStorage, + "images": imageStorage, + "imageStreams/secrets": imageStreamSecretsStorage, + "imageStreams": imageStreamStorage, + "imageStreams/status": imageStreamStatusStorage, + "imageStreamImports": imageStreamImportStorage, + "imageStreamImages": imageStreamImageStorage, + "imageStreamMappings": imageStreamMappingStorage, + "imageStreamTags": imageStreamTagStorage, "deploymentConfigs": deployConfigStorage.DeploymentConfig, "deploymentConfigs/scale": deployConfigStorage.Scale, diff --git a/pkg/cmd/server/origin/master_config.go b/pkg/cmd/server/origin/master_config.go index 1e209a453211..d513016e36c1 100644 --- a/pkg/cmd/server/origin/master_config.go +++ b/pkg/cmd/server/origin/master_config.go @@ -511,6 +511,16 @@ func (c *MasterConfig) RouteAllocatorClients() (*osclient.Client, *kclient.Clien return c.PrivilegedLoopbackOpenShiftClient, c.PrivilegedLoopbackKubernetesClient } +// ImageStreamSecretClient returns the client capable of retrieving secrets for an image secret wrapper +func (c *MasterConfig) ImageStreamSecretClient() *kclient.Client { + return c.PrivilegedLoopbackKubernetesClient +} + +// ImageStreamImportSecretClient returns the client capable of retrieving image secrets for a namespace +func (c *MasterConfig) ImageStreamImportSecretClient() *osclient.Client { + return c.PrivilegedLoopbackOpenShiftClient +} + // WebConsoleEnabled says whether web ui is not a disabled feature and asset service is configured. func (c *MasterConfig) WebConsoleEnabled() bool { return c.Options.AssetConfig != nil && !c.Options.DisabledFeatures.Has(configapi.FeatureWebConsole) diff --git a/pkg/dockerregistry/client.go b/pkg/dockerregistry/client.go index 55b4cf072888..70c47e40d826 100644 --- a/pkg/dockerregistry/client.go +++ b/pkg/dockerregistry/client.go @@ -52,16 +52,18 @@ type Connection interface { type client struct { dialTimeout time.Duration connections map[string]*connection + allowV2 bool } // NewClient returns a client object which allows public access to // a Docker registry. enableV2 allows a client to prefer V1 registry // API connections. // TODO: accept a docker auth config -func NewClient(dialTimeout time.Duration) Client { +func NewClient(dialTimeout time.Duration, allowV2 bool) Client { return &client{ dialTimeout: dialTimeout, connections: make(map[string]*connection), + allowV2: allowV2, } } @@ -77,7 +79,7 @@ func (c *client) Connect(name string, allowInsecure bool) (Connection, error) { if conn, ok := c.connections[prefix]; ok && conn.allowInsecure == allowInsecure { return conn, nil } - conn := newConnection(*target, c.dialTimeout, allowInsecure, true) + conn := newConnection(*target, c.dialTimeout, allowInsecure, c.allowV2) c.connections[prefix] = conn return conn, nil } diff --git a/pkg/dockerregistry/client_test.go b/pkg/dockerregistry/client_test.go index ab472b61e1ee..11e0d6d3e86c 100644 --- a/pkg/dockerregistry/client_test.go +++ b/pkg/dockerregistry/client_test.go @@ -26,7 +26,7 @@ func TestHTTPFallback(t *testing.T) { w.WriteHeader(http.StatusOK) })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -57,7 +57,7 @@ func TestV2Check(t *testing.T) { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.RequestURI()) })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -113,7 +113,7 @@ func TestV2CheckNoDistributionHeader(t *testing.T) { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.RequestURI()) })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func TestInsecureHTTPS(t *testing.T) { w.WriteHeader(http.StatusOK) })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -172,7 +172,7 @@ func TestProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", "secure.proxy.tld") os.Setenv("NO_PROXY", "") uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -228,7 +228,7 @@ func TestTokenExpiration(t *testing.T) { })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) if err != nil { t.Fatal(err) } @@ -288,7 +288,7 @@ func TestGetTagFallback(t *testing.T) { w.WriteHeader(http.StatusNotFound) })) uri, _ = url.Parse(server.URL) - conn, err := NewClient(10*time.Second).Connect(uri.Host, true) + conn, err := NewClient(10*time.Second, true).Connect(uri.Host, true) c := conn.(*connection) if err != nil { t.Fatal(err) diff --git a/pkg/generate/app/cmd/newapp.go b/pkg/generate/app/cmd/newapp.go index f49d8318baf0..5cb494b6fd02 100644 --- a/pkg/generate/app/cmd/newapp.go +++ b/pkg/generate/app/cmd/newapp.go @@ -173,16 +173,16 @@ func (c *AppConfig) SetClientMapper(clientMapper resource.ClientMapper) { c.clientMapper = clientMapper } -func (c *AppConfig) dockerRegistrySearcher() app.Searcher { +func (c *AppConfig) dockerImageSearcher() app.Searcher { return app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(30 * time.Second), + Client: dockerregistry.NewClient(30*time.Second, true), AllowInsecure: c.InsecureRegistry, } } func (c *AppConfig) ensureDockerSearcher() { if c.dockerSearcher == nil { - c.dockerSearcher = c.dockerRegistrySearcher() + c.dockerSearcher = c.dockerImageSearcher() } } @@ -190,7 +190,7 @@ func (c *AppConfig) ensureDockerSearcher() { func (c *AppConfig) SetDockerClient(dockerclient *docker.Client) { c.dockerSearcher = app.DockerClientSearcher{ Client: dockerclient, - RegistrySearcher: c.dockerRegistrySearcher(), + RegistrySearcher: c.dockerImageSearcher(), Insecure: c.InsecureRegistry, AllowMissingImages: c.AllowMissingImages, } @@ -222,6 +222,11 @@ func (c *AppConfig) SetOpenShiftClient(osclient client.Interface, originNamespac ClientMapper: c.clientMapper, Namespace: originNamespace, } + c.dockerSearcher = app.ImageImportSearcher{ + Client: osclient.ImageStreams(originNamespace), + AllowInsecure: c.InsecureRegistry, + Fallback: c.dockerImageSearcher(), + } } // AddArguments converts command line arguments into the appropriate bucket based on what they look like diff --git a/pkg/generate/app/cmd/newapp_test.go b/pkg/generate/app/cmd/newapp_test.go index 7174882576e1..3be1966e638b 100644 --- a/pkg/generate/app/cmd/newapp_test.go +++ b/pkg/generate/app/cmd/newapp_test.go @@ -374,7 +374,7 @@ func TestResolve(t *testing.T) { Value: "mysql:invalid", Resolver: app.UniqueExactOrInexactMatchResolver{ Searcher: app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), }, }, })}, @@ -432,7 +432,7 @@ func TestDetectSource(t *testing.T) { defer os.RemoveAll(gitLocalDir) dockerSearcher := app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), } mocks := MockSourceRepositories(t, gitLocalDir) tests := []struct { @@ -511,7 +511,7 @@ func (r *ExactMatchDockerSearcher) Search(terms ...string) (app.ComponentMatches func TestRunAll(t *testing.T) { skipExternalGit(t) dockerSearcher := app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), } tests := []struct { name string @@ -1278,7 +1278,7 @@ func PrepareAppConfig(config *AppConfig) (stdout, stderr *bytes.Buffer) { Tester: dockerfile.NewTester(), } config.dockerSearcher = app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), } config.imageStreamByAnnotationSearcher = &app.ImageStreamByAnnotationSearcher{ Client: &client.Fake{}, @@ -1301,7 +1301,7 @@ func PrepareAppConfig(config *AppConfig) (stdout, stderr *bytes.Buffer) { func TestNewBuildEnvVars(t *testing.T) { skipExternalGit(t) dockerSearcher := app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), } tests := []struct { @@ -1363,7 +1363,7 @@ func TestNewBuildEnvVars(t *testing.T) { func TestNewAppBuildConfigEnvVarsAndSecrets(t *testing.T) { skipExternalGit(t) dockerSearcher := app.DockerRegistrySearcher{ - Client: dockerregistry.NewClient(10 * time.Second), + Client: dockerregistry.NewClient(10*time.Second, true), } tests := []struct { diff --git a/pkg/generate/app/dockerimagelookup.go b/pkg/generate/app/dockerimagelookup.go index 63d6b3eb5cad..f55086b2280e 100644 --- a/pkg/generate/app/dockerimagelookup.go +++ b/pkg/generate/app/dockerimagelookup.go @@ -9,8 +9,10 @@ import ( "github.com/golang/glog" kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" utilerrors "k8s.io/kubernetes/pkg/util/errors" + "github.com/openshift/origin/pkg/client" "github.com/openshift/origin/pkg/dockerregistry" imageapi "github.com/openshift/origin/pkg/image/api" ) @@ -154,6 +156,76 @@ func (r MissingImageSearcher) Search(terms ...string) (ComponentMatches, error) return componentMatches, nil } +type ImageImportSearcher struct { + Client client.ImageStreamInterface + AllowInsecure bool + Fallback Searcher +} + +func (s ImageImportSearcher) Search(terms ...string) (ComponentMatches, error) { + isi := &imageapi.ImageStreamImport{} + for _, term := range terms { + isi.Spec.Images = append(isi.Spec.Images, imageapi.ImageImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: term}, + ImportPolicy: imageapi.TagImportPolicy{Insecure: s.AllowInsecure}, + }) + } + isi.Name = "newapp" + result, err := s.Client.Import(isi) + if err != nil { + if err == client.ErrImageStreamImportUnsupported { + return s.Fallback.Search(terms...) + } + return nil, fmt.Errorf("can't lookup images: %v", err) + } + + var lastErr error + componentMatches := ComponentMatches{} + for i, image := range result.Status.Images { + term := result.Spec.Images[i].From.Name + if image.Status.Status != unversioned.StatusSuccess { + glog.V(4).Infof("image import failed: %#v", image) + // TODO: handle differently? + lastErr = fmt.Errorf("can't import image %q (%s): %s", term, image.Status.Reason, image.Status.Message) + continue + } + ref, err := imageapi.ParseDockerImageReference(term) + if err != nil { + glog.V(4).Infof("image import failed, can't parse ref %q: %v", term, err) + continue + } + if len(ref.Tag) == 0 { + ref.Tag = imageapi.DefaultImageTag + } + if len(ref.Registry) == 0 { + ref.Registry = "Docker Hub" + } + + match := &ComponentMatch{ + Value: term, + Argument: fmt.Sprintf("--docker-image=%q", term), + Name: term, + Description: descriptionFor(&image.Image.DockerImageMetadata, term, ref.Registry, ref.Tag), + Score: 0, + Image: &image.Image.DockerImageMetadata, + ImageTag: ref.Tag, + Insecure: s.AllowInsecure, + Meta: map[string]string{"registry": ref.Registry, "direct-tag": "1"}, + } + glog.V(2).Infof("Adding %s as component match for %q with score %v", match.Description, term, match.Score) + componentMatches = append(componentMatches, match) + } + + if len(componentMatches) == 0 { + if lastErr != nil { + return nil, lastErr + } + return nil, ErrNoMatch{value: terms[0]} + } + + return componentMatches, nil +} + // DockerRegistrySearcher searches for images in a given docker registry. // Notice that it only matches exact searches - so a search for "rub" will // not return images with the name "ruby". diff --git a/pkg/generate/app/imageref.go b/pkg/generate/app/imageref.go index bf8b77af46fc..ba4639b8c428 100644 --- a/pkg/generate/app/imageref.go +++ b/pkg/generate/app/imageref.go @@ -140,6 +140,11 @@ type ImageRef struct { OutputImage bool Insecure bool HasEmptyDir bool + // If true, create the image stream using a tag for this reference, not a bulk + // import. + TagDirectly bool + // If set, the default tag for other components that reference this image + InternalDefaultTag string Env Environment @@ -157,7 +162,7 @@ func (r *ImageRef) Exists() bool { return r.Stream != nil } -// ObjectReference returns an object reference from the image reference +// ObjectReference returns an object reference to this ref (as it would exist during generation) func (r *ImageRef) ObjectReference() kapi.ObjectReference { switch { case r.Stream != nil: @@ -167,9 +172,10 @@ func (r *ImageRef) ObjectReference() kapi.ObjectReference { Namespace: r.Stream.Namespace, } case r.AsImageStream: + name, _ := r.SuggestName() return kapi.ObjectReference{ Kind: "ImageStreamTag", - Name: imageapi.JoinImageStreamTag(r.Reference.Name, r.Reference.Tag), + Name: imageapi.JoinImageStreamTag(name, r.InternalTag()), } default: return kapi.ObjectReference{ @@ -179,11 +185,22 @@ func (r *ImageRef) ObjectReference() kapi.ObjectReference { } } +func (r *ImageRef) InternalTag() string { + tag := r.Reference.Tag + if len(tag) == 0 { + tag = r.InternalDefaultTag + } + if len(tag) == 0 { + tag = imageapi.DefaultImageTag + } + return tag +} + func (r *ImageRef) PullSpec() string { if r.AsResolvedImage && r.ResolvedReference != nil { - return r.ResolvedReference.String() + return r.ResolvedReference.Exact() } - return r.Reference.String() + return r.Reference.Exact() } // RepoName returns the name of the image in namespace/name format @@ -261,7 +278,12 @@ func (r *ImageRef) ImageStream() (*imageapi.ImageStream, error) { Name: name, }, } - if !r.OutputImage { + if r.OutputImage { + return stream, nil + } + + // Legacy path, talking to a server that cannot do granular import of exact image stream spec tags. + if !r.TagDirectly { // Ignore AsResolvedImage here because we are attempting to get images from this location. stream.Spec.DockerImageRepository = r.Reference.AsRepository().String() if r.Insecure { @@ -269,6 +291,20 @@ func (r *ImageRef) ImageStream() (*imageapi.ImageStream, error) { imageapi.InsecureRepositoryAnnotation: "true", } } + return stream, nil + } + + if stream.Spec.Tags == nil { + stream.Spec.Tags = make(map[string]imageapi.TagReference) + } + stream.Spec.Tags[r.InternalTag()] = imageapi.TagReference{ + // Make this a constant + Annotations: map[string]string{"openshift.io/imported-from": r.Reference.Exact()}, + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: r.PullSpec(), + }, + ImportPolicy: imageapi.TagImportPolicy{Insecure: r.Insecure}, } return stream, nil @@ -281,30 +317,14 @@ func (r *ImageRef) DeployableContainer() (container *kapi.Container, triggers [] return nil, nil, fmt.Errorf("unable to suggest a container name for the image %q", r.Reference.String()) } if r.AsImageStream { - tag := r.Reference.Tag - if len(tag) == 0 { - tag = imageapi.DefaultImageTag - } - imageChangeParams := &deployapi.DeploymentTriggerImageChangeParams{ - Automatic: true, - ContainerNames: []string{name}, - } - if r.Stream != nil { - imageChangeParams.From = kapi.ObjectReference{ - Kind: "ImageStreamTag", - Name: imageapi.JoinImageStreamTag(r.Stream.Name, tag), - Namespace: r.Stream.Namespace, - } - } else { - imageChangeParams.From = kapi.ObjectReference{ - Kind: "ImageStreamTag", - Name: imageapi.JoinImageStreamTag(name, tag), - } - } triggers = []deployapi.DeploymentTriggerPolicy{ { - Type: deployapi.DeploymentTriggerOnImageChange, - ImageChangeParams: imageChangeParams, + Type: deployapi.DeploymentTriggerOnImageChange, + ImageChangeParams: &deployapi.DeploymentTriggerImageChangeParams{ + Automatic: true, + ContainerNames: []string{name}, + From: r.ObjectReference(), + }, }, } } diff --git a/pkg/generate/app/imagestreamlookup.go b/pkg/generate/app/imagestreamlookup.go index 379e8c019a31..b51305116718 100644 --- a/pkg/generate/app/imagestreamlookup.go +++ b/pkg/generate/app/imagestreamlookup.go @@ -53,55 +53,65 @@ func (r ImageStreamSearcher) Search(terms ...string) (ComponentMatches, error) { score, scored := imageStreamScorer(*stream, ref.Name) if !scored { glog.V(2).Infof("unscored %s: %v", stream.Name, score) - } else { - imageref, _ := imageapi.ParseDockerImageReference(term) - imageref.Name = stream.Name - matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) - latest := imageapi.LatestTaggedImage(stream, searchTag) - if latest == nil || len(latest.Image) == 0 { - glog.V(2).Infof("no image recorded for %s/%s:%s", stream.Namespace, stream.Name, searchTag) - componentMatches = append(componentMatches, &ComponentMatch{ - Value: imageref.Exact(), - Argument: fmt.Sprintf("--image-stream=%q", matchName), - Name: matchName, - Description: fmt.Sprintf("Image stream %s in project %s", stream.Name, stream.Namespace), - Score: 0.5 + score, - ImageStream: stream, - ImageTag: searchTag, - }) - continue - } + continue + } - imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(stream.Name, latest.Image) - if err != nil { - if errors.IsNotFound(err) { - // continue searching - glog.V(2).Infof("tag %q is set, but image %q has been removed", searchTag, latest.Image) - continue - } - return nil, err - } - imageData := imageStreamImage.Image + // indicate the server knows how to directly tag images + var meta map[string]string + if stream.Generation > 0 { + meta = map[string]string{"direct-tag": "1"} + } - imageref.Registry = "" + imageref, _ := imageapi.ParseDockerImageReference(term) + imageref.Name = stream.Name + imageref.Registry = "" + matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) - match := &ComponentMatch{ + latest := imageapi.LatestTaggedImage(stream, searchTag) + if latest == nil || len(latest.Image) == 0 { + glog.V(2).Infof("no image recorded for %s/%s:%s", stream.Namespace, stream.Name, searchTag) + componentMatches = append(componentMatches, &ComponentMatch{ Value: imageref.Exact(), Argument: fmt.Sprintf("--image-stream=%q", matchName), Name: matchName, - Description: fmt.Sprintf("Image stream %q (tag %q) in project %q", stream.Name, searchTag, stream.Namespace), - Score: score, + Description: fmt.Sprintf("Image stream %s in project %s", stream.Name, stream.Namespace), + Score: 0.5 + score, ImageStream: stream, - Image: &imageData.DockerImageMetadata, ImageTag: searchTag, + Meta: meta, + }) + continue + } + + imageStreamImage, err := r.ImageStreamImages.ImageStreamImages(namespace).Get(stream.Name, latest.Image) + if err != nil { + if errors.IsNotFound(err) { + // continue searching + glog.V(2).Infof("tag %q is set, but image %q has been removed", searchTag, latest.Image) + continue } - glog.V(2).Infof("Adding %s as component match for %q with score %v", match.Description, term, match.Score) - if match.Score == 0.0 { - foundExactInNamespace = true - } - componentMatches = append(componentMatches, match) + return nil, err + } + + imageData := imageStreamImage.Image + match := &ComponentMatch{ + Value: imageref.Exact(), + Argument: fmt.Sprintf("--image-stream=%q", imageref.Exact()), + Name: imageref.Name, + Description: fmt.Sprintf("Image stream %q (tag %q) in project %q", stream.Name, searchTag, stream.Namespace), + Score: score, + ImageStream: stream, + Image: &imageData.DockerImageMetadata, + ImageTag: searchTag, + Meta: meta, } + glog.V(2).Infof("Adding %s as component match for %q with score %v", match.Description, term, match.Score) + if match.Score == 0.0 { + foundExactInNamespace = true + } + componentMatches = append(componentMatches, match) } + // If we found an exact match in this namespace, do not continue looking at // other namespaces if foundExactInNamespace && r.StopOnMatch { @@ -123,6 +133,9 @@ func InputImageFromMatch(match *ComponentMatch) (*ImageRef, error) { if err != nil { return nil, err } + if match.Meta["direct-tag"] == "1" { + input.TagDirectly = true + } input.AsImageStream = true input.Info = match.Image return input, nil @@ -132,6 +145,10 @@ func InputImageFromMatch(match *ComponentMatch) (*ImageRef, error) { if err != nil { return nil, err } + if match.Meta["direct-tag"] == "1" { + input.TagDirectly = true + input.AsResolvedImage = true + } input.AsImageStream = !match.LocalOnly input.Info = match.Image input.Insecure = match.Insecure @@ -233,6 +250,13 @@ func (r *ImageStreamByAnnotationSearcher) annotationMatches(stream *imageapi.Ima if imageStream == nil { continue } + + // indicate the server knows how to directly tag images + var meta map[string]string + if imageStream.Generation > 0 { + meta = map[string]string{"direct-tag": "1"} + } + imageData := imageStream.Image matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) glog.V(5).Infof("ImageStreamAnnotationSearcher match found: %s for %s with score %f", matchName, value, score) @@ -246,6 +270,7 @@ func (r *ImageStreamByAnnotationSearcher) annotationMatches(stream *imageapi.Ima ImageStream: stream, Image: &imageData.DockerImageMetadata, ImageTag: tag, + Meta: meta, } matches = append(matches, match) } diff --git a/pkg/image/api/dockertypes.go b/pkg/image/api/dockertypes.go index 54a1e79d7e76..44fac888abb7 100644 --- a/pkg/image/api/dockertypes.go +++ b/pkg/image/api/dockertypes.go @@ -2,6 +2,8 @@ package api import ( "k8s.io/kubernetes/pkg/api/unversioned" + + "github.com/docker/distribution" ) // DockerImage is the type representing a docker image and its various properties when @@ -55,12 +57,19 @@ type DockerConfig struct { // DockerImageManifest represents the Docker v2 image format. type DockerImageManifest struct { - SchemaVersion int `json:"schemaVersion"` - Name string `json:"name"` - Tag string `json:"tag"` - Architecture string `json:"architecture"` - FSLayers []DockerFSLayer `json:"fsLayers"` - History []DockerHistory `json:"history"` + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType,omitempty"` + + // schema1 + Name string `json:"name"` + Tag string `json:"tag"` + Architecture string `json:"architecture"` + FSLayers []DockerFSLayer `json:"fsLayers"` + History []DockerHistory `json:"history"` + + // schema2 + Layers []distribution.Descriptor `json:"layers"` + Config distribution.Descriptor `json:"config"` } // DockerFSLayer is a container struct for BlobSums defined in an image manifest @@ -91,3 +100,9 @@ type DockerV1CompatibilityImage struct { Architecture string `json:"architecture,omitempty"` Size int64 `json:"size,omitempty"` } + +// DockerV1CompatibilityImageSize represents the structured v1 +// compatibility information for size +type DockerV1CompatibilityImageSize struct { + Size int64 `json:"size,omitempty"` +} diff --git a/pkg/image/api/helper.go b/pkg/image/api/helper.go index 4652b3efdb90..86de7bc657ca 100644 --- a/pkg/image/api/helper.go +++ b/pkg/image/api/helper.go @@ -3,12 +3,16 @@ package api import ( "encoding/json" "fmt" + "net/url" + "regexp" + "sort" "strings" "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/util/sets" + "github.com/blang/semver" "github.com/docker/distribution/digest" "github.com/golang/glog" ) @@ -160,6 +164,22 @@ func (r DockerImageReference) AsRepository() DockerImageReference { return r } +// RepositoryName returns the registry relative name +func (r DockerImageReference) RepositoryName() string { + r.Tag = "" + r.ID = "" + r.Registry = "" + return r.Exact() +} + +// RepositoryName returns the registry relative name +func (r DockerImageReference) RegistryURL() *url.URL { + return &url.URL{ + Scheme: "https", + Host: r.AsV2().Registry, + } +} + // DaemonMinimal clears defaults that Docker assumes. func (r DockerImageReference) DaemonMinimal() DockerImageReference { if r.Namespace == "library" { @@ -172,6 +192,32 @@ func (r DockerImageReference) DaemonMinimal() DockerImageReference { return r.Minimal() } +func (r DockerImageReference) AsV2() DockerImageReference { + switch r.Registry { + case "index.docker.io", "docker.io": + r.Registry = "registry-1.docker.io" + } + return r +} + +// MostSpecific returns the most specific image reference that can be constructed from the +// current ref, preferring an ID over a Tag. Allows client code dealing with both tags and IDs +// to get the most specific reference easily. +func (r DockerImageReference) MostSpecific() DockerImageReference { + if len(r.ID) == 0 { + return r + } + if _, err := digest.ParseDigest(r.ID); err == nil { + r.Tag = "" + return r + } + if len(r.Tag) == 0 { + r.Tag, r.ID = r.ID, "" + return r + } + return r +} + // NameString returns the name of the reference with its tag or ID. func (r DockerImageReference) NameString() string { switch { @@ -252,45 +298,83 @@ func NormalizeImageStreamTag(name string) string { return name } -// ImageWithMetadata returns a copy of image with the DockerImageMetadata filled in +// ImageWithMetadata modifies the image to fill in DockerImageMetadata // from the raw DockerImageManifest data stored in the image. -func ImageWithMetadata(image Image) (*Image, error) { +func ImageWithMetadata(image *Image) error { if len(image.DockerImageManifest) == 0 { - return &image, nil + return nil } manifestData := image.DockerImageManifest - image.DockerImageManifest = "" - manifest := DockerImageManifest{} if err := json.Unmarshal([]byte(manifestData), &manifest); err != nil { - return nil, err + return err } - if len(manifest.History) == 0 { - // should never have an empty history, but just in case... - return &image, nil - } + switch manifest.SchemaVersion { + case 0: + // legacy config object + case 1: + if len(manifest.History) == 0 { + // should never have an empty history, but just in case... + return nil + } - v1Metadata := DockerV1CompatibilityImage{} - if err := json.Unmarshal([]byte(manifest.History[0].DockerV1Compatibility), &v1Metadata); err != nil { - return nil, err - } + v1Metadata := DockerV1CompatibilityImage{} + if err := json.Unmarshal([]byte(manifest.History[0].DockerV1Compatibility), &v1Metadata); err != nil { + return err + } + + image.DockerImageLayers = make([]ImageLayer, len(manifest.FSLayers)) + for i, layer := range manifest.FSLayers { + image.DockerImageLayers[i].Name = layer.DockerBlobSum + } + if len(manifest.History) == len(image.DockerImageLayers) { + image.DockerImageLayers[0].Size = v1Metadata.Size + var size = DockerV1CompatibilityImageSize{} + for i, obj := range manifest.History[1:] { + size.Size = 0 + if err := json.Unmarshal([]byte(obj.DockerV1Compatibility), &size); err != nil { + continue + } + image.DockerImageLayers[i+1].Size = size.Size + } + } else { + glog.V(4).Infof("Imported image has mismatched layer count and history count, not updating image metadata: %s", image.Name) + } + // reverse order of the layers for v1 (lowest = 0, highest = i) + for i, j := 0, len(image.DockerImageLayers)-1; i < j; i, j = i+1, j-1 { + image.DockerImageLayers[i], image.DockerImageLayers[j] = image.DockerImageLayers[j], image.DockerImageLayers[i] + } - image.DockerImageMetadata.ID = v1Metadata.ID - image.DockerImageMetadata.Parent = v1Metadata.Parent - image.DockerImageMetadata.Comment = v1Metadata.Comment - image.DockerImageMetadata.Created = v1Metadata.Created - image.DockerImageMetadata.Container = v1Metadata.Container - image.DockerImageMetadata.ContainerConfig = v1Metadata.ContainerConfig - image.DockerImageMetadata.DockerVersion = v1Metadata.DockerVersion - image.DockerImageMetadata.Author = v1Metadata.Author - image.DockerImageMetadata.Config = v1Metadata.Config - image.DockerImageMetadata.Architecture = v1Metadata.Architecture - image.DockerImageMetadata.Size = v1Metadata.Size + image.DockerImageMetadata.ID = v1Metadata.ID + image.DockerImageMetadata.Parent = v1Metadata.Parent + image.DockerImageMetadata.Comment = v1Metadata.Comment + image.DockerImageMetadata.Created = v1Metadata.Created + image.DockerImageMetadata.Container = v1Metadata.Container + image.DockerImageMetadata.ContainerConfig = v1Metadata.ContainerConfig + image.DockerImageMetadata.DockerVersion = v1Metadata.DockerVersion + image.DockerImageMetadata.Author = v1Metadata.Author + image.DockerImageMetadata.Config = v1Metadata.Config + image.DockerImageMetadata.Architecture = v1Metadata.Architecture + if len(image.DockerImageLayers) > 0 { + size := int64(0) + for _, layer := range image.DockerImageLayers { + size += layer.Size + } + image.DockerImageMetadata.Size = size + } else { + image.DockerImageMetadata.Size = v1Metadata.Size + } + case 2: + // TODO: need to prepare for this + return fmt.Errorf("unrecognized Docker image manifest schema %d for %q (%s)", manifest.SchemaVersion, image.Name, image.DockerImageReference) + default: + return fmt.Errorf("unrecognized Docker image manifest schema %d for %q (%s)", manifest.SchemaVersion, image.Name, image.DockerImageReference) + } - return &image, nil + return nil } // DockerImageReferenceForStream returns a DockerImageReference that represents @@ -306,6 +390,34 @@ func DockerImageReferenceForStream(stream *ImageStream) (DockerImageReference, e return ParseDockerImageReference(spec) } +// FollowTagReference walks through the defined tags on a stream, following any referential tags in the stream. +// Will return ok if the tag is valid, multiple if the tag had at least reference, and ref and finalTag will be the last +// tag seen. If a circular loop is found ok will be false. +func FollowTagReference(stream *ImageStream, tag string) (finalTag string, ref *TagReference, ok bool, multiple bool) { + seen := sets.NewString() + for { + if seen.Has(tag) { + // circular reference + return tag, nil, false, multiple + } + seen.Insert(tag) + + tagRef, ok := stream.Spec.Tags[tag] + if !ok { + // no tag at the end of the rainbow + return tag, nil, false, multiple + } + if tagRef.From == nil || tagRef.From.Kind != "ImageStreamTag" || strings.Contains(tagRef.From.Name, ":") { + // terminating tag + return tag, &tagRef, true, multiple + } + + // follow the referenec + tag = tagRef.From.Name + multiple = true + } +} + // LatestTaggedImage returns the most recent TagEvent for the specified image // repository and tag. Will resolve lookups for the empty tag. Returns nil // if tag isn't present in stream.status.tags. @@ -328,7 +440,7 @@ func LatestTaggedImage(stream *ImageStream, tag string) *TagEvent { // AddTagEventToImageStream attempts to update the given image stream with a tag event. It will // collapse duplicate entries - returning true if a change was made or false if no change -// occurred. +// occurred. Any successful tag resets the status field. func AddTagEventToImageStream(stream *ImageStream, tag string, next TagEvent) bool { if stream.Status.Tags == nil { stream.Status.Tags = make(map[string]TagEventList) @@ -342,24 +454,30 @@ func AddTagEventToImageStream(stream *ImageStream, tag string, next TagEvent) bo previous := &tags.Items[0] - // image reference has not changed - if previous.DockerImageReference == next.DockerImageReference { - if next.Image == previous.Image { - return false - } - previous.Image = next.Image - stream.Status.Tags[tag] = tags - return true - } + sameRef := previous.DockerImageReference == next.DockerImageReference + sameImage := previous.Image == next.Image + sameGen := previous.Generation == next.Generation + + switch { + // shouldn't change the tag + case sameRef && sameImage && sameGen: + return false - // image has not changed, but image reference has - if next.Image == previous.Image { + case sameImage && sameRef: + // collapse the tag + case sameRef: + previous.Image = next.Image + case sameImage: previous.DockerImageReference = next.DockerImageReference + default: + // shouldn't collapse the tag + tags.Conditions = nil + tags.Items = append([]TagEvent{next}, tags.Items...) stream.Status.Tags[tag] = tags return true } - - tags.Items = append([]TagEvent{next}, tags.Items...) + previous.Generation = next.Generation + tags.Conditions = nil stream.Status.Tags[tag] = tags return true } @@ -370,13 +488,17 @@ func AddTagEventToImageStream(stream *ImageStream, tag string, next TagEvent) bo func UpdateChangedTrackingTags(new, old *ImageStream) int { changes := 0 for newTag, newImages := range new.Status.Tags { - if oldImages, ok := old.Status.Tags[newTag]; ok { - changed, deleted := tagsChanged(oldImages.Items, newImages.Items) + if len(newImages.Items) == 0 { + continue + } + if old != nil { + oldImages := old.Status.Tags[newTag] + changed, deleted := tagsChanged(newImages.Items, oldImages.Items) if !changed || deleted { continue } - changes += UpdateTrackingTags(new, newTag, newImages.Items[0]) } + changes += UpdateTrackingTags(new, newTag, newImages.Items[0]) } return changes } @@ -435,6 +557,7 @@ func UpdateTrackingTags(stream *ImageStream, updatedTag string, updatedImage Tag continue } + // TODO: this is probably wrong - we should require ":", but we can't break old clients tagRefName := tagRef.From.Name parts := strings.Split(tagRefName, ":") tag := "" @@ -505,6 +628,22 @@ func ResolveImageID(stream *ImageStream, imageID string) (*TagEvent, error) { } } +// MostAccuratePullSpec returns a docker image reference that uses the current ID if possible, the current tag otherwise, and +// returns false if the reference if the spec could not be parsed. The returned spec has all client defaults applied. +func MostAccuratePullSpec(pullSpec string, id, tag string) (string, bool) { + ref, err := ParseDockerImageReference(pullSpec) + if err != nil { + return pullSpec, false + } + if len(id) > 0 { + ref.ID = id + } + if len(tag) > 0 { + ref.Tag = tag + } + return ref.MostSpecific().Exact(), true +} + // ShortDockerImageID returns a short form of the provided DockerImage ID for display func ShortDockerImageID(image *DockerImage, length int) string { id := image.ID @@ -516,3 +655,112 @@ func ShortDockerImageID(image *DockerImage, length int) string { } return id } + +// SetTagConditions applies the specified conditions to the status of the given tag. +func SetTagConditions(stream *ImageStream, tag string, conditions ...TagEventCondition) { + tagEvents := stream.Status.Tags[tag] + tagEvents.Conditions = conditions + if stream.Status.Tags == nil { + stream.Status.Tags = make(map[string]TagEventList) + } + stream.Status.Tags[tag] = tagEvents +} + +// LatestObservedTagGeneration returns the generation value for the given tag that has been observed by the controller +// monitoring the image stream. If the tag has not been observed, the generation is zero. +func LatestObservedTagGeneration(stream *ImageStream, tag string) int64 { + tagEvents, ok := stream.Status.Tags[tag] + if !ok { + return 0 + } + + // find the most recent generation + lastGen := int64(0) + if items := tagEvents.Items; len(items) > 0 { + tagEvent := items[0] + if tagEvent.Generation > lastGen { + lastGen = tagEvent.Generation + } + } + for _, condition := range tagEvents.Conditions { + if condition.Type != ImportSuccess { + continue + } + if condition.Generation > lastGen { + lastGen = condition.Generation + } + break + } + return lastGen +} + +var ( + reMajorSemantic = regexp.MustCompile(`^[\d]+$`) + reMinorSemantic = regexp.MustCompile(`^[\d]+\.[\d]+$`) +) + +// PrioritizeTags orders a set of image tags with a few conventions: +// +// 1. the "latest" tag, if present, should be first +// 2. any tags that represent a semantic major version ("5", "v5") should be next, in descending order +// 3. any tags that represent a semantic minor version ("5.1", "v5.1") should be next, in descending order +// 4. any tags that represent a full semantic version ("5.1.3-other", "v5.1.3-other") should be next, in descending order +// 5. any remaining tags should be sorted in lexicographic order +// +// The method updates the tags in place. +func PrioritizeTags(tags []string) { + remaining := tags + finalTags := make([]string, 0, len(tags)) + for i, tag := range tags { + if tag == DefaultImageTag { + tags[0], tags[i] = tags[i], tags[0] + finalTags = append(finalTags, tags[0]) + remaining = tags[1:] + break + } + } + + exact := make(map[string]string) + var major, minor, micro semver.Versions + other := make([]string, 0, len(remaining)) + for _, tag := range remaining { + short := strings.TrimLeft(tag, "v") + v, err := semver.Parse(short) + switch { + case err == nil: + exact[v.String()] = tag + micro = append(micro, v) + continue + case reMajorSemantic.MatchString(short): + if v, err = semver.Parse(short + ".0.0"); err == nil { + exact[v.String()] = tag + major = append(major, v) + continue + } + case reMinorSemantic.MatchString(short): + if v, err = semver.Parse(short + ".0"); err == nil { + exact[v.String()] = tag + minor = append(minor, v) + continue + } + } + other = append(other, tag) + } + sort.Sort(sort.Reverse(major)) + sort.Sort(sort.Reverse(minor)) + sort.Sort(sort.Reverse(micro)) + sort.Sort(sort.StringSlice(other)) + for _, v := range major { + finalTags = append(finalTags, exact[v.String()]) + } + for _, v := range minor { + finalTags = append(finalTags, exact[v.String()]) + } + for _, v := range micro { + finalTags = append(finalTags, exact[v.String()]) + } + for _, v := range other { + finalTags = append(finalTags, v) + } + copy(tags, finalTags) +} diff --git a/pkg/image/api/helper_test.go b/pkg/image/api/helper_test.go index 87e530983183..7ab44f41cf1c 100644 --- a/pkg/image/api/helper_test.go +++ b/pkg/image/api/helper_test.go @@ -1,7 +1,6 @@ package api import ( - "fmt" "reflect" "strings" "testing" @@ -472,11 +471,13 @@ func TestImageWithMetadata(t *testing.T) { image: Image{ DockerImageManifest: `{"name": "library/ubuntu", "tag": "latest"}`, }, - expectedImage: Image{}, + expectedImage: Image{ + DockerImageManifest: `{"name": "library/ubuntu", "tag": "latest"}`, + }, }, "error unmarshalling v1 compat": { image: Image{ - DockerImageManifest: `{"name": "library/ubuntu", "tag": "latest", "history": ["v1Compatibility": "{ not valid {{ json" }`, + DockerImageManifest: "{\"name\": \"library/ubuntu\", \"tag\": \"latest\", \"history\": [\"v1Compatibility\": \"{ not valid {{ json\" }", }, expectError: true, }, @@ -486,7 +487,14 @@ func TestImageWithMetadata(t *testing.T) { ObjectMeta: kapi.ObjectMeta{ Name: "id", }, - DockerImageManifest: "", + DockerImageManifest: validImageWithManifestData().DockerImageManifest, + DockerImageLayers: []ImageLayer{ + {Name: "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 0}, + {Name: "tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd", Size: 188097705}, + {Name: "tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171", Size: 194533}, + {Name: "tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0", Size: 1895}, + {Name: "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 0}, + }, DockerImageMetadata: DockerImage{ ID: "2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7", Parent: "117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c", @@ -547,14 +555,15 @@ func TestImageWithMetadata(t *testing.T) { OnBuild: []string{}, }, Architecture: "amd64", - Size: 0, + Size: 188294133, }, }, }, } for name, test := range tests { - imageWithMetadata, err := ImageWithMetadata(test.image) + imageWithMetadata := test.image + err := ImageWithMetadata(&imageWithMetadata) gotError := err != nil if e, a := test.expectError, gotError; e != a { t.Fatalf("%s: expectError=%t, gotError=%t: %s", name, e, a, err) @@ -562,10 +571,8 @@ func TestImageWithMetadata(t *testing.T) { if test.expectError { continue } - if e, a := test.expectedImage, *imageWithMetadata; !kapi.Semantic.DeepEqual(e, a) { - stringE := fmt.Sprintf("%#v", e) - stringA := fmt.Sprintf("%#v", a) - t.Errorf("%s: image: %s", name, util.StringDiff(stringE, stringA)) + if e, a := test.expectedImage, imageWithMetadata; !kapi.Semantic.DeepEqual(e, a) { + t.Errorf("%s: image: %s", name, util.ObjectDiff(e, a)) } } } @@ -801,7 +808,7 @@ func TestAddTagEventToImageStream(t *testing.T) { t.Errorf("%s: expected updated=%t, got %t", name, e, a) } if e, a := test.expectedTags, stream.Status.Tags; !reflect.DeepEqual(e, a) { - t.Errorf("%s: expected tags=%v, got %v", name, e, a) + t.Errorf("%s: expected\ntags=%#v\ngot=%#v", name, e, a) } } } @@ -1148,3 +1155,11 @@ func TestDockerImageReferenceEquality(t *testing.T) { } } } + +func TestPrioritizeTags(t *testing.T) { + tags := []string{"5", "other", "latest", "v5.5", "v6", "5.2.3", "v5.3.6-bother", "5.3.6-abba", "5.6"} + PrioritizeTags(tags) + if !reflect.DeepEqual(tags, []string{"latest", "v6", "5", "5.6", "v5.5", "v5.3.6-bother", "5.3.6-abba", "5.2.3", "other"}) { + t.Errorf("unexpected order: %v", tags) + } +} diff --git a/pkg/image/api/register.go b/pkg/image/api/register.go index 89bc9ef412f0..cd57e45af607 100644 --- a/pkg/image/api/register.go +++ b/pkg/image/api/register.go @@ -8,13 +8,14 @@ func init() { api.Scheme.AddKnownTypes("", &Image{}, &ImageList{}, + &DockerImage{}, &ImageStream{}, &ImageStreamList{}, &ImageStreamMapping{}, &ImageStreamTag{}, &ImageStreamTagList{}, &ImageStreamImage{}, - &DockerImage{}, + &ImageStreamImport{}, ) } @@ -27,3 +28,4 @@ func (*ImageStreamMapping) IsAnAPIObject() {} func (*ImageStreamTag) IsAnAPIObject() {} func (*ImageStreamTagList) IsAnAPIObject() {} func (*ImageStreamImage) IsAnAPIObject() {} +func (*ImageStreamImport) IsAnAPIObject() {} diff --git a/pkg/image/api/types.go b/pkg/image/api/types.go index fbc200e7bf96..e5fefbc1bdc4 100644 --- a/pkg/image/api/types.go +++ b/pkg/image/api/types.go @@ -25,6 +25,9 @@ const ( // InsecureRepositoryAnnotation may be set true on an image stream to allow insecure access to pull content. InsecureRepositoryAnnotation = "openshift.io/image.insecureRepository" + // ExcludeImageSecretAnnotation indicates that a secret should not be returned by imagestream/secrets. + ExcludeImageSecretAnnotation = "openshift.io/image.excludeSecret" + // DefaultImageTag is used when an image tag is needed and the configuration does not specify a tag to use. DefaultImageTag = "latest" ) @@ -42,6 +45,16 @@ type Image struct { DockerImageMetadataVersion string // The raw JSON of the manifest DockerImageManifest string + // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. + DockerImageLayers []ImageLayer +} + +// ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. +type ImageLayer struct { + // Name of the layer as defined by the underlying store. + Name string + // Size of the layer as defined by the underlying store. + Size int64 } // ImageStreamList is a list of ImageStream objects. @@ -73,14 +86,32 @@ type ImageStreamSpec struct { Tags map[string]TagReference } -// TagReference specifies optional annotations for images using this tag and an optional reference to an ImageStreamTag, ImageStreamImage, or DockerImage this tag should track. +// TagReference specifies optional annotations for images using this tag and an optional reference to +// an ImageStreamTag, ImageStreamImage, or DockerImage this tag should track. type TagReference struct { // Optional; if specified, annotations that are applied to images retrieved via ImageStreamTags. Annotations map[string]string - // Optional; if specified, a reference to another image that this tag should point to. Valid values are ImageStreamTag, ImageStreamImage, and DockerImage. + // Optional; if specified, a reference to another image that this tag should point to. Valid values + // are ImageStreamTag, ImageStreamImage, and DockerImage. From *kapi.ObjectReference - // Reference states if the tag will be imported. Default value is false, which means the tag will be imported. + // Reference states if the tag will be imported. Default value is false, which means the tag will + // be imported. Reference bool + // Generation is a counter that tracks mutations to the spec tag (user intent). When a tag reference + // is changed the generation is set to match the current stream generation (which is incremented every + // time spec is changed). Other processes in the system like the image importer observe that the + // generation of spec tag is newer than the generation recorded in the status and use that as a trigger + // to import the newest remote tag. To trigger a new import, clients may set this value to zero which + // will reset the generation to the latest stream generation. Legacy clients will send this value as + // nil which will be merged with the current tag generation. + Generation *int64 + // ImportPolicy is information that controls how images may be imported by the server. + ImportPolicy TagImportPolicy +} + +type TagImportPolicy struct { + // Insecure is true if the server may bypass certificate verification or connect directly over HTTP during image import. + Insecure bool } // ImageStreamStatus contains information about the state of this image stream. @@ -96,6 +127,8 @@ type ImageStreamStatus struct { // TagEventList contains a historical record of images associated with a tag. type TagEventList struct { Items []TagEvent + // Conditions is an array of conditions that apply to the tag event list. + Conditions []TagEventCondition } // TagEvent is used by ImageRepositoryStatus to keep a historical record of images associated with a tag. @@ -106,6 +139,35 @@ type TagEvent struct { DockerImageReference string // The image Image string + // Generation is the spec tag generation that resulted in this tag being updated + Generation int64 +} + +type TagEventConditionType string + +// These are valid conditions of TagEvents. +const ( + // ImportSuccess with status False means the import of the specific tag failed + ImportSuccess TagEventConditionType = "ImportSuccess" +) + +// TagEventCondition contains condition information for a tag event. +type TagEventCondition struct { + // Type of tag event condition, currently only ImportSuccess + Type TagEventConditionType + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus + // LastTransitionTIme is the time the condition transitioned from one status to another. + LastTransitionTime unversioned.Time + // Reason is a brief machine readable explanation for the condition's last transition. + Reason string + // Message is a human readable description of the details about last transition, complementing reason. + Message string + // Generation is the spec tag generation that this status corresponds to. If this value is + // older than the spec tag generation, the user has requested this status tag be updated. + // This value is set to zero for older versions of streams, which means that no generation + // was recorded. + Generation int64 } // ImageStreamMapping represents a mapping from a single tag to a Docker image as @@ -157,3 +219,75 @@ type DockerImageReference struct { Tag string ID string } + +// ImageStreamImport allows a caller to request information about a set of images for possible +// import into an image stream, or actually tag the images into the image stream. +type ImageStreamImport struct { + unversioned.TypeMeta + // ObjectMeta must identify the name of the image stream to create or update. If resourceVersion + // or UID are set, they must match the image stream that will be loaded from the server. + kapi.ObjectMeta + + // Spec is the set of items desired to be imported + Spec ImageStreamImportSpec + // Status is the result of the import + Status ImageStreamImportStatus +} + +// ImageStreamImportSpec defines what images should be imported. +type ImageStreamImportSpec struct { + // Import indicates whether to perform an import - if so, the specified tags are set on the spec + // and status of the image stream defined by the type meta. + Import bool + // Repository is an optional import of an entire Docker image repository. A maximum limit on the + // number of tags imported this way is imposed by the server. + Repository *RepositoryImportSpec + // Images are a list of individual images to import. + Images []ImageImportSpec +} + +// ImageStreamImportStatus contains information about the status of an image stream import. +type ImageStreamImportStatus struct { + // Import is the image stream that was successfully updated or created when 'to' was set. + Import *ImageStream + // Repository is set if spec.repository was set to the outcome of the import + Repository *RepositoryImportStatus + // Images is set with the result of importing spec.images + Images []ImageImportStatus +} + +// RepositoryImport indicates to load a set of tags from a given Docker image repository +type RepositoryImportSpec struct { + // The source of the import, only kind DockerImage is supported + From kapi.ObjectReference + + ImportPolicy TagImportPolicy + IncludeManifest bool +} + +// RepositoryImportStatus describes the outcome of the repository import +type RepositoryImportStatus struct { + // Status reflects whether any failure occurred during import + Status unversioned.Status + // Images is the list of imported images + Images []ImageImportStatus + // AdditionalTags are tags that exist in the repository but were not imported because + // a maximum limit of automatic imports was applied. + AdditionalTags []string +} + +// ImageImportSpec defines how an image is imported. +type ImageImportSpec struct { + From kapi.ObjectReference + To *kapi.LocalObjectReference + + ImportPolicy TagImportPolicy + IncludeManifest bool +} + +// ImageImportStatus describes the result of an image import. +type ImageImportStatus struct { + Tag string + Status unversioned.Status + Image *Image +} diff --git a/pkg/image/api/v1/conversion.go b/pkg/image/api/v1/conversion.go index ec3fe068cb44..ed0b38c3369e 100644 --- a/pkg/image/api/v1/conversion.go +++ b/pkg/image/api/v1/conversion.go @@ -4,6 +4,7 @@ import ( "sort" kapi "k8s.io/kubernetes/pkg/api" + v1 "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/conversion" oapi "github.com/openshift/origin/pkg/api" @@ -30,6 +31,17 @@ func convert_api_Image_To_v1_Image(in *newer.Image, out *Image, s conversion.Sco out.DockerImageMetadata.RawJSON = data out.DockerImageMetadataVersion = version + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } + return nil } @@ -60,6 +72,17 @@ func convert_v1_Image_To_api_Image(in *Image, out *newer.Image, s conversion.Sco } out.DockerImageMetadataVersion = version + if in.DockerImageLayers != nil { + out.DockerImageLayers = make([]newer.ImageLayer, len(in.DockerImageLayers)) + for i := range in.DockerImageLayers { + if err := s.Convert(&in.DockerImageLayers[i], &out.DockerImageLayers[i], 0); err != nil { + return err + } + } + } else { + out.DockerImageLayers = nil + } + return nil } @@ -114,10 +137,27 @@ func convert_v1_ImageStreamMapping_To_api_ImageStreamMapping(in *ImageStreamMapp } func init() { - err := kapi.Scheme.AddConversionFuncs( + err := kapi.Scheme.AddDefaultingFuncs( + func(obj *ImageImportSpec) { + if obj.To == nil { + if ref, err := newer.ParseDockerImageReference(obj.From.Name); err == nil { + if len(ref.Tag) > 0 { + obj.To = &v1.LocalObjectReference{Name: ref.Tag} + } + } + } + }) + if err != nil { + // If one of the default functions is malformed, detect it immediately. + panic(err) + } + err = kapi.Scheme.AddConversionFuncs( func(in *[]NamedTagEventList, out *map[string]newer.TagEventList, s conversion.Scope) error { for _, curr := range *in { newTagEventList := newer.TagEventList{} + if err := s.Convert(&curr.Conditions, &newTagEventList.Conditions, 0); err != nil { + return err + } if err := s.Convert(&curr.Items, &newTagEventList.Items, 0); err != nil { return err } @@ -136,6 +176,9 @@ func init() { for _, key := range allKeys { newTagEventList := (*in)[key] oldTagEventList := &NamedTagEventList{Tag: key} + if err := s.Convert(&newTagEventList.Conditions, &oldTagEventList.Conditions, 0); err != nil { + return err + } if err := s.Convert(&newTagEventList.Items, &oldTagEventList.Items, 0); err != nil { return err } @@ -150,6 +193,13 @@ func init() { r := newer.TagReference{ Annotations: curr.Annotations, Reference: curr.Reference, + ImportPolicy: newer.TagImportPolicy{ + Insecure: curr.ImportPolicy.Insecure, + }, + } + if curr.Generation != nil { + gen := *curr.Generation + r.Generation = &gen } if err := s.Convert(&curr.From, &r.From, 0); err != nil { return err @@ -171,6 +221,13 @@ func init() { Name: tag, Annotations: newTagReference.Annotations, Reference: newTagReference.Reference, + ImportPolicy: TagImportPolicy{ + Insecure: newTagReference.ImportPolicy.Insecure, + }, + } + if newTagReference.Generation != nil { + gen := *newTagReference.Generation + oldTagReference.Generation = &gen } if err := s.Convert(&newTagReference.From, &oldTagReference.From, 0); err != nil { return err diff --git a/pkg/image/api/v1/conversion_test.go b/pkg/image/api/v1/conversion_test.go index 696a20d078f7..1ffd74fe2865 100644 --- a/pkg/image/api/v1/conversion_test.go +++ b/pkg/image/api/v1/conversion_test.go @@ -23,6 +23,7 @@ func TestRoundTripVersionedObject(t *testing.T) { i := &newer.Image{ ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + DockerImageLayers: []newer.ImageLayer{{Name: "foo", Size: 10}}, DockerImageMetadata: *d, DockerImageReference: "foo/bar/baz", } diff --git a/pkg/image/api/v1/register.go b/pkg/image/api/v1/register.go index 8f121c24e77c..d9038834af20 100644 --- a/pkg/image/api/v1/register.go +++ b/pkg/image/api/v1/register.go @@ -17,6 +17,7 @@ func init() { &ImageStreamTag{}, &ImageStreamTagList{}, &ImageStreamImage{}, + &ImageStreamImport{}, ) } @@ -28,3 +29,4 @@ func (*ImageStreamMapping) IsAnAPIObject() {} func (*ImageStreamTag) IsAnAPIObject() {} func (*ImageStreamTagList) IsAnAPIObject() {} func (*ImageStreamImage) IsAnAPIObject() {} +func (*ImageStreamImport) IsAnAPIObject() {} diff --git a/pkg/image/api/v1/types.go b/pkg/image/api/v1/types.go index 62512fc38d2a..7848f39969bb 100644 --- a/pkg/image/api/v1/types.go +++ b/pkg/image/api/v1/types.go @@ -28,6 +28,16 @@ type Image struct { DockerImageMetadataVersion string `json:"dockerImageMetadataVersion,omitempty" description:"conveys version of the object, if empty defaults to '1.0'"` // DockerImageManifest is the raw JSON of the manifest DockerImageManifest string `json:"dockerImageManifest,omitempty" description:"raw JSON of the manifest"` + // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. + DockerImageLayers []ImageLayer `json:"dockerImageLayers" description:"a list of the image layers from lowest to highest"` +} + +// ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. +type ImageLayer struct { + // Name of the layer as defined by the underlying store. + Name string `json:"name" description:"the name of the layer (blob, in Docker parlance)"` + // Size of the layer as defined by the underlying store. + Size int64 `json:"size" description:"size of the layer in bytes"` } // ImageStreamList is a list of ImageStream objects. @@ -70,6 +80,16 @@ type NamedTagReference struct { From *kapi.ObjectReference `json:"from,omitempty" description:"a reference to an image stream tag or image stream this tag should track"` // Reference states if the tag will be imported. Default value is false, which means the tag will be imported. Reference bool `json:"reference,omitempty" description:"if true consider this tag a reference only and do not attempt to import metadata about the image"` + // Generation is the image stream generation that updated this tag - setting it to 0 is an indication that the generation must be updated. + // Legacy clients will send this as nil, which means the client doesn't know or care. + Generation *int64 `json:"generation" description:"the generation of the image stream this was updated to"` + // Import is information that controls how images may be imported by the server. + ImportPolicy TagImportPolicy `json:"importPolicy,omitempty" description:"attributes controlling how this reference is imported"` +} + +type TagImportPolicy struct { + // Insecure is true if the server may bypass certificate verification or connect directly over HTTP during image import. + Insecure bool `json:"insecure,omitempty" description:"if true, the server may bypass certificate verification or connect directly over HTTP during image import"` } // ImageStreamStatus contains information about the state of this image stream. @@ -86,6 +106,8 @@ type ImageStreamStatus struct { type NamedTagEventList struct { Tag string `json:"tag" description:"the tag"` Items []TagEvent `json:"items" description:"list of tag events related to the tag"` + // Conditions is an array of conditions that apply to the tag event list. + Conditions []TagEventCondition `json:"conditions,omitempty" description:"the set of conditions that apply to this tag"` } // TagEvent is used by ImageStreamStatus to keep a historical record of images associated with a tag. @@ -96,6 +118,32 @@ type TagEvent struct { DockerImageReference string `json:"dockerImageReference" description:"the string that can be used to pull this image"` // Image is the image Image string `json:"image" description:"the image"` + // Generation is the spec tag generation that resulted in this tag being updated + Generation int64 `json:"generation" description:"the generation of the image stream spec tag this tag event represents"` +} + +type TagEventConditionType string + +// These are valid conditions of TagEvents. +const ( + // ImportSuccess with status False means the import of the specific tag failed + ImportSuccess TagEventConditionType = "ImportSuccess" +) + +// TagEventCondition contains condition information for a tag event. +type TagEventCondition struct { + // Type of tag event condition, currently only ImportSuccess + Type TagEventConditionType `json:"type" description:"type of tag event condition, currently only ImportSuccess"` + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` + // LastTransitionTIme is the time the condition transitioned from one status to another. + LastTransitionTime unversioned.Time `json:"lastTransitionTime,omitempty" description:"last time the condition transitioned from one status to another"` + // Reason is a brief machine readable explanation for the condition's last transition. + Reason string `json:"reason,omitempty" description:"machine-readable reason for the last condition transition"` + // Message is a human readable description of the details about last transition, complementing reason. + Message string `json:"message,omitempty" description:"human-readable message indicating details of the last transition"` + // Generation is the spec tag generation that this status corresponds to + Generation int64 `json:"generation" description:"the generation of the image stream spec tag this condition represents"` } // ImageStreamMapping represents a mapping from a single tag to a Docker image as @@ -144,3 +192,69 @@ type DockerImageReference struct { Tag string ID string } + +// ImageStreamImport imports an image from remote repositories into OpenShift. +type ImageStreamImport struct { + unversioned.TypeMeta `json:",inline"` + kapi.ObjectMeta `json:"metadata,omitempty" description:"metadata about the image stream, name is required"` + + Spec ImageStreamImportSpec `json:"spec" description:"description of the images that the user wishes to import"` + Status ImageStreamImportStatus `json:"status" description:"the result of importing the image"` +} + +// ImageStreamImportSpec defines what images should be imported. +type ImageStreamImportSpec struct { + // Import indicates whether to perform an import - if so, the specified tags are set on the spec + // and status of the image stream defined by the type meta. + Import bool `json:"import" description:"if true, the images will be imported to the server and the resulting image stream will be returned in status.import"` + // Repository is an optional import of an entire Docker image repository. A maximum limit on the + // number of tags imported this way is imposed by the server. + Repository *RepositoryImportSpec `json:"repository,omitempty" description:"if specified, import a single Docker repository's tags to this image stream"` + // Images are a list of individual images to import. + Images []ImageImportSpec `json:"images,omitempty" description:"a list of images to import into this image stream"` +} + +// ImageStreamImportStatus contains information about the status of an image stream import. +type ImageStreamImportStatus struct { + // Import is the image stream that was successfully updated or created when 'to' was set. + Import *ImageStream `json:"import,omitempty" description:"if the user requested any images be imported, this field will be set with the successful image stream create or update"` + // Repository is set if spec.repository was set to the outcome of the import + Repository *RepositoryImportStatus `json:"repository,omitempty" description:"status of the attempt to import a repository"` + // Images is set with the result of importing spec.images + Images []ImageImportStatus `json:"images,omitempty" description:"status of the attempt to import images"` +} + +// RepositoryImportSpec describes a request to import images from a Docker image repository. +type RepositoryImportSpec struct { + From kapi.ObjectReference `json:"from" description:"the source for the image repository to import; only kind DockerImage and a name of a Docker image repository is allowed"` + + ImportPolicy TagImportPolicy `json:"importPolicy,omitempty" description:"policy controlling how the image is imported"` + IncludeManifest bool `json:"includeManifest,omitempty" description:"if true, return the manifest for each image in the response"` +} + +// RepositoryImportStatus describes the result of an image repository import +type RepositoryImportStatus struct { + // Status reflects whether any failure occurred during import + Status unversioned.Status `json:"status,omitempty" description:"the result of the import attempt, will include a reason and message if the repository could not be imported"` + // Images is a list of images successfully retrieved by the import of the repository. + Images []ImageImportStatus `json:"images,omitempty" description:"a list of the images retrieved by the import of the repository"` + // AdditionalTags are tags that exist in the repository but were not imported because + // a maximum limit of automatic imports was applied. + AdditionalTags []string `json:"additionalTags,omitempty" description:"a list of additional tags on the repository that were not retrieved"` +} + +// ImageImportSpec describes a request to import a specific image. +type ImageImportSpec struct { + From kapi.ObjectReference `json:"from" description:"the source of an image to import; only kind DockerImage is allowed"` + To *kapi.LocalObjectReference `json:"to,omitempty" description:"a tag in the current image stream to assign the imported image to, if name is not specified the default tag from from.name will be used"` + + ImportPolicy TagImportPolicy `json:"importPolicy,omitempty" description:"policy controlling how the image is imported"` + IncludeManifest bool `json:"includeManifest,omitempty" description:"if true, return the manifest for this image in the response"` +} + +// ImageImportStatus describes the result of an image import. +type ImageImportStatus struct { + Status unversioned.Status `json:"status" description:"the status of the image import, including errors encountered while retrieving the image"` + Image *Image `json:"image,omitempty" description:"if the image was located, the metadata of that image"` + Tag string `json:"tag,omitempty" description:"the tag this image was located under, if any"` +} diff --git a/pkg/image/api/v1beta3/conversion.go b/pkg/image/api/v1beta3/conversion.go index 1541c11e9141..587785e9d24b 100644 --- a/pkg/image/api/v1beta3/conversion.go +++ b/pkg/image/api/v1beta3/conversion.go @@ -203,6 +203,9 @@ func init() { func(in *[]NamedTagEventList, out *map[string]newer.TagEventList, s conversion.Scope) error { for _, curr := range *in { newTagEventList := newer.TagEventList{} + if err := s.Convert(&curr.Conditions, &newTagEventList.Conditions, 0); err != nil { + return err + } if err := s.Convert(&curr.Items, &newTagEventList.Items, 0); err != nil { return err } @@ -221,6 +224,9 @@ func init() { for _, key := range allKeys { newTagEventList := (*in)[key] oldTagEventList := &NamedTagEventList{Tag: key} + if err := s.Convert(&newTagEventList.Conditions, &oldTagEventList.Conditions, 0); err != nil { + return err + } if err := s.Convert(&newTagEventList.Items, &oldTagEventList.Items, 0); err != nil { return err } @@ -235,6 +241,13 @@ func init() { r := newer.TagReference{ Annotations: curr.Annotations, Reference: curr.Reference, + ImportPolicy: newer.TagImportPolicy{ + Insecure: curr.ImportPolicy.Insecure, + }, + } + if curr.Generation != nil { + gen := *curr.Generation + r.Generation = &gen } if err := s.Convert(&curr.From, &r.From, 0); err != nil { return err @@ -256,6 +269,13 @@ func init() { Name: tag, Annotations: newTagReference.Annotations, Reference: newTagReference.Reference, + ImportPolicy: TagImportPolicy{ + Insecure: newTagReference.ImportPolicy.Insecure, + }, + } + if newTagReference.Generation != nil { + gen := *newTagReference.Generation + oldTagReference.Generation = &gen } if err := s.Convert(&newTagReference.From, &oldTagReference.From, 0); err != nil { return err diff --git a/pkg/image/api/v1beta3/types.go b/pkg/image/api/v1beta3/types.go index 98d24f305180..e8724af95f15 100644 --- a/pkg/image/api/v1beta3/types.go +++ b/pkg/image/api/v1beta3/types.go @@ -27,6 +27,16 @@ type Image struct { DockerImageMetadataVersion string `json:"dockerImageMetadataVersion,omitempty"` // The raw JSON of the manifest DockerImageManifest string `json:"dockerImageManifest,omitempty"` + // DockerImageLayers represents the layers in the image. May not be set if the image does not define that data. + DockerImageLayers []ImageLayer `json:"dockerImageLayers" description:"a list of the image layers from lowest to highest"` +} + +// ImageLayer represents a single layer of the image. Some images may have multiple layers. Some may have none. +type ImageLayer struct { + // Name of the layer as defined by the underlying store. + Name string `json:"name" description:"the name of the layer (blob, in Docker parlance)"` + // Size of the layer as defined by the underlying store. + Size int64 `json:"size" description:"size of the layer in bytes"` } // ImageStreamList is a list of ImageStream objects. @@ -65,6 +75,16 @@ type NamedTagReference struct { From *kapi.ObjectReference `json:"from,omitempty"` // Reference states if the tag will be imported. Default value is false, which means the tag will be imported. Reference bool `json:"reference,omitempty" description:"if true consider this tag a reference only and do not attempt to import metadata about the image"` + // Generation is the image stream generation that updated this tag - setting it to 0 is an indication that the generation must be updated. + // Legacy clients will send this as nil, which means the client doesn't know or care. + Generation *int64 `json:"generation" description:"the generation of the image stream this was updated to"` + // Import is information that controls how images may be imported by the server. + ImportPolicy TagImportPolicy `json:"importPolicy,omitempty" description:"attributes controlling how this reference is imported"` +} + +type TagImportPolicy struct { + // Insecure is true if the server may bypass certificate verification or connect directly over HTTP during image import. + Insecure bool `json:"insecure,omitempty" description:"if true, the server may bypass certificate verification or connect directly over HTTP during image import"` } // ImageStreamStatus contains information about the state of this image stream. @@ -81,16 +101,44 @@ type ImageStreamStatus struct { type NamedTagEventList struct { Tag string `json:"tag"` Items []TagEvent `json:"items"` + // Conditions is an array of conditions that apply to the tag event list. + Conditions []TagEventCondition `json:"conditions" description:"the set of conditions that apply to this tag"` } -// TagEvent is used by ImageRepositoryStatus to keep a historical record of images associated with a tag. +// TagEvent is used by ImageStreamStatus to keep a historical record of images associated with a tag. type TagEvent struct { - // When the TagEvent was created - Created unversioned.Time `json:"created"` - // The string that can be used to pull this image - DockerImageReference string `json:"dockerImageReference"` - // The image - Image string `json:"image"` + // Created holds the time the TagEvent was created + Created unversioned.Time `json:"created" description:"when the event was created"` + // DockerImageReference is the string that can be used to pull this image + DockerImageReference string `json:"dockerImageReference" description:"the string that can be used to pull this image"` + // Image is the image + Image string `json:"image" description:"the image"` + // Generation is the spec tag generation that resulted in this tag being updated + Generation int64 `json:"generation" description:"the generation of the image stream spec tag this tag event represents"` +} + +type TagEventConditionType string + +// These are valid conditions of TagEvents. +const ( + // ImportSuccess with status False means the import of the specific tag failed + ImportSuccess TagEventConditionType = "ImportSuccess" +) + +// TagEventCondition contains condition information for a tag event. +type TagEventCondition struct { + // Type of tag event condition, currently only ImportSuccess + Type TagEventConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status kapi.ConditionStatus `json:"status"` + // Last time the condition transit from one status to another. + LastTransitionTime unversioned.Time `json:"lastTransitionTime,omitempty"` + // (brief) reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + Message string `json:"message,omitempty"` + // Generation is the spec tag generation that this status corresponds to + Generation int64 `json:"generation" description:"the generation of the image stream spec tag this tag event represents"` } // ImageStreamMapping represents a mapping from a single tag to a Docker image as diff --git a/pkg/image/api/validation/validation.go b/pkg/image/api/validation/validation.go index 0a4c09ca6710..9e43e9499209 100644 --- a/pkg/image/api/validation/validation.go +++ b/pkg/image/api/validation/validation.go @@ -125,8 +125,6 @@ func ValidateImageStreamUpdate(newStream, oldStream *api.ImageStream) fielderror func ValidateImageStreamStatusUpdate(newStream, oldStream *api.ImageStream) fielderrors.ValidationErrorList { result := fielderrors.ValidationErrorList{} result = append(result, validation.ValidateObjectMetaUpdate(&newStream.ObjectMeta, &oldStream.ObjectMeta).Prefix("metadata")...) - newStream.Spec.Tags = oldStream.Spec.Tags - newStream.Spec.DockerImageRepository = oldStream.Spec.DockerImageRepository return result } @@ -185,3 +183,48 @@ func ValidateImageStreamTagUpdate(newIST, oldIST *api.ImageStreamTag) fielderror return result } + +func ValidateImageStreamImport(isi *api.ImageStreamImport) fielderrors.ValidationErrorList { + errs := fielderrors.ValidationErrorList{} + for i, spec := range isi.Spec.Images { + from := spec.From + switch from.Kind { + case "DockerImage": + if spec.To != nil && len(spec.To.Name) == 0 { + errs = append(errs, fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("to.name", spec.To.Name, "the name of the target tag must be specified")}.PrefixIndex(i)...) + } + if len(spec.From.Name) == 0 { + errs = append(errs, fielderrors.ValidationErrorList{fielderrors.NewFieldRequired("from.name")}.PrefixIndex(i)...) + } else { + if _, err := api.ParseDockerImageReference(spec.From.Name); err != nil { + errs = append(errs, fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("from.name", spec.From.Name, err.Error())}.PrefixIndex(i)...) + } + } + default: + errs = append(errs, fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("from.kind", from.Kind, "only DockerImage is supported")}.PrefixIndex(i)...) + } + } + errs = errs.Prefix("spec.images") + + if spec := isi.Spec.Repository; spec != nil { + from := spec.From + switch from.Kind { + case "DockerImage": + if len(spec.From.Name) == 0 { + errs = append(errs, fielderrors.NewFieldRequired("spec.repository.from.name")) + } else { + if _, err := api.ParseDockerImageReference(spec.From.Name); err != nil { + errs = append(errs, fielderrors.NewFieldInvalid("spec.repository.from.name", spec.From.Name, err.Error())) + } + } + default: + errs = append(errs, fielderrors.NewFieldInvalid("spec.repository.from.kind", from.Kind, "only DockerImage is supported")) + } + } + if len(isi.Spec.Images) == 0 && isi.Spec.Repository == nil { + errs = append(errs, fielderrors.NewFieldInvalid("spec.images", nil, "you must specify at least one image or a repository import")) + } + + errs = append(errs, validation.ValidateObjectMeta(&isi.ObjectMeta, true, ValidateImageStreamName).Prefix("metadata")...) + return errs +} diff --git a/pkg/image/controller/controller.go b/pkg/image/controller/controller.go index 6240e6116eed..0c69523eb162 100644 --- a/pkg/image/controller/controller.go +++ b/pkg/image/controller/controller.go @@ -1,32 +1,64 @@ package controller import ( - "fmt" - "time" - "github.com/golang/glog" kapi "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/api/errors" - "k8s.io/kubernetes/pkg/api/unversioned" - kerrors "k8s.io/kubernetes/pkg/util/errors" - "k8s.io/kubernetes/pkg/util/sets" "github.com/openshift/origin/pkg/client" - "github.com/openshift/origin/pkg/dockerregistry" "github.com/openshift/origin/pkg/image/api" ) type ImportController struct { - streams client.ImageStreamsNamespacer - mappings client.ImageStreamMappingsNamespacer - // injected for testing - client dockerregistry.Client + streams client.ImageStreamsNamespacer +} + +// tagImportable is true if the given TagReference is importable by this controller +func tagImportable(tagRef api.TagReference) bool { + if tagRef.From == nil { + return false + } + if tagRef.From.Kind != "DockerImage" || tagRef.Reference { + return false + } + return true } -// needsImport returns true if the provided image stream should have its tags imported. -func needsImport(stream *api.ImageStream) bool { - return stream.Annotations == nil || len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 +// tagNeedsImport is true if the observed tag generation for this tag is older than the +// specified tag generation (if no tag generation is specified, the controller does not +// need to import this tag). +func tagNeedsImport(stream *api.ImageStream, tag string, tagRef api.TagReference, importWhenGenerationNil bool) bool { + if !tagImportable(tagRef) { + return false + } + if tagRef.Generation == nil { + return importWhenGenerationNil + } + return *tagRef.Generation > api.LatestObservedTagGeneration(stream, tag) +} + +// needsImport returns true if the provided image stream should have tags imported. Partial is returned +// as true if the spec.dockerImageRepository does not need to be refreshed (if only tags have to be imported). +func needsImport(stream *api.ImageStream) (ok bool, partial bool) { + if stream.Annotations == nil || len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { + if len(stream.Spec.DockerImageRepository) > 0 { + return true, false + } + // for backwards compatibility, if any of our tags are importable, trigger a partial import when the + // annotation is cleared. + for _, tagRef := range stream.Spec.Tags { + if tagImportable(tagRef) { + return true, true + } + } + } + // find any tags with a newer generation than their status + for tag, tagRef := range stream.Spec.Tags { + if tagNeedsImport(stream, tag, tagRef, false) { + return true, true + } + } + return false, false } // retryCount is the number of times to retry on a conflict when updating an image stream @@ -35,246 +67,60 @@ const retryCount = 2 // Next processes the given image stream, looking for streams that have DockerImageRepository // set but have not yet been marked as "ready". If transient errors occur, err is returned but // the image stream is not modified (so it will be tried again later). If a permanent -// failure occurs the image is marked with an annotation. The tags of the original spec image -// are left as is (those are updated through status). -// There are 3 use cases here: +// failure occurs the image is marked with an annotation and conditions are set on the status +// tags. The tags of the original spec image are left as is (those are updated through status). +// +// There are 3 scenarios: +// // 1. spec.DockerImageRepository defined without any tags results in all tags being imported // from upstream image repository +// // 2. spec.DockerImageRepository + tags defined - import all tags from upstream image repository, // and all the specified which (if name matches) will overwrite the default ones. // Additionally: -// for kind == DockerImage import or reference underlying image, iow. exact tag (not provided means latest), +// for kind == DockerImage import or reference underlying image, exact tag (not provided means latest), // for kind != DockerImage reference tag from the same or other ImageStream -// 3. spec.DockerImageRepository not defined - import tags per its definition. -// Current behavior of the controller is to process import as far as possible, but -// we still want to keep backwards compatibility and retries, for that we'll return -// error in the following cases: -// 1. connection failure to upstream image repository -// 2. reading tags when error is different from RepositoryNotFound or RegistryNotFound -// 3. image retrieving when error is different from RepositoryNotFound, RegistryNotFound or ImageNotFound -// 4. ImageStreamMapping save error -// 5. error when marking ImageStream as imported +// +// 3. spec.DockerImageRepository not defined - import tags per each definition. +// func (c *ImportController) Next(stream *api.ImageStream) error { - if !needsImport(stream) { + ok, partial := needsImport(stream) + if !ok { return nil } - glog.V(4).Infof("Importing stream %s/%s...", stream.Namespace, stream.Name) + glog.V(3).Infof("Importing stream %s/%s partial=%t...", stream.Namespace, stream.Name, partial) - insecure := stream.Annotations[api.InsecureRepositoryAnnotation] == "true" - client := c.client - if client == nil { - client = dockerregistry.NewClient(5 * time.Second) - } - - var errlist []error - toImport, retry, err := getTags(stream, client, insecure) - // return here, only if there is an error and nothing to import - if err != nil && len(toImport) == 0 { - if retry { - return err - } - return c.done(stream, err.Error(), retryCount) - } - if err != nil { - errlist = append(errlist, err) - } - - retry, err = c.importTags(stream, toImport, client, insecure) - if err != nil { - if retry { - return err - } - errlist = append(errlist, err) - } - - if len(errlist) > 0 { - return c.done(stream, kerrors.NewAggregate(errlist).Error(), retryCount) + isi := &api.ImageStreamImport{ + ObjectMeta: kapi.ObjectMeta{ + Name: stream.Name, + Namespace: stream.Namespace, + ResourceVersion: stream.ResourceVersion, + UID: stream.UID, + }, + Spec: api.ImageStreamImportSpec{Import: true}, } - - return c.done(stream, "", retryCount) -} - -// getTags returns a map of tags to be imported, a flag saying if we should retry -// imports, meaning not setting the import annotation and an error if one occurs. -// Tags explicitly defined will overwrite those from default upstream image repository. -func getTags(stream *api.ImageStream, client dockerregistry.Client, insecure bool) (map[string]api.DockerImageReference, bool, error) { - imports := make(map[string]api.DockerImageReference) - references := sets.NewString() - - // read explicitly defined tags - for tagName, specTag := range stream.Spec.Tags { - if specTag.From == nil { + for tag, tagRef := range stream.Spec.Tags { + if !(partial && tagImportable(tagRef)) && !tagNeedsImport(stream, tag, tagRef, true) { continue } - if specTag.From.Kind != "DockerImage" || specTag.Reference { - references.Insert(tagName) - continue - } - ref, err := api.ParseDockerImageReference(specTag.From.Name) - if err != nil { - glog.V(2).Infof("error parsing DockerImage %s: %v", specTag.From.Name, err) - continue + isi.Spec.Images = append(isi.Spec.Images, api.ImageImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: tagRef.From.Name}, + To: &kapi.LocalObjectReference{Name: tag}, + ImportPolicy: tagRef.ImportPolicy, + }) + } + if repo := stream.Spec.DockerImageRepository; !partial && len(repo) > 0 { + insecure := stream.Annotations[api.InsecureRepositoryAnnotation] == "true" + isi.Spec.Repository = &api.RepositoryImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: repo}, + ImportPolicy: api.TagImportPolicy{Insecure: insecure}, } - imports[tagName] = ref.DockerClientDefaults() } - - if len(stream.Spec.DockerImageRepository) == 0 { - return imports, false, nil - } - - // read tags from default upstream image repository - streamRef, err := api.ParseDockerImageReference(stream.Spec.DockerImageRepository) + result, err := c.streams.ImageStreams(stream.Namespace).Import(isi) if err != nil { - return imports, false, err - } - glog.V(5).Infof("Connecting to %s...", streamRef.Registry) - conn, err := client.Connect(streamRef.Registry, insecure) - if err != nil { - glog.V(5).Infof("Error connecting to %s: %v", streamRef.Registry, err) - // retry-able error no. 1 - return imports, true, err - } - glog.V(5).Infof("Fetching tags for %s/%s...", streamRef.Namespace, streamRef.Name) - tags, err := conn.ImageTags(streamRef.Namespace, streamRef.Name) - switch { - case dockerregistry.IsRepositoryNotFound(err), dockerregistry.IsRegistryNotFound(err): - glog.V(5).Infof("Error fetching tags for %s/%s: %v", streamRef.Namespace, streamRef.Name, err) - return imports, false, err - case err != nil: - // retry-able error no. 2 - glog.V(5).Infof("Error fetching tags for %s/%s: %v", streamRef.Namespace, streamRef.Name, err) - return imports, true, err - } - glog.V(5).Infof("Got tags for %s/%s: %#v", streamRef.Namespace, streamRef.Name, tags) - for tag, image := range tags { - if _, ok := imports[tag]; ok || references.Has(tag) { - continue - } - idTagPresent := false - // this for loop is for backwards compatibility with v1 repo, where - // there was no image id returned with tags, like v2 does right now. - for t2, i2 := range tags { - if i2 == image && t2 == image { - idTagPresent = true - break - } - } - ref := streamRef - if idTagPresent { - ref.Tag = image - } else { - ref.Tag = tag - } - ref.ID = image - imports[tag] = ref - } - - return imports, false, nil -} - -// importTags imports tags specified in a map from given ImageStream. Returns flag -// saying if we should retry imports, meaning not setting the import annotation -// and an error if one occurs. -func (c *ImportController) importTags(stream *api.ImageStream, imports map[string]api.DockerImageReference, client dockerregistry.Client, insecure bool) (bool, error) { - retrieved := make(map[string]*dockerregistry.Image) - var errlist []error - shouldRetry := false - for tag, ref := range imports { - image, retry, err := c.importTag(stream, tag, ref, retrieved[ref.ID], client, insecure) - if err != nil { - if retry { - shouldRetry = retry - } - errlist = append(errlist, err) - continue - } - // save image object for next tag imports, this is to avoid re-downloading the default image registry - if len(ref.ID) > 0 { - retrieved[ref.ID] = image - } - } - return shouldRetry, kerrors.NewAggregate(errlist) -} - -// importTag import single tag from given ImageStream. Returns retrieved image (for later reuse), -// a flag saying if we should retry imports and an error if one occurs. -func (c *ImportController) importTag(stream *api.ImageStream, tag string, ref api.DockerImageReference, dockerImage *dockerregistry.Image, client dockerregistry.Client, insecure bool) (*dockerregistry.Image, bool, error) { - glog.V(5).Infof("Importing tag %s from %s/%s...", tag, stream.Namespace, stream.Name) - if dockerImage == nil { - // TODO insecure applies to the stream's spec.dockerImageRepository, not necessarily to an external one! - conn, err := client.Connect(ref.Registry, insecure) - if err != nil { - // retry-able error no. 3 - return nil, true, err - } - if len(ref.ID) > 0 { - dockerImage, err = conn.ImageByID(ref.Namespace, ref.Name, ref.ID) - } else { - dockerImage, err = conn.ImageByTag(ref.Namespace, ref.Name, ref.Tag) - } - switch { - case dockerregistry.IsRepositoryNotFound(err), dockerregistry.IsRegistryNotFound(err), dockerregistry.IsImageNotFound(err), dockerregistry.IsTagNotFound(err): - return nil, false, err - case err != nil: - // retry-able error no. 4 - return nil, true, err - } - } - var image api.DockerImage - if err := kapi.Scheme.Convert(&dockerImage.Image, &image); err != nil { - return nil, false, fmt.Errorf("could not convert image: %#v", err) - } - - // prefer to pull by ID always - if dockerImage.PullByID { - // if the registry indicates the image is pullable by ID, clear the tag - ref.Tag = "" - ref.ID = dockerImage.ID - } - - mapping := &api.ImageStreamMapping{ - ObjectMeta: kapi.ObjectMeta{ - Name: stream.Name, - Namespace: stream.Namespace, - }, - Tag: tag, - Image: api.Image{ - ObjectMeta: kapi.ObjectMeta{ - Name: dockerImage.ID, - }, - DockerImageReference: ref.String(), - DockerImageMetadata: image, - }, - } - if err := c.mappings.ImageStreamMappings(stream.Namespace).Create(mapping); err != nil { - // retry-able no. 5 - return nil, true, err - } - return dockerImage, false, nil -} - -// done marks the stream as being processed due to an error or failure condition. -func (c *ImportController) done(stream *api.ImageStream, reason string, retry int) error { - if len(reason) == 0 { - reason = unversioned.Now().UTC().Format(time.RFC3339) - } else if len(reason) > 300 { - // cut down the reason up to 300 characters max. - reason = reason[:300] - } - if stream.Annotations == nil { - stream.Annotations = make(map[string]string) - } - stream.Annotations[api.DockerImageRepositoryCheckAnnotation] = reason - if _, err := c.streams.ImageStreams(stream.Namespace).Update(stream); err != nil && !errors.IsNotFound(err) { - if errors.IsConflict(err) && retry > 0 { - if newStream, err := c.streams.ImageStreams(stream.Namespace).Get(stream.Name); err == nil { - if stream.UID != newStream.UID { - return nil - } - return c.done(newStream, reason, retry-1) - } - } - return err + glog.V(4).Infof("Import stream %s/%s partial=%t error: %v", stream.Namespace, stream.Name, partial, err) + } else { + glog.V(5).Infof("Import stream %s/%s partial=%t import: %#v", stream.Namespace, stream.Name, partial, result.Status.Import) } - return nil + return err } diff --git a/pkg/image/controller/controller_test.go b/pkg/image/controller/controller_test.go index 9586b74c0af8..4e9ad4f623f8 100644 --- a/pkg/image/controller/controller_test.go +++ b/pkg/image/controller/controller_test.go @@ -2,16 +2,12 @@ package controller import ( "fmt" - "strings" "testing" "time" - "github.com/fsouza/go-dockerclient" - kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" - kclient "k8s.io/kubernetes/pkg/client/unversioned/testclient" - "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util" client "github.com/openshift/origin/pkg/client/testclient" "github.com/openshift/origin/pkg/dockerregistry" @@ -35,20 +31,25 @@ type fakeDockerRegistryClient struct { ConnErr error Images []expectedImage + + Called bool } func (f *fakeDockerRegistryClient) Connect(registry string, insecure bool) (dockerregistry.Connection, error) { + f.Called = true f.Registry = registry f.Insecure = insecure return f, f.ConnErr } func (f *fakeDockerRegistryClient) ImageTags(namespace, name string) (map[string]string, error) { + f.Called = true f.Namespace, f.Name = namespace, name return f.Tags, f.Err } func (f *fakeDockerRegistryClient) ImageByTag(namespace, name, tag string) (*dockerregistry.Image, error) { + f.Called = true if len(tag) == 0 { tag = api.DefaultImageTag } @@ -62,6 +63,7 @@ func (f *fakeDockerRegistryClient) ImageByTag(namespace, name, tag string) (*doc } func (f *fakeDockerRegistryClient) ImageByID(namespace, name, id string) (*dockerregistry.Image, error) { + f.Called = true f.Namespace, f.Name, f.ID = namespace, name, id for _, t := range f.Images { if t.ID == id { @@ -71,746 +73,254 @@ func (f *fakeDockerRegistryClient) ImageByID(namespace, name, id string) (*docke return nil, dockerregistry.NewImageNotFoundError(fmt.Sprintf("%s/%s", namespace, name), id, "") } -func TestControllerNoOp(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, - Name: "test", - Namespace: "other", - }, - } - other, err := kapi.Scheme.DeepCopy(stream) - if err != nil { - t.Fatalf("unexpected deepcopy error: %v", err) - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if !kapi.Semantic.DeepEqual(stream, other) { - t.Errorf("did not expect change to stream") - } -} - -func TestControllerNoDockerRepo(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 1 { - t.Fatalf("expected 1 action, got %#v", actions) - } - if !actions[0].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerExternalRepo(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{ - Images: []expectedImage{ - { - Tag: "mytag", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, - }, - }, - }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - Tags: map[string]api.TagReference{ - "1.1": { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:mytag", - }, - }, - }, - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 2 { - t.Fatalf("expected 2 actions, got %#v", actions) - } - if !actions[0].Matches("create", "imagestreammappings") { - t.Errorf("expected a create action: %#v", actions) - } - if !actions[1].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerExternalReferenceRepo(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{ - Images: []expectedImage{ - { - Tag: "mytag", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, - }, - }, - }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - Tags: map[string]api.TagReference{ - "1.1": { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:mytag", - }, - Reference: true, - }, - }, - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 1 { - t.Fatalf("expected 1 action, got %#v", actions) - } - if !actions[0].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerExternalRepoFails(t *testing.T) { - expectedError := fmt.Errorf("test error") - cli, fake := &fakeDockerRegistryClient{ - Images: []expectedImage{ - { - Tag: "mytag", - Err: expectedError, - }, - }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - Tags: map[string]api.TagReference{ - "1.1": { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:mytag", - }, - }, - }, - }, - } - if err := c.Next(&stream); !strings.Contains(err.Error(), expectedError.Error()) { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) != 0 { - t.Errorf("should not set annotation: %#v", stream) - } - if len(fake.Actions()) != 0 { - t.Error("expected no actions on fake client") - } -} - -func TestControllerRepoHandled(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 1 { - t.Fatalf("expected 1 action, got %#v", actions) - } - if !actions[0].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerTagRetrievalFails(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{Err: fmt.Errorf("test error")}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); err != cli.Err { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) != 0 { - t.Errorf("should not set annotation: %#v", stream) - } - if len(fake.Actions()) != 0 { - t.Error("expected no actions on fake client") - } -} - -func TestControllerRetrievesInsecure(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{Err: fmt.Errorf("test error")}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - Annotations: map[string]string{ - api.InsecureRepositoryAnnotation: "true", - }, - }, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); err != cli.Err { - t.Errorf("unexpected error: %v", err) - } - if !cli.Insecure { - t.Errorf("expected insecure call: %#v", cli) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) != 0 { - t.Errorf("should not set annotation: %#v", stream) - } - if len(fake.Actions()) != 0 { - t.Error("expected no actions on fake client") - } -} - -func TestControllerImageNotFoundError(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{Tags: map[string]string{api.DefaultImageTag: "not_found"}}, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 1 { - t.Fatalf("expected 1 action, got %#v", actions) - } - if !actions[0].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerImageWithGenericError(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{ - Tags: map[string]string{api.DefaultImageTag: "found"}, - Images: []expectedImage{ - { - ID: "found", - Err: fmt.Errorf("test error"), - }, - }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); !strings.Contains(err.Error(), cli.Images[0].Err.Error()) { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) != 0 { - t.Errorf("should not set annotation: %#v", stream) - } - if len(fake.Actions()) != 0 { - t.Error("expected no actions on fake client") - } -} - -func TestControllerWithImage(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{ - Tags: map[string]string{api.DefaultImageTag: "found"}, - Images: []expectedImage{ - { - ID: "found", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, +func TestControllerStart(t *testing.T) { + two := int64(2) + testCases := []struct { + stream *api.ImageStream + run bool + }{ + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, + Name: "test", + Namespace: "other", }, }, }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if !isRFC3339(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) { - t.Fatalf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 2 { - t.Fatalf("expected 2 actions, got %#v", actions) - } - if !actions[0].Matches("create", "imagestreammappings") { - t.Errorf("expected a create action: %#v", actions) - } - if !actions[1].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions) - } -} - -func TestControllerWithExternalAndDefaultRegistry(t *testing.T) { - cli, fake := &fakeDockerRegistryClient{ - Tags: map[string]string{"one": "1", "two": "2"}, - Images: []expectedImage{ - { - ID: "1", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, - }, - }, - { - ID: "2", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, - }, - }, - { - Tag: "mytag", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, + Name: "test", + Namespace: "other", }, - }, - }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - Tags: map[string]api.TagReference{ - "ext": { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:mytag", - }, + Spec: api.ImageStreamSpec{ + DockerImageRepository: "test/other", }, }, }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) == 0 { - t.Errorf("did not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 4 { - t.Fatalf("expected 4 actions, got %#v", actions) - } - for i := 0; i < 3; i++ { - if !actions[i].Matches("create", "imagestreammappings") { - t.Errorf("expected a create action: %d %#v", i, actions[i]) - } - } - if !actions[3].Matches("update", "imagestreams") { - t.Errorf("expected an update action: %#v", actions[0]) - } -} - -func TestControllerWithExternalAndDefaultRegistryErrorOnOneTag(t *testing.T) { - expectedError := fmt.Errorf("test error") - cli, fake := &fakeDockerRegistryClient{ - Tags: map[string]string{"one": "1", "two": "2"}, - Images: []expectedImage{ - { - ID: "1", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: "a random error"}, + Name: "test", + Namespace: "other", }, - }, - { - ID: "2", - Err: expectedError, - }, - { - Tag: "mytag", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, + Spec: api.ImageStreamSpec{ + DockerImageRepository: "test/other", }, }, }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{ - Name: "test", - Namespace: "other", - }, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - Tags: map[string]api.TagReference{ - "ext": { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:mytag", + // references are ignored + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, + Spec: api.ImageStreamSpec{ + Tags: map[string]api.TagReference{ + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Reference: true, + }, }, }, }, }, - } - if err := c.Next(&stream); !strings.Contains(err.Error(), cli.Images[1].Err.Error()) { - t.Errorf("unexpected error: %v", err) - } - if len(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) != 0 { - t.Errorf("should not set annotation: %#v", stream) - } - actions := fake.Actions() - if len(actions) != 2 { - t.Fatalf("expected 2 actions, got %#v", actions) - } - for i := 0; i < 1; i++ { - if !actions[i].Matches("create", "imagestreammappings") { - t.Errorf("expected a create action: %d %#v", i, actions[i]) - } - } -} - -func TestControllerWithSpecTags(t *testing.T) { - tests := map[string]struct { - dockerImageReference string - from *kapi.ObjectReference - expectUpdate bool - }{ - "no tracking": { - expectUpdate: true, - }, - "docker image": { - from: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "some/repo:tagX", - }, - expectUpdate: true, - }, - "from image stream tag": { - from: &kapi.ObjectReference{ - Kind: "ImageStreamTag", - Name: "2.0", - }, - expectUpdate: false, - }, - "from image stream image": { - from: &kapi.ObjectReference{ - Kind: "ImageStreamImage", - Name: "foo@sha256:1234", - }, - expectUpdate: false, - }, - } - - for name, test := range tests { - cli, fake := &fakeDockerRegistryClient{ - Tags: map[string]string{api.DefaultImageTag: "found"}, - Images: []expectedImage{ - { - ID: "found", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, - }, - }, - { - Tag: "tagX", - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, + Spec: api.ImageStreamSpec{ + Tags: map[string]api.TagReference{ + "latest": { + From: &kapi.ObjectReference{Kind: "AnotherImage", Name: "test/other:latest"}, + Reference: true, }, }, }, }, - }, &client.Fake{} - c := ImportController{client: cli, streams: fake, mappings: fake} - stream := api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, - Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", - Tags: map[string]api.TagReference{ - api.DefaultImageTag: { - From: test.from, - }, - }, - }, - } - if err := c.Next(&stream); err != nil { - t.Errorf("%s: unexpected error: %v", name, err) - } - if !isRFC3339(stream.Annotations[api.DockerImageRepositoryCheckAnnotation]) { - t.Errorf("%s: did not set annotation: %#v", name, stream) - } - actions := fake.Actions() - if test.expectUpdate { - if len(actions) != 2 { - t.Errorf("%s: expected an update action: %#v", name, actions) - } - if !actions[0].Matches("create", "imagestreammappings") { - t.Errorf("%s: expected %s, got %v", name, "create-imagestreammappings", actions[0]) - } - if !actions[1].Matches("update", "imagestreams") { - t.Errorf("%s: expected %s, got %v", name, "update-imagestreams", actions[1]) - } - } else { - if len(actions) != 1 { - t.Errorf("%s: expected no update action: %#v", name, actions) - } - if !actions[0].Matches("update", "imagestreams") { - t.Errorf("%s: expected %s, got %v", name, "update-imagestreams", actions[0]) - } - } - } -} + }, -func TestControllerReturnsErrForRetries(t *testing.T) { - expErr := fmt.Errorf("expected error") - osClient := &client.Fake{} - errISMClient := &client.Fake{} - errISMClient.PrependReactor("create", "imagestreammappings", func(action kclient.Action) (handled bool, ret runtime.Object, err error) { - return true, ret, expErr - }) - tests := map[string]struct { - singleError bool - expActions int - fakeClient *client.Fake - fakeDocker *fakeDockerRegistryClient - stream *api.ImageStream - }{ - "retry-able error no. 1": { - singleError: true, - expActions: 0, - fakeClient: osClient, - fakeDocker: &fakeDockerRegistryClient{ - ConnErr: expErr, - }, + // spec tag will be imported + { + run: true, stream: &api.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", + Tags: map[string]api.TagReference{ + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + }, + }, }, }, }, - "retry-able error no. 2": { - singleError: true, - expActions: 0, - fakeClient: osClient, - fakeDocker: &fakeDockerRegistryClient{ - Err: expErr, - }, + // spec tag with generation with no pending status will be imported + { + run: true, stream: &api.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, Spec: api.ImageStreamSpec{ - DockerImageRepository: "foo/bar", + Tags: map[string]api.TagReference{ + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Generation: &two, + }, + }, }, }, }, - "retry-able error no. 3": { - singleError: false, - expActions: 0, - fakeClient: osClient, - fakeDocker: &fakeDockerRegistryClient{ - ConnErr: expErr, - }, + // spec tag with generation with older status generation will be imported + { + run: true, stream: &api.ImageStream{ ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ - api.DefaultImageTag: { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "foo/bar", - }, + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Generation: &two, }, }, }, + Status: api.ImageStreamStatus{ + Tags: map[string]api.TagEventList{"latest": {Items: []api.TagEvent{{Generation: 1}}}}, + }, }, }, - "retry-able error no. 4": { - singleError: false, - expActions: 0, - fakeClient: osClient, - fakeDocker: &fakeDockerRegistryClient{ - Images: []expectedImage{ - { - Tag: api.DefaultImageTag, - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, + // spec tag with generation with status condition error and equal generation will not be imported + { + stream: &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, + Name: "test", + Namespace: "other", + }, + Spec: api.ImageStreamSpec{ + Tags: map[string]api.TagReference{ + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Generation: &two, }, - Err: expErr, }, }, + Status: api.ImageStreamStatus{ + Tags: map[string]api.TagEventList{"latest": {Conditions: []api.TagEventCondition{ + { + Type: api.ImportSuccess, + Status: kapi.ConditionFalse, + Generation: 2, + }, + }}}, + }, }, + }, + // spec tag with generation with status condition error and older generation will be imported + { + run: true, stream: &api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, + Name: "test", + Namespace: "other", + }, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ - api.DefaultImageTag: { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "foo/bar", - }, + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Generation: &two, }, }, }, - }, - }, - "retry-able error no. 5": { - singleError: false, - expActions: 1, - fakeClient: errISMClient, - fakeDocker: &fakeDockerRegistryClient{ - Images: []expectedImage{ - { - Tag: api.DefaultImageTag, - Image: &dockerregistry.Image{ - Image: docker.Image{ - Comment: "foo", - Config: &docker.Config{}, - }, + Status: api.ImageStreamStatus{ + Tags: map[string]api.TagEventList{"latest": {Conditions: []api.TagEventCondition{ + { + Type: api.ImportSuccess, + Status: kapi.ConditionFalse, + Generation: 1, }, - }, + }}}, }, }, + }, + // spec tag with generation with older status generation will be imported + { + run: true, stream: &api.ImageStream{ - ObjectMeta: kapi.ObjectMeta{Name: "test", Namespace: "other"}, + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{api.DockerImageRepositoryCheckAnnotation: unversioned.Now().UTC().Format(time.RFC3339)}, + Name: "test", + Namespace: "other", + }, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ - api.DefaultImageTag: { - From: &kapi.ObjectReference{ - Kind: "DockerImage", - Name: "foo/bar", - }, + "latest": { + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "test/other:latest"}, + Generation: &two, }, }, }, + Status: api.ImageStreamStatus{ + Tags: map[string]api.TagEventList{"latest": {Items: []api.TagEvent{{Generation: 1}}}}, + }, }, }, } - for name, test := range tests { - c := ImportController{client: test.fakeDocker, streams: test.fakeClient, mappings: test.fakeClient} - - err := c.Next(test.stream) - if err == nil { - t.Errorf("%s: unexpected error: %v", name, err) + for i, test := range testCases { + fake := &client.Fake{} + c := ImportController{streams: fake} + other, err := kapi.Scheme.DeepCopy(test.stream) + if err != nil { + t.Fatal(err) } - // The first condition checks error from the getTags method only, - // iow. where the error returned is the exact error that happened. - // The second condition checks error from the importTags method only, - // iow. where the error is an aggregate. - if test.singleError && err != expErr { - t.Errorf("%s: unexpected error from getTags: %v", name, err) - } else if !test.singleError && !strings.Contains(err.Error(), expErr.Error()) { - t.Errorf("%s: unexpected error from importTags: %v", name, err) + + if err := c.Next(test.stream); err != nil { + t.Errorf("%d: unexpected error: %v", i, err) } - if len(test.fakeClient.Actions()) != test.expActions { - t.Errorf("%s: expected no actions: %#v", name, test.fakeClient.Actions()) + if test.run { + if len(fake.Actions()) == 0 { + t.Errorf("%d: expected remote calls: %#v", i, fake) + } + } else { + if !kapi.Semantic.DeepEqual(test.stream, other) { + t.Errorf("%d: did not expect change to stream: %s", i, util.ObjectGoPrintDiff(test.stream, other)) + } + if len(fake.Actions()) != 0 { + t.Errorf("%d: did not expect remote calls", i) + } } } } -func isRFC3339(s string) bool { - _, err := time.Parse(time.RFC3339, s) - return err == nil +func TestControllerExternalRepo(t *testing.T) { + fake := &client.Fake{} + c := ImportController{streams: fake} + + stream := api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Name: "test", + Namespace: "other", + }, + Spec: api.ImageStreamSpec{ + Tags: map[string]api.TagReference{ + "1.1": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "some/repo:mytag", + }, + }, + }, + }, + } + if err := c.Next(&stream); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fake.Actions() + if len(actions) != 1 { + t.Fatalf("expected 1 actions, got %#v", actions) + } + if !actions[0].Matches("create", "imagestreamimports") { + t.Errorf("expected a create action: %#v", actions) + } } diff --git a/pkg/image/controller/factory.go b/pkg/image/controller/factory.go index 181d1dc524e0..387e63679f80 100644 --- a/pkg/image/controller/factory.go +++ b/pkg/image/controller/factory.go @@ -36,8 +36,7 @@ func (f *ImportControllerFactory) Create() controller.RunnableController { cache.NewReflector(lw, &api.ImageStream{}, q, 2*time.Minute).Run() c := &ImportController{ - streams: f.Client, - mappings: f.Client, + streams: f.Client, } return &controller.RetryController{ diff --git a/pkg/image/importer/credentials.go b/pkg/image/importer/credentials.go new file mode 100644 index 000000000000..cc765bd09f9a --- /dev/null +++ b/pkg/image/importer/credentials.go @@ -0,0 +1,112 @@ +package importer + +import ( + "fmt" + "net/url" + "sync" + + "github.com/golang/glog" + + "github.com/docker/distribution/registry/client/auth" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/credentialprovider" + "k8s.io/kubernetes/pkg/util" +) + +var ( + NoCredentials auth.CredentialStore = &noopCredentialStore{} + + emptyKeyring = &credentialprovider.BasicDockerKeyring{} +) + +type noopCredentialStore struct{} + +func (s *noopCredentialStore) Basic(url *url.URL) (string, string) { + glog.Infof("asked to provide Basic credentials for %s", url) + return "", "" +} + +func NewBasicCredentials() *BasicCredentials { + return &BasicCredentials{} +} + +type basicForURL struct { + url url.URL + username, password string +} + +type BasicCredentials struct { + creds []basicForURL +} + +func (c *BasicCredentials) Add(url *url.URL, username, password string) { + c.creds = append(c.creds, basicForURL{*url, username, password}) +} + +func (c *BasicCredentials) Basic(url *url.URL) (string, string) { + for _, cred := range c.creds { + if len(cred.url.Host) != 0 && cred.url.Host != url.Host { + continue + } + if len(cred.url.Path) != 0 && cred.url.Path != url.Path { + continue + } + return cred.username, cred.password + } + return "", "" +} + +func NewLocalCredentials() auth.CredentialStore { + return &keyringCredentialStore{credentialprovider.NewDockerKeyring()} +} + +type keyringCredentialStore struct { + credentialprovider.DockerKeyring +} + +func (s *keyringCredentialStore) Basic(url *url.URL) (string, string) { + return basicCredentialsFromKeyring(s.DockerKeyring, url) +} + +func NewCredentialsForSecrets(secrets []kapi.Secret) auth.CredentialStore { + return &secretCredentialStore{secrets: secrets} +} + +type secretCredentialStore struct { + secrets []kapi.Secret + + lock sync.Mutex + keyring credentialprovider.DockerKeyring +} + +func (s *secretCredentialStore) Basic(url *url.URL) (string, string) { + return basicCredentialsFromKeyring(s.init(), url) +} + +func (s *secretCredentialStore) init() credentialprovider.DockerKeyring { + s.lock.Lock() + defer s.lock.Unlock() + if s.keyring != nil { + return s.keyring + } + // TODO: need a version of this that is best effort secret - otherwise one error blocks all secrets + keyring, err := credentialprovider.MakeDockerKeyring(s.secrets, emptyKeyring) + if err != nil { + util.HandleError(fmt.Errorf("unable to create Docker registry pull credentials for secrets: %v", err)) + keyring = emptyKeyring + } + s.keyring = keyring + return keyring +} + +func basicCredentialsFromKeyring(keyring credentialprovider.DockerKeyring, url *url.URL) (string, string) { + // TODO: compare this logic to Docker authConfig in v2 configuration + value := url.Host + url.Path + configs, found := keyring.Lookup(value) + if !found || len(configs) == 0 { + glog.V(5).Infof("Unable to find a secret to match %s (%s)", url, value) + return "", "" + } + return configs[0].Username, configs[0].Password +} diff --git a/pkg/image/importer/credentials_test.go b/pkg/image/importer/credentials_test.go new file mode 100644 index 000000000000..b36a3b7c6a64 --- /dev/null +++ b/pkg/image/importer/credentials_test.go @@ -0,0 +1,39 @@ +package importer + +import ( + "io/ioutil" + "net/url" + "testing" + + kapi "k8s.io/kubernetes/pkg/api" +) + +func TestCredentialsForSecrets(t *testing.T) { + data, err := ioutil.ReadFile("../../../test/fixtures/image-secrets.json") + if err != nil { + t.Fatal(err) + } + obj, err := kapi.Codec.Decode(data) + if err != nil { + t.Fatal(err) + } + store := NewCredentialsForSecrets(obj.(*kapi.SecretList).Items) + user, pass := store.Basic(&url.URL{Scheme: "https", Host: "172.30.213.112:5000"}) + if user != "serviceaccount" || len(pass) == 0 { + t.Errorf("unexpected username and password: %s %s", user, pass) + } +} + +func TestBasicCredentials(t *testing.T) { + creds := NewBasicCredentials() + creds.Add(&url.URL{Host: "localhost"}, "test", "other") + if u, p := creds.Basic(&url.URL{Host: "test"}); u != "" || p != "" { + t.Fatalf("unexpected response: %s %s", u, p) + } + if u, p := creds.Basic(&url.URL{Host: "localhost"}); u != "test" || p != "other" { + t.Fatalf("unexpected response: %s %s", u, p) + } + if u, p := creds.Basic(&url.URL{Host: "localhost", Path: "/foo"}); u != "test" || p != "other" { + t.Fatalf("unexpected response: %s %s", u, p) + } +} diff --git a/pkg/image/importer/importer.go b/pkg/image/importer/importer.go new file mode 100644 index 000000000000..1ea3de163cdd --- /dev/null +++ b/pkg/image/importer/importer.go @@ -0,0 +1,792 @@ +package importer + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/golang/glog" + gocontext "golang.org/x/net/context" + + "github.com/docker/distribution" + "github.com/docker/distribution/context" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/distribution/registry/api/v2" + registryclient "github.com/docker/distribution/registry/client" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + + kapi "k8s.io/kubernetes/pkg/api" + kapierrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/util" + "k8s.io/kubernetes/pkg/util/fielderrors" + "k8s.io/kubernetes/pkg/util/sets" + + "github.com/openshift/origin/pkg/dockerregistry" + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/api/dockerpre012" +) + +// Add a dockerregistry.Client to the passed context with this key to support v1 Docker registry importing +const ContextKeyV1RegistryClient = "v1-registry-client" + +// Interface loads images into an image stream import request. +type Interface interface { + Import(ctx gocontext.Context, isi *api.ImageStreamImport) error +} + +// RepositoryRetriever fetches a Docker distribution.Repository. +type RepositoryRetriever interface { + // Repository returns a properly authenticated distribution.Repository for the given registry, repository + // name, and insecure toleration behavior. + Repository(ctx gocontext.Context, registry *url.URL, repoName string, insecure bool) (distribution.Repository, error) +} + +// ErrNotV2Registry is returned when the server does not report itself as a V2 Docker registry +type ErrNotV2Registry struct { + Registry string +} + +func (e *ErrNotV2Registry) Error() string { + return fmt.Sprintf("endpoint %q does not support v2 API", e.Registry) +} + +// ImageStreamImport implements an import strategy for Docker images. It keeps a cache of images +// per distinct auth context to reduce duplicate loads. This type is not thread safe. +type ImageStreamImporter struct { + maximumTagsPerRepo int + + retriever RepositoryRetriever + limiter util.RateLimiter + + imageCache map[gocontext.Context]map[manifestKey]*api.Image +} + +// NewImageStreamImport creates an importer that will load images from a remote Docker registry into an +// ImageStreamImport object. Limiter may be nil. +func NewImageStreamImporter(retriever RepositoryRetriever, maximumTagsPerRepo int, limiter util.RateLimiter) *ImageStreamImporter { + if limiter == nil { + limiter = util.NewFakeRateLimiter() + } + return &ImageStreamImporter{ + maximumTagsPerRepo: maximumTagsPerRepo, + + retriever: retriever, + limiter: limiter, + + imageCache: make(map[gocontext.Context]map[manifestKey]*api.Image), + } +} + +// contextImageCache returns the image cache entry for a context. +func (i *ImageStreamImporter) contextImageCache(ctx gocontext.Context) map[manifestKey]*api.Image { + cache := i.imageCache[ctx] + if cache == nil { + cache = make(map[manifestKey]*api.Image) + i.imageCache[ctx] = cache + } + return cache +} + +// Import tries to complete the provided isi object with images loaded from remote registries. +func (i *ImageStreamImporter) Import(ctx gocontext.Context, isi *api.ImageStreamImport) error { + cache := i.contextImageCache(ctx) + importImages(ctx, i.retriever, isi, cache, i.limiter) + importFromRepository(ctx, i.retriever, isi, i.maximumTagsPerRepo, cache, i.limiter) + return nil +} + +// importImages updates the passed ImageStreamImport object and sets Status for each image based on whether the import +// succeeded or failed. Cache is updated with any loaded images. Limiter is optional and controls how fast images are updated. +func importImages(ctx gocontext.Context, retriever RepositoryRetriever, isi *api.ImageStreamImport, cache map[manifestKey]*api.Image, limiter util.RateLimiter) { + tags := make(map[manifestKey][]int) + ids := make(map[manifestKey][]int) + repositories := make(map[repositoryKey]*importRepository) + + isi.Status.Images = make([]api.ImageImportStatus, len(isi.Spec.Images)) + for i := range isi.Spec.Images { + spec := &isi.Spec.Images[i] + from := spec.From + if from.Kind != "DockerImage" { + continue + } + ref, err := api.ParseDockerImageReference(from.Name) + if err != nil { + isi.Status.Images[i].Status = invalidStatus("", fielderrors.NewFieldInvalid("from.name", from.Name, fmt.Sprintf("invalid name: %v", err))) + continue + } + defaultRef := ref.DockerClientDefaults() + repoName := defaultRef.RepositoryName() + registryURL := defaultRef.RegistryURL() + + key := repositoryKey{url: *registryURL, name: repoName} + repo, ok := repositories[key] + if !ok { + repo = &importRepository{ + Ref: ref, + Registry: &key.url, + Name: key.name, + Insecure: spec.ImportPolicy.Insecure, + } + repositories[key] = repo + } + + if len(defaultRef.ID) > 0 { + id := manifestKey{repositoryKey: key} + id.value = defaultRef.ID + ids[id] = append(ids[id], i) + if len(ids[id]) == 1 { + repo.Digests = append(repo.Digests, importDigest{ + Name: defaultRef.ID, + Image: cache[id], + }) + } + } else { + tag := manifestKey{repositoryKey: key} + tag.value = defaultRef.Tag + tags[tag] = append(tags[tag], i) + if len(tags[tag]) == 1 { + repo.Tags = append(repo.Tags, importTag{ + Name: defaultRef.Tag, + Image: cache[tag], + }) + } + } + } + + // for each repository we found, import all tags and digests + for key, repo := range repositories { + importRepositoryFromDocker(ctx, retriever, repo, limiter) + for _, tag := range repo.Tags { + j := manifestKey{repositoryKey: key} + j.value = tag.Name + if tag.Image != nil { + cache[j] = tag.Image + } + for _, index := range tags[j] { + if tag.Err != nil { + setImageImportStatus(isi, index, tag.Err) + continue + } + copied := *tag.Image + image := &isi.Status.Images[index] + ref := repo.Ref + ref.Tag, ref.ID = tag.Name, copied.Name + copied.DockerImageReference = ref.MostSpecific().Exact() + image.Tag = tag.Name + image.Image = &copied + image.Status.Status = unversioned.StatusSuccess + } + } + for _, digest := range repo.Digests { + j := manifestKey{repositoryKey: key} + j.value = digest.Name + if digest.Image != nil { + cache[j] = digest.Image + } + for _, index := range ids[j] { + if digest.Err != nil { + setImageImportStatus(isi, index, digest.Err) + continue + } + image := &isi.Status.Images[index] + copied := *digest.Image + ref := repo.Ref + ref.Tag, ref.ID = "", copied.Name + copied.DockerImageReference = ref.MostSpecific().Exact() + image.Image = &copied + image.Status.Status = unversioned.StatusSuccess + } + } + } +} + +// importFromRepository imports the repository named on the ImageStreamImport, if any, importing up to maximumTags, and reporting +// status on each image that is attempted to be imported. If the repository cannot be found or tags cannot be retrieved, the repository +// status field is set. +func importFromRepository(ctx gocontext.Context, retriever RepositoryRetriever, isi *api.ImageStreamImport, maximumTags int, cache map[manifestKey]*api.Image, limiter util.RateLimiter) { + if isi.Spec.Repository == nil { + return + } + isi.Status.Repository = &api.RepositoryImportStatus{} + status := isi.Status.Repository + + spec := isi.Spec.Repository + from := spec.From + if from.Kind != "DockerImage" { + return + } + ref, err := api.ParseDockerImageReference(from.Name) + if err != nil { + status.Status = invalidStatus("", fielderrors.NewFieldInvalid("from.name", from.Name, fmt.Sprintf("invalid name: %v", err))) + return + } + defaultRef := ref.DockerClientDefaults() + repoName := defaultRef.RepositoryName() + registryURL := defaultRef.RegistryURL() + + key := repositoryKey{url: *registryURL, name: repoName} + repo := &importRepository{ + Ref: ref, + Registry: &key.url, + Name: key.name, + Insecure: spec.ImportPolicy.Insecure, + MaximumTags: maximumTags, + } + importRepositoryFromDocker(ctx, retriever, repo, limiter) + + if repo.Err != nil { + status.Status = imageImportStatus(repo.Err, "", "repository") + return + } + + additional := []string{} + tagKey := manifestKey{repositoryKey: key} + for _, s := range repo.AdditionalTags { + tagKey.value = s + if image, ok := cache[tagKey]; ok { + repo.Tags = append(repo.Tags, importTag{ + Name: s, + Image: image, + }) + } else { + additional = append(additional, s) + } + } + status.AdditionalTags = additional + + failures := 0 + status.Status.Status = unversioned.StatusSuccess + status.Images = make([]api.ImageImportStatus, len(repo.Tags)) + for i, tag := range repo.Tags { + if tag.Err != nil { + failures++ + status.Images[i].Status = imageImportStatus(tag.Err, "", "repository") + continue + } + status.Images[i].Status.Status = unversioned.StatusSuccess + + copied := *tag.Image + ref.Tag, ref.ID = tag.Name, copied.Name + copied.DockerImageReference = ref.MostSpecific().Exact() + status.Images[i].Tag = tag.Name + status.Images[i].Image = &copied + } + if failures > 0 { + status.Status.Status = unversioned.StatusFailure + status.Status.Reason = unversioned.StatusReason("ImportFailed") + switch failures { + case 1: + status.Status.Message = "one of the images from this repository failed to import" + default: + status.Status.Message = fmt.Sprintf("%d of the images from this repository failed to import", failures) + } + } +} + +func applyErrorToRepository(repository *importRepository, err error) { + repository.Err = err + for i := range repository.Tags { + repository.Tags[i].Err = err + } + for i := range repository.Digests { + repository.Digests[i].Err = err + } +} + +// importRepositoryFromDocker loads the tags and images requested in the passed importRepository, obeying the +// optional rate limiter. Errors are set onto the individual tags and digest objects. +func importRepositoryFromDocker(ctx gocontext.Context, retriever RepositoryRetriever, repository *importRepository, limiter util.RateLimiter) { + // retrieve the repository + repo, err := retriever.Repository(ctx, repository.Registry, repository.Name, repository.Insecure) + if err != nil { + glog.V(5).Infof("unable to access repository %#v: %#v", repository, err) + switch { + case isDockerError(err, v2.ErrorCodeNameUnknown): + err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) + case isDockerError(err, errcode.ErrorCodeUnauthorized): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + case strings.Contains(err.Error(), "tls: oversized record received with length") && !repository.Insecure: + err = kapierrors.NewBadRequest("this repository is HTTP only and requires the insecure flag to import") + case strings.HasSuffix(err.Error(), "no basic auth credentials"): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) + case strings.HasSuffix(err.Error(), "does not support v2 API"): + importRepositoryFromDockerV1(ctx, repository, limiter) + return + } + applyErrorToRepository(repository, err) + return + } + + // get a manifest context + s, err := repo.Manifests(ctx) + if err != nil { + glog.V(5).Infof("unable to access manifests for repository %#v: %#v", repository, err) + switch { + case isDockerError(err, v2.ErrorCodeNameUnknown): + err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) + case isDockerError(err, errcode.ErrorCodeUnauthorized): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + case strings.HasSuffix(err.Error(), "no basic auth credentials"): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) + } + applyErrorToRepository(repository, err) + return + } + + // if repository import is requested (MaximumTags), attempt to load the tags, sort them, and request the first N + if count := repository.MaximumTags; count > 0 { + tags, err := s.Tags() + if err != nil { + glog.V(5).Infof("unable to access tags for repository %#v: %#v", repository, err) + switch { + case isDockerError(err, v2.ErrorCodeNameUnknown): + err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) + case isDockerError(err, errcode.ErrorCodeUnauthorized): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + } + repository.Err = err + return + } + // some images on the Hub have empty tags - treat those as "latest" + set := sets.NewString(tags...) + if set.Has("") { + set.Delete("") + set.Insert(api.DefaultImageTag) + } + tags = set.List() + // include only the top N tags in the result, put the rest in AdditionalTags + api.PrioritizeTags(tags) + for _, s := range tags { + if count <= 0 { + repository.AdditionalTags = append(repository.AdditionalTags, s) + continue + } + count-- + repository.Tags = append(repository.Tags, importTag{ + Name: s, + }) + } + } + + // load digests + for i := range repository.Digests { + importDigest := &repository.Digests[i] + if importDigest.Err != nil || importDigest.Image != nil { + continue + } + d, err := digest.ParseDigest(importDigest.Name) + if err != nil { + importDigest.Err = err + continue + } + limiter.Accept() + m, err := s.Get(d) + if err != nil { + glog.V(5).Infof("unable to access digest %q for repository %#v: %#v", d, repository, err) + switch { + case isDockerError(err, v2.ErrorCodeManifestUnknown): + ref := repository.Ref + ref.Tag, ref.ID = "", importDigest.Name + err = kapierrors.NewNotFound("DockerImage", ref.Exact()) + case isDockerError(err, errcode.ErrorCodeUnauthorized): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + case strings.HasSuffix(err.Error(), "no basic auth credentials"): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + } + importDigest.Err = err + continue + } + importDigest.Image, err = schema1ToImage(m, d) + if err != nil { + importDigest.Err = err + continue + } + if err := api.ImageWithMetadata(importDigest.Image); err != nil { + importDigest.Err = err + continue + } + } + + for i := range repository.Tags { + importTag := &repository.Tags[i] + if importTag.Err != nil || importTag.Image != nil { + continue + } + limiter.Accept() + m, err := s.GetByTag(importTag.Name) + if err != nil { + glog.V(5).Infof("unable to access tag %q for repository %#v: %#v", importTag.Name, repository, err) + switch { + case isDockerError(err, v2.ErrorCodeManifestUnknown): + ref := repository.Ref + ref.Tag = importTag.Name + err = kapierrors.NewNotFound("DockerImage", ref.Exact()) + case isDockerError(err, errcode.ErrorCodeUnauthorized): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + case strings.HasSuffix(err.Error(), "no basic auth credentials"): + err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) + } + importTag.Err = err + continue + } + importTag.Image, err = schema1ToImage(m, "") + if err != nil { + importTag.Err = err + continue + } + if err := api.ImageWithMetadata(importTag.Image); err != nil { + importTag.Err = err + continue + } + } +} + +func importRepositoryFromDockerV1(ctx gocontext.Context, repository *importRepository, limiter util.RateLimiter) { + value := ctx.Value(ContextKeyV1RegistryClient) + if value == nil { + err := kapierrors.NewForbidden("", "", fmt.Errorf("registry %q does not support the v2 Registry API", repository.Registry.Host)).(*kapierrors.StatusError) + err.ErrStatus.Reason = "NotV2Registry" + applyErrorToRepository(repository, err) + return + } + client, ok := value.(dockerregistry.Client) + if !ok { + err := kapierrors.NewForbidden("", "", fmt.Errorf("registry %q does not support the v2 Registry API", repository.Registry.Host)).(*kapierrors.StatusError) + err.ErrStatus.Reason = "NotV2Registry" + return + } + conn, err := client.Connect(repository.Registry.Host, repository.Insecure) + if err != nil { + applyErrorToRepository(repository, err) + return + } + + // if repository import is requested (MaximumTags), attempt to load the tags, sort them, and request the first N + if count := repository.MaximumTags; count > 0 { + tagMap, err := conn.ImageTags(repository.Ref.Namespace, repository.Ref.Name) + if err != nil { + repository.Err = err + return + } + tags := make([]string, 0, len(tagMap)) + for tag := range tagMap { + tags = append(tags, tag) + } + // some images on the Hub have empty tags - treat those as "latest" + set := sets.NewString(tags...) + if set.Has("") { + set.Delete("") + set.Insert(api.DefaultImageTag) + } + tags = set.List() + // include only the top N tags in the result, put the rest in AdditionalTags + api.PrioritizeTags(tags) + for _, s := range tags { + if count <= 0 { + repository.AdditionalTags = append(repository.AdditionalTags, s) + continue + } + count-- + repository.Tags = append(repository.Tags, importTag{ + Name: s, + }) + } + } + + // load digests + for i := range repository.Digests { + importDigest := &repository.Digests[i] + if importDigest.Err != nil || importDigest.Image != nil { + continue + } + limiter.Accept() + image, err := conn.ImageByID(repository.Ref.Namespace, repository.Ref.Name, importDigest.Name) + if err != nil { + importDigest.Err = err + continue + } + // we do not preserve manifests of legacy images + importDigest.Image, err = schema0ToImage(image, importDigest.Name) + if err != nil { + importDigest.Err = err + continue + } + } + + for i := range repository.Tags { + importTag := &repository.Tags[i] + if importTag.Err != nil || importTag.Image != nil { + continue + } + limiter.Accept() + image, err := conn.ImageByTag(repository.Ref.Namespace, repository.Ref.Name, importTag.Name) + if err != nil { + importTag.Err = err + continue + } + // we do not preserve manifests of legacy images + importTag.Image, err = schema0ToImage(image, "") + if err != nil { + importTag.Err = err + continue + } + } +} + +type importTag struct { + Name string + Image *api.Image + Err error +} + +type importDigest struct { + Name string + Image *api.Image + Err error +} + +type importRepository struct { + Ref api.DockerImageReference + Registry *url.URL + Name string + Insecure bool + + Tags []importTag + Digests []importDigest + + MaximumTags int + AdditionalTags []string + Err error +} + +// repositoryKey is the key used to cache information loaded from a remote Docker repository. +type repositoryKey struct { + // The URL of the server + url url.URL + // The name of the image repository (contains both namespace and path) + name string +} + +// manifestKey is a key for a map between a Docker image tag or image ID and a retrieved api.Image, used +// to ensure we don't fetch the same image multiple times. +type manifestKey struct { + repositoryKey + // The tag or ID of the image, not used within the same map + value string +} + +func imageImportStatus(err error, kind, position string) unversioned.Status { + switch t := err.(type) { + case client.APIStatus: + return t.Status() + case *fielderrors.ValidationError: + return kapierrors.NewInvalid(kind, position, fielderrors.ValidationErrorList{t}).(client.APIStatus).Status() + default: + return kapierrors.NewInternalError(err).(client.APIStatus).Status() + } +} + +func setImageImportStatus(images *api.ImageStreamImport, i int, err error) { + images.Status.Images[i].Status = imageImportStatus(err, "", "") +} + +func invalidStatus(position string, errs ...error) unversioned.Status { + return kapierrors.NewInvalid("", position, errs).(client.APIStatus).Status() +} + +// NewContext is capable of creating RepositoryRetrievers. +func NewContext(transport http.RoundTripper) Context { + return Context{ + Transport: transport, + Challenges: auth.NewSimpleChallengeManager(), + } +} + +type Context struct { + Transport http.RoundTripper + Challenges auth.ChallengeManager +} + +func (c Context) WithCredentials(credentials auth.CredentialStore) RepositoryRetriever { + return &repositoryRetriever{ + context: c, + credentials: credentials, + + pings: make(map[url.URL]error), + redirect: make(map[url.URL]*url.URL), + } +} + +type repositoryRetriever struct { + context Context + credentials auth.CredentialStore + + pings map[url.URL]error + redirect map[url.URL]*url.URL +} + +func (r *repositoryRetriever) Repository(ctx gocontext.Context, registry *url.URL, repoName string, insecure bool) (distribution.Repository, error) { + src := *registry + // ping the registry to get challenge headers + if err, ok := r.pings[src]; ok { + if err != nil { + return nil, err + } + if redirect, ok := r.redirect[src]; ok { + src = *redirect + } + } else { + redirect, err := r.ping(src, insecure) + r.pings[src] = err + if err != nil { + return nil, err + } + if redirect != nil { + r.redirect[src] = redirect + src = *redirect + } + } + + rt := transport.NewTransport( + r.context.Transport, + // TODO: slightly smarter authorizer that retries unauthenticated requests + auth.NewAuthorizer( + r.context.Challenges, + auth.NewTokenHandler(r.context.Transport, r.credentials, repoName, "pull"), + auth.NewBasicHandler(r.credentials), + ), + ) + return registryclient.NewRepository(context.Context(ctx), repoName, src.String(), rt) +} + +func (r *repositoryRetriever) ping(registry url.URL, insecure bool) (*url.URL, error) { + pingClient := &http.Client{ + Transport: r.context.Transport, + Timeout: 15 * time.Second, + } + target := registry + target.Path = path.Join(target.Path, "v2") + "/" + req, err := http.NewRequest("GET", target.String(), nil) + if err != nil { + return nil, err + } + resp, err := pingClient.Do(req) + if err != nil { + if insecure && registry.Scheme == "https" { + glog.V(5).Infof("Falling back to an HTTP check for an insecure registry %s: %v", registry, err) + registry.Scheme = "http" + _, nErr := r.ping(registry, true) + if nErr != nil { + return nil, err + } + return ®istry, nil + } + return nil, err + } + defer resp.Body.Close() + + versions := auth.APIVersions(resp, "Docker-Distribution-API-Version") + if len(versions) == 0 { + glog.V(5).Infof("Registry responded to v2 Docker endpoint, but has no header for Docker Distribution %s: %d, %#v", req.URL, resp.StatusCode, resp.Header) + return nil, &ErrNotV2Registry{Registry: registry.String()} + } + + r.context.Challenges.AddResponse(resp) + + return nil, nil +} + +func schema1ToImage(manifest *schema1.SignedManifest, d digest.Digest) (*api.Image, error) { + if len(manifest.History) == 0 { + return nil, fmt.Errorf("image has no v1Compatibility history and cannot be used") + } + dockerImage, err := unmarshalDockerImage([]byte(manifest.History[0].V1Compatibility)) + if err != nil { + return nil, err + } + if len(d) > 0 { + dockerImage.ID = d.String() + } else { + if p, err := manifest.Payload(); err == nil { + d, err := digest.FromBytes(p) + if err != nil { + return nil, fmt.Errorf("unable to create digest from image payload: %v", err) + } + dockerImage.ID = d.String() + } else { + d, err := digest.FromBytes(manifest.Raw) + if err != nil { + return nil, fmt.Errorf("unable to create digest from image bytes: %v", err) + } + dockerImage.ID = d.String() + } + } + image := &api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: dockerImage.ID, + }, + DockerImageMetadata: *dockerImage, + DockerImageManifest: string(manifest.Raw), + DockerImageMetadataVersion: "1.0", + } + + return image, nil +} + +func schema0ToImage(dockerImage *dockerregistry.Image, id string) (*api.Image, error) { + var baseImage api.DockerImage + if err := kapi.Scheme.Convert(&dockerImage.Image, &baseImage); err != nil { + return nil, fmt.Errorf("could not convert image: %#v", err) + } + + image := &api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: dockerImage.ID, + }, + DockerImageMetadata: baseImage, + DockerImageMetadataVersion: "1.0", + } + + return image, nil +} + +func unmarshalDockerImage(body []byte) (*api.DockerImage, error) { + var image dockerpre012.DockerImage + if err := json.Unmarshal(body, &image); err != nil { + return nil, err + } + dockerImage := &api.DockerImage{} + if err := kapi.Scheme.Convert(&image, dockerImage); err != nil { + return nil, err + } + return dockerImage, nil +} + +func isDockerError(err error, code errcode.ErrorCode) bool { + switch t := err.(type) { + case errcode.Errors: + for _, err := range t { + if isDockerError(err, code) { + return true + } + } + case errcode.ErrorCode: + if code == t { + return true + } + case errcode.Error: + if t.ErrorCode() == code { + return true + } + } + return false +} diff --git a/pkg/image/importer/importer_test.go b/pkg/image/importer/importer_test.go new file mode 100644 index 000000000000..c280b1107a05 --- /dev/null +++ b/pkg/image/importer/importer_test.go @@ -0,0 +1,354 @@ +package importer + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "strings" + "testing" + "time" + + gocontext "golang.org/x/net/context" + + "github.com/docker/distribution" + "github.com/docker/distribution/context" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + + "github.com/openshift/origin/pkg/dockerregistry" + "github.com/openshift/origin/pkg/image/api" +) + +type mockRetriever struct { + repo distribution.Repository + insecure bool + err error +} + +func (r *mockRetriever) Repository(ctx gocontext.Context, registry *url.URL, repoName string, insecure bool) (distribution.Repository, error) { + r.insecure = insecure + return r.repo, r.err +} + +type mockRepository struct { + repoErr, getErr, getByTagErr, tagsErr, err error + + manifest *schema1.SignedManifest + tags []string +} + +func (r *mockRepository) Name() string { return "test" } + +func (r *mockRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { + return r, r.repoErr +} +func (r *mockRepository) Blobs(ctx context.Context) distribution.BlobStore { return nil } +func (r *mockRepository) Signatures() distribution.SignatureService { return nil } +func (r *mockRepository) Exists(dgst digest.Digest) (bool, error) { + return false, fmt.Errorf("not implemented") +} +func (r *mockRepository) Get(dgst digest.Digest) (*schema1.SignedManifest, error) { + return r.manifest, r.getErr +} +func (r *mockRepository) Enumerate() ([]digest.Digest, error) { + return nil, fmt.Errorf("not implemented") +} +func (r *mockRepository) Delete(dgst digest.Digest) error { return fmt.Errorf("not implemented") } +func (r *mockRepository) Put(manifest *schema1.SignedManifest) error { + return fmt.Errorf("not implemented") +} +func (r *mockRepository) Tags() ([]string, error) { return r.tags, r.tagsErr } +func (r *mockRepository) ExistsByTag(tag string) (bool, error) { + return false, fmt.Errorf("not implemented") +} +func (r *mockRepository) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*schema1.SignedManifest, error) { + return r.manifest, r.getByTagErr +} + +func TestImportNothing(t *testing.T) { + ctx := NewContext(http.DefaultTransport).WithCredentials(NoCredentials) + isi := &api.ImageStreamImport{} + i := NewImageStreamImporter(ctx, 5, nil) + if err := i.Import(nil, isi); err != nil { + t.Fatal(err) + } +} + +func expectStatusError(status unversioned.Status, message string) bool { + if status.Status != unversioned.StatusFailure || status.Message != message { + return false + } + return true +} + +func TestImport(t *testing.T) { + m := &schema1.SignedManifest{Raw: []byte(etcdManifest)} + if err := json.Unmarshal([]byte(etcdManifest), m); err != nil { + t.Fatal(err) + } + insecureRetriever := &mockRetriever{ + repo: &mockRepository{ + getByTagErr: fmt.Errorf("no such tag"), + getErr: fmt.Errorf("no such digest"), + }, + } + testCases := []struct { + retriever RepositoryRetriever + isi api.ImageStreamImport + expect func(*api.ImageStreamImport, *testing.T) + }{ + { + retriever: insecureRetriever, + isi: api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test"}, ImportPolicy: api.TagImportPolicy{Insecure: true}}, + }, + }, + }, + expect: func(isi *api.ImageStreamImport, t *testing.T) { + if !insecureRetriever.insecure { + t.Errorf("expected retriever to beset insecure: %#v", insecureRetriever) + } + }, + }, + { + retriever: &mockRetriever{ + repo: &mockRepository{ + getByTagErr: fmt.Errorf("no such tag"), + getErr: fmt.Errorf("no such digest"), + }, + }, + isi: api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test/un/parse/able/image"}}, + {From: kapi.ObjectReference{Kind: "ImageStreamTag", Name: "test:other"}}, + }, + }, + }, + expect: func(isi *api.ImageStreamImport, t *testing.T) { + if !expectStatusError(isi.Status.Images[0].Status, "Internal error occurred: no such tag") { + t.Errorf("unexpected status: %#v", isi.Status.Images[0].Status) + } + if !expectStatusError(isi.Status.Images[1].Status, "Internal error occurred: no such digest") { + t.Errorf("unexpected status: %#v", isi.Status.Images[1].Status) + } + if !expectStatusError(isi.Status.Images[2].Status, " \"\" is invalid: from.name: invalid value 'test/un/parse/able/image', Details: invalid name: the docker pull spec \"test/un/parse/able/image\" must be two or three segments separated by slashes") { + t.Errorf("unexpected status: %#v", isi.Status.Images[2].Status) + } + // non DockerImage refs are no-ops + if status := isi.Status.Images[3].Status; status.Status != "" { + t.Errorf("unexpected status: %#v", isi.Status.Images[3].Status) + } + }, + }, + { + retriever: &mockRetriever{err: fmt.Errorf("error")}, + isi: api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Repository: &api.RepositoryImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: "test"}, + }, + }, + }, + expect: func(isi *api.ImageStreamImport, t *testing.T) { + if !reflect.DeepEqual(isi.Status.Repository.AdditionalTags, []string(nil)) { + t.Errorf("unexpected additional tags: %#v", isi.Status.Repository) + } + if len(isi.Status.Repository.Images) != 0 { + t.Errorf("unexpected number of images: %#v", isi.Status.Repository.Images) + } + if isi.Status.Repository.Status.Status != unversioned.StatusFailure || isi.Status.Repository.Status.Message != "Internal error occurred: error" { + t.Errorf("unexpected status: %#v", isi.Status.Repository.Status) + } + }, + }, + { + retriever: &mockRetriever{repo: &mockRepository{manifest: m}}, + isi: api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test@sha256:958608f8ecc1dc62c93b6c610f3a834dae4220c9642e6e8b4e0f2b3ad7cbd238"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "test:tag"}}, + }, + }, + }, + expect: func(isi *api.ImageStreamImport, t *testing.T) { + if len(isi.Status.Images) != 2 { + t.Errorf("unexpected number of images: %#v", isi.Status.Repository.Images) + } + for i, image := range isi.Status.Images { + if image.Status.Status != unversioned.StatusSuccess { + t.Errorf("unexpected status %d: %#v", i, image.Status) + } + // the image name is always the sha256, and size is calculated + if image.Image == nil || image.Image.Name != "sha256:958608f8ecc1dc62c93b6c610f3a834dae4220c9642e6e8b4e0f2b3ad7cbd238" || image.Image.DockerImageMetadata.Size != 28643712 { + t.Errorf("unexpected image %d: %#v", i, image.Image.Name) + } + // the most specific reference is returned + if image.Image.DockerImageReference != "test@sha256:958608f8ecc1dc62c93b6c610f3a834dae4220c9642e6e8b4e0f2b3ad7cbd238" { + t.Errorf("unexpected ref %d: %#v", i, image.Image.DockerImageReference) + } + } + }, + }, + { + retriever: &mockRetriever{ + repo: &mockRepository{ + tags: []string{"v1", "other", "v2", "3", "3.1", "abc"}, + getByTagErr: fmt.Errorf("no such tag"), + }, + }, + isi: api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Repository: &api.RepositoryImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: "test"}, + }, + }, + }, + expect: func(isi *api.ImageStreamImport, t *testing.T) { + if !reflect.DeepEqual(isi.Status.Repository.AdditionalTags, []string{"other"}) { + t.Errorf("unexpected additional tags: %#v", isi.Status.Repository) + } + if len(isi.Status.Repository.Images) != 5 { + t.Errorf("unexpected number of images: %#v", isi.Status.Repository.Images) + } + for i, image := range isi.Status.Repository.Images { + if image.Status.Status != unversioned.StatusFailure || image.Status.Message != "Internal error occurred: no such tag" { + t.Errorf("unexpected status %d: %#v", i, isi.Status.Repository.Images) + } + } + }, + }, + } + for i, test := range testCases { + im := NewImageStreamImporter(test.retriever, 5, nil) + if err := im.Import(nil, &test.isi); err != nil { + t.Errorf("%d: %v", i, err) + } + if test.expect != nil { + test.expect(&test.isi, t) + } + } +} + +const etcdManifest = ` +{ + "schemaVersion": 1, + "tag": "latest", + "name": "coreos/etcd", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:2560187847cadddef806eaf244b7755af247a9dbabb90ca953dd2703cf423766" + }, + { + "blobSum": "sha256:744b46d0ac8636c45870a03830d8d82c20b75fbfb9bc937d5e61005d23ad4cfe" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"fe50ac14986497fa6b5d2cc24feb4a561d01767bc64413752c0988cb70b0b8b9\",\"parent\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"created\":\"2015-12-30T22:29:13.967754365Z\",\"container\":\"c8d0f1a274b5f52fa5beb280775ef07cf18ec0f95e5ae42fbad01157e2614d42\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT \\u0026{[\\\"/etcd\\\"]}\"],\"Image\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/etcd\"],\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/etcd\"],\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + }, + { + "v1Compatibility": "{\"id\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"parent\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"created\":\"2015-12-30T22:29:13.504159783Z\",\"container\":\"080708d544f85052a46fab72e701b4358c1b96cb4b805a5b2d66276fc2aaf85d\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) EXPOSE 2379/tcp 2380/tcp 4001/tcp 7001/tcp\"],\"Image\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + }, + { + "v1Compatibility": "{\"id\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"parent\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"created\":\"2015-12-30T22:29:12.912813629Z\",\"container\":\"f28be899c9b8680d4cf8585e663ad20b35019db062526844e7cfef117ce9037f\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:e330b1da49d993059975e46560b3bd360691498b0f2f6e00f39fc160cf8d4ec3 in /\"],\"Image\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":13502144}" + }, + { + "v1Compatibility": "{\"id\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"created\":\"2015-12-30T22:29:12.346834862Z\",\"container\":\"1b97abade59e4b5b935aede236980a54fb500cd9ee5bd4323c832c6d7b3ffc6e\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:74912593c6783292c4520514f5cc9313acbd1da0f46edee0fdbed2a24a264d6f in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":15141568}" + } + ], + "signatures": [ + { + "header": { + "alg": "RS256", + "jwk": { + "e": "AQAB", + "kty": "RSA", + "n": "yB40ou1GMvIxYs1jhxWaeoDiw3oa0_Q2UJThUPtArvO0tRzaun9FnSphhOEHIGcezfq95jy-3MN-FIjmsWgbPHY8lVDS38fF75aCw6qkholwqjmMtUIgPNYoMrg0rLUE5RRyJ84-hKf9Fk7V3fItp1mvCTGKaS3ze-y5dTTrfbNGE7qG638Dla2Fuz-9CNgRQj0JH54o547WkKJC-pG-j0jTDr8lzsXhrZC7lJas4yc-vpt3D60iG4cW_mkdtIj52ZFEgHZ56sUj7AhnNVly0ZP9W1hmw4xEHDn9WLjlt7ivwARVeb2qzsNdguUitcI5hUQNwpOVZ_O3f1rUIL_kRw" + } + }, + "protected": "eyJmb3JtYXRUYWlsIjogIkNuMCIsICJmb3JtYXRMZW5ndGgiOiA1OTI2LCAidGltZSI6ICIyMDE2LTAxLTAyVDAyOjAxOjMzWiJ9", + "signature": "DrQ43UWeit-thDoRGTCP0Gd2wL5K2ecyPhHo_au0FoXwuKODja0tfwHexB9ypvFWngk-ijXuwO02x3aRIZqkWpvKLxxzxwkrZnPSje4o_VrFU4z5zwmN8sJw52ODkQlW38PURIVksOxCrb0zRl87yTAAsUAJ_4UUPNltZSLnhwy-qPb2NQ8ghgsONcBxRQrhPFiWNkxDKZ3kjvzYyrXDxTcvwK3Kk_YagZ4rCOhH1B7mAdVSiSHIvvNV5grPshw_ipAoqL2iNMsxWxLjYZl9xSJQI2asaq3fvh8G8cZ7T-OahDUos_GyhnIj39C-9ouqdJqMUYFETqbzRCR6d36CpQ" + } + ] +}` + +func TestSchema1ToImage(t *testing.T) { + m := &schema1.SignedManifest{} + if err := json.Unmarshal([]byte(etcdManifest), m); err != nil { + t.Fatal(err) + } + image, err := schema1ToImage(m, digest.Digest("sha256:test")) + if err != nil { + t.Fatal(err) + } + if image.DockerImageMetadata.ID != "sha256:test" { + t.Errorf("unexpected image: %#v", image.DockerImageMetadata.ID) + } +} + +func TestDockerV1Fallback(t *testing.T) { + var uri *url.URL + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Docker-Endpoints", uri.Host) + + // get all tags + if strings.HasSuffix(r.URL.Path, "/tags") { + fmt.Fprintln(w, `{"tag1":"image1", "test":"image2"}`) + w.WriteHeader(http.StatusOK) + return + } + if strings.HasSuffix(r.URL.Path, "/images") { + fmt.Fprintln(w, `{"tag1":"image1", "test":"image2"}`) + w.WriteHeader(http.StatusOK) + return + } + if strings.HasSuffix(r.URL.Path, "/json") { + fmt.Fprintln(w, `{"ID":"image2"}`) + w.WriteHeader(http.StatusOK) + return + } + t.Logf("tried to access %s", r.URL.Path) + w.WriteHeader(http.StatusNotFound) + })) + + client := dockerregistry.NewClient(10*time.Second, false) + ctx := gocontext.WithValue(gocontext.Background(), ContextKeyV1RegistryClient, client) + + uri, _ = url.Parse(server.URL) + isi := &api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Repository: &api.RepositoryImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: uri.Host + "/test:test"}, + ImportPolicy: api.TagImportPolicy{Insecure: true}, + }, + }, + } + + retriever := &mockRetriever{err: fmt.Errorf("does not support v2 API")} + im := NewImageStreamImporter(retriever, 5, nil) + if err := im.Import(ctx, isi); err != nil { + t.Fatal(err) + } + if images := isi.Status.Repository.Images; len(images) != 2 || images[0].Tag != "tag1" || images[1].Tag != "test" { + t.Errorf("unexpected images: %#v", images) + } +} diff --git a/pkg/image/registry/image/etcd/etcd_test.go b/pkg/image/registry/image/etcd/etcd_test.go index 719d22e2b6d6..2fa9765c7c25 100644 --- a/pkg/image/registry/image/etcd/etcd_test.go +++ b/pkg/image/registry/image/etcd/etcd_test.go @@ -323,6 +323,231 @@ func TestGetNotFound(t *testing.T) { } } +const etcdManifest = ` +{ + "schemaVersion": 1, + "tag": "latest", + "name": "coreos/etcd", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:2560187847cadddef806eaf244b7755af247a9dbabb90ca953dd2703cf423766" + }, + { + "blobSum": "sha256:744b46d0ac8636c45870a03830d8d82c20b75fbfb9bc937d5e61005d23ad4cfe" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"fe50ac14986497fa6b5d2cc24feb4a561d01767bc64413752c0988cb70b0b8b9\",\"parent\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"created\":\"2015-12-30T22:29:13.967754365Z\",\"container\":\"c8d0f1a274b5f52fa5beb280775ef07cf18ec0f95e5ae42fbad01157e2614d42\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT \\u0026{[\\\"/etcd\\\"]}\"],\"Image\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/etcd\"],\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/etcd\"],\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + }, + { + "v1Compatibility": "{\"id\":\"a5a18474fa96a3c6e240bc88e41de2afd236520caf904356ad9d5f8d875c3481\",\"parent\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"created\":\"2015-12-30T22:29:13.504159783Z\",\"container\":\"080708d544f85052a46fab72e701b4358c1b96cb4b805a5b2d66276fc2aaf85d\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) EXPOSE 2379/tcp 2380/tcp 4001/tcp 7001/tcp\"],\"Image\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"2379/tcp\":{},\"2380/tcp\":{},\"4001/tcp\":{},\"7001/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + }, + { + "v1Compatibility": "{\"id\":\"796d581500e960cc02095dcdeccf55db215b8e54c57e3a0b11392145ffe60cf6\",\"parent\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"created\":\"2015-12-30T22:29:12.912813629Z\",\"container\":\"f28be899c9b8680d4cf8585e663ad20b35019db062526844e7cfef117ce9037f\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:e330b1da49d993059975e46560b3bd360691498b0f2f6e00f39fc160cf8d4ec3 in /\"],\"Image\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":13502144}" + }, + { + "v1Compatibility": "{\"id\":\"309c960c7f875411ae2ee2bfb97b86eee5058f3dad77206dd0df4f97df8a77fa\",\"created\":\"2015-12-30T22:29:12.346834862Z\",\"container\":\"1b97abade59e4b5b935aede236980a54fb500cd9ee5bd4323c832c6d7b3ffc6e\",\"container_config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:74912593c6783292c4520514f5cc9313acbd1da0f46edee0fdbed2a24a264d6f in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"1b97abade59e\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":15141568}" + } + ], + "signatures": [ + { + "header": { + "alg": "RS256", + "jwk": { + "e": "AQAB", + "kty": "RSA", + "n": "yB40ou1GMvIxYs1jhxWaeoDiw3oa0_Q2UJThUPtArvO0tRzaun9FnSphhOEHIGcezfq95jy-3MN-FIjmsWgbPHY8lVDS38fF75aCw6qkholwqjmMtUIgPNYoMrg0rLUE5RRyJ84-hKf9Fk7V3fItp1mvCTGKaS3ze-y5dTTrfbNGE7qG638Dla2Fuz-9CNgRQj0JH54o547WkKJC-pG-j0jTDr8lzsXhrZC7lJas4yc-vpt3D60iG4cW_mkdtIj52ZFEgHZ56sUj7AhnNVly0ZP9W1hmw4xEHDn9WLjlt7ivwARVeb2qzsNdguUitcI5hUQNwpOVZ_O3f1rUIL_kRw" + } + }, + "protected": "eyJmb3JtYXRUYWlsIjogIkNuMCIsICJmb3JtYXRMZW5ndGgiOiA1OTI2LCAidGltZSI6ICIyMDE2LTAxLTAyVDAyOjAxOjMzWiJ9", + "signature": "DrQ43UWeit-thDoRGTCP0Gd2wL5K2ecyPhHo_au0FoXwuKODja0tfwHexB9ypvFWngk-ijXuwO02x3aRIZqkWpvKLxxzxwkrZnPSje4o_VrFU4z5zwmN8sJw52ODkQlW38PURIVksOxCrb0zRl87yTAAsUAJ_4UUPNltZSLnhwy-qPb2NQ8ghgsONcBxRQrhPFiWNkxDKZ3kjvzYyrXDxTcvwK3Kk_YagZ4rCOhH1B7mAdVSiSHIvvNV5grPshw_ipAoqL2iNMsxWxLjYZl9xSJQI2asaq3fvh8G8cZ7T-OahDUos_GyhnIj39C-9ouqdJqMUYFETqbzRCR6d36CpQ" + } + ] +}` + +func TestCreateSetsMetadata(t *testing.T) { + testCases := []struct { + image *api.Image + expect func(*api.Image) bool + }{ + { + image: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + DockerImageReference: "openshift/ruby-19-centos", + }, + }, + { + expect: func(image *api.Image) bool { + if image.DockerImageMetadata.Size != 28643712 { + t.Errorf("image had size %d", image.DockerImageMetadata.Size) + return false + } + if len(image.DockerImageLayers) != 4 || image.DockerImageLayers[0].Name != "sha256:744b46d0ac8636c45870a03830d8d82c20b75fbfb9bc937d5e61005d23ad4cfe" || image.DockerImageLayers[0].Size != 15141568 { + t.Errorf("unexpected layers: %#v", image.DockerImageLayers) + return false + } + return true + }, + image: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + DockerImageReference: "openshift/ruby-19-centos", + DockerImageManifest: etcdManifest, + }, + }, + } + + for i, test := range testCases { + _, helper := newHelper(t) + storage := NewREST(helper) + + obj, err := storage.Create(kapi.NewDefaultContext(), test.image) + if obj == nil { + t.Errorf("%d: Expected nil obj, got %v", i, obj) + continue + } + if err != nil { + t.Errorf("%d: Unexpected non-nil error: %#v", i, err) + continue + } + image, ok := obj.(*api.Image) + if !ok { + t.Errorf("%d: Expected image type, got: %#v", i, obj) + continue + } + if test.expect != nil && !test.expect(image) { + t.Errorf("%d: Unexpected image: %#v", i, obj) + } + } +} + +func TestUpdateResetsMetadata(t *testing.T) { + testCases := []struct { + image *api.Image + existing *api.Image + expect func(*api.Image) bool + }{ + // manifest changes are ignored + { + expect: func(image *api.Image) bool { + if image.Labels["a"] != "b" { + t.Errorf("unexpected labels: %s", image.Labels) + return false + } + if image.DockerImageManifest != "" { + t.Errorf("unexpected manifest: %s", image.DockerImageManifest) + return false + } + if image.DockerImageMetadata.ID != "foo" { + t.Errorf("unexpected docker image: %#v", image.DockerImageMetadata) + return false + } + if image.DockerImageReference != "openshift/ruby-19-centos-2" { + t.Errorf("image reference changed: %s", image.DockerImageReference) + return false + } + if image.DockerImageMetadata.Size != 0 { + t.Errorf("image had size %d", image.DockerImageMetadata.Size) + return false + } + if len(image.DockerImageLayers) != 1 && image.DockerImageLayers[0].Size != 10 { + t.Errorf("unexpected layers: %#v", image.DockerImageLayers) + return false + } + return true + }, + existing: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1"}, + DockerImageReference: "openshift/ruby-19-centos-2", + DockerImageLayers: []api.ImageLayer{{Name: "test", Size: 10}}, + DockerImageMetadata: api.DockerImage{ID: "foo"}, + }, + image: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1", Labels: map[string]string{"a": "b"}}, + DockerImageReference: "openshift/ruby-19-centos", + DockerImageManifest: etcdManifest, + }, + }, + // existing manifest is preserved, and unpacked + { + expect: func(image *api.Image) bool { + if image.DockerImageManifest != etcdManifest { + t.Errorf("unexpected manifest: %s", image.DockerImageManifest) + return false + } + if image.DockerImageMetadata.ID != "fe50ac14986497fa6b5d2cc24feb4a561d01767bc64413752c0988cb70b0b8b9" { + t.Errorf("unexpected docker image: %#v", image.DockerImageMetadata) + return false + } + if image.DockerImageReference != "openshift/ruby-19-centos-2" { + t.Errorf("image reference changed: %s", image.DockerImageReference) + return false + } + if image.DockerImageMetadata.Size != 28643712 { + t.Errorf("image had size %d", image.DockerImageMetadata.Size) + return false + } + if len(image.DockerImageLayers) != 4 || image.DockerImageLayers[0].Name != "sha256:744b46d0ac8636c45870a03830d8d82c20b75fbfb9bc937d5e61005d23ad4cfe" || image.DockerImageLayers[0].Size != 15141568 { + t.Errorf("unexpected layers: %#v", image.DockerImageLayers) + return false + } + return true + }, + existing: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1"}, + DockerImageReference: "openshift/ruby-19-centos-2", + DockerImageLayers: []api.ImageLayer{}, + DockerImageManifest: etcdManifest, + }, + image: &api.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1"}, + DockerImageReference: "openshift/ruby-19-centos", + DockerImageMetadata: api.DockerImage{ID: "foo"}, + }, + }, + } + + for i, test := range testCases { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + + fakeEtcdClient.Data[etcdtest.AddPrefix("/images/foo")] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, test.existing), + CreatedIndex: 1, + ModifiedIndex: 1, + }, + }, + } + + obj, _, err := storage.Update(kapi.NewDefaultContext(), test.image) + if err != nil { + t.Errorf("%d: Unexpected non-nil error: %#v", i, err) + continue + } + if obj == nil { + t.Errorf("%d: Expected nil obj, got %v", i, obj) + continue + } + image, ok := obj.(*api.Image) + if !ok { + t.Errorf("%d: Expected image type, got: %#v", i, obj) + continue + } + if test.expect != nil && !test.expect(image) { + t.Errorf("%d: Unexpected image: %#v", i, obj) + } + } +} + func TestGetOK(t *testing.T) { fakeEtcdClient, helper := newHelper(t) expectedImage := &api.Image{ diff --git a/pkg/image/registry/image/strategy.go b/pkg/image/registry/image/strategy.go index a61ea76c9dd5..a872ce82960d 100644 --- a/pkg/image/registry/image/strategy.go +++ b/pkg/image/registry/image/strategy.go @@ -8,6 +8,7 @@ import ( "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/registry/generic" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/fielderrors" errs "k8s.io/kubernetes/pkg/util/fielderrors" @@ -31,7 +32,13 @@ func (imageStrategy) NamespaceScoped() bool { } // PrepareForCreate clears fields that are not allowed to be set by end users on creation. +// It extracts the latest information from the manifest (if available) and sets that onto the object. func (imageStrategy) PrepareForCreate(obj runtime.Object) { + newImage := obj.(*api.Image) + // ignore errors, change in place + if err := api.ImageWithMetadata(newImage); err != nil { + util.HandleError(fmt.Errorf("Unable to update image metadata for %q: %v", newImage.Name, err)) + } } // Validate validates a new image. @@ -50,13 +57,21 @@ func (imageStrategy) AllowUnconditionalUpdate() bool { } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. +// It extracts the latest info from the manifest and sets that on the object. func (imageStrategy) PrepareForUpdate(obj, old runtime.Object) { newImage := obj.(*api.Image) oldImage := old.(*api.Image) + // image metadata cannot be altered + newImage.DockerImageReference = oldImage.DockerImageReference newImage.DockerImageMetadata = oldImage.DockerImageMetadata newImage.DockerImageManifest = oldImage.DockerImageManifest newImage.DockerImageMetadataVersion = oldImage.DockerImageMetadataVersion + newImage.DockerImageLayers = oldImage.DockerImageLayers + + if err := api.ImageWithMetadata(newImage); err != nil { + util.HandleError(fmt.Errorf("Unable to update image metadata for %q: %v", newImage.Name, err)) + } } // ValidateUpdate is the default update validation for an end user. diff --git a/pkg/image/registry/imagesecret/rest.go b/pkg/image/registry/imagesecret/rest.go new file mode 100644 index 000000000000..e6f258c0b35e --- /dev/null +++ b/pkg/image/registry/imagesecret/rest.go @@ -0,0 +1,58 @@ +package imagesecret + +import ( + "fmt" + + kapi "k8s.io/kubernetes/pkg/api" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + + "github.com/openshift/origin/pkg/image/api" +) + +// REST implements the RESTStorage interface for ImageStreamImport +type REST struct { + secrets client.SecretsNamespacer +} + +// NewREST returns a new REST. +func NewREST(secrets client.SecretsNamespacer) *REST { + return &REST{secrets: secrets} +} + +func (r *REST) New() runtime.Object { + return &kapi.SecretList{} +} + +func (r *REST) NewGetOptions() (runtime.Object, bool, string) { + return &kapi.ListOptions{}, false, "" +} + +// Get retrieves all pull type secrets in the current namespace. Name is currently ignored and +// reserved for future use. +func (r *REST) Get(ctx kapi.Context, _ string, options runtime.Object) (runtime.Object, error) { + listOptions, ok := options.(*kapi.ListOptions) + if !ok { + return nil, fmt.Errorf("unexpected options: %v", listOptions) + } + ns, ok := kapi.NamespaceFrom(ctx) + if !ok { + ns = kapi.NamespaceAll + } + secrets, err := r.secrets.Secrets(ns).List(listOptions.LabelSelector, listOptions.FieldSelector) + if err != nil { + return nil, err + } + filtered := make([]kapi.Secret, 0, len(secrets.Items)) + for i := range secrets.Items { + if secrets.Items[i].Annotations[api.ExcludeImageSecretAnnotation] == "true" { + continue + } + switch secrets.Items[i].Type { + case kapi.SecretTypeDockercfg, kapi.SecretTypeDockerConfigJson: + filtered = append(filtered, secrets.Items[i]) + } + } + secrets.Items = filtered + return secrets, nil +} diff --git a/pkg/image/registry/imagesecret/rest_test.go b/pkg/image/registry/imagesecret/rest_test.go new file mode 100644 index 000000000000..fe2cfe69bc1c --- /dev/null +++ b/pkg/image/registry/imagesecret/rest_test.go @@ -0,0 +1,50 @@ +package imagesecret + +import ( + "testing" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + + "github.com/openshift/origin/pkg/image/api" +) + +func TestGetSecrets(t *testing.T) { + fake := testclient.NewSimpleFake(&kapi.SecretList{ + Items: []kapi.Secret{ + { + ObjectMeta: kapi.ObjectMeta{Name: "secret-1"}, + Type: kapi.SecretTypeDockercfg, + }, + { + ObjectMeta: kapi.ObjectMeta{Name: "secret-2", Annotations: map[string]string{api.ExcludeImageSecretAnnotation: "true"}}, + Type: kapi.SecretTypeDockercfg, + }, + { + ObjectMeta: kapi.ObjectMeta{Name: "secret-3"}, + Type: kapi.SecretTypeOpaque, + }, + { + ObjectMeta: kapi.ObjectMeta{Name: "secret-4"}, + Type: kapi.SecretTypeServiceAccountToken, + }, + { + ObjectMeta: kapi.ObjectMeta{Name: "secret-5"}, + Type: kapi.SecretTypeDockerConfigJson, + }, + }, + }) + rest := NewREST(fake) + opts, _, _ := rest.NewGetOptions() + obj, err := rest.Get(kapi.NewDefaultContext(), "", opts) + if err != nil { + t.Fatal(err) + } + list := obj.(*kapi.SecretList) + if len(list.Items) != 2 { + t.Fatal(list) + } + if list.Items[0].Name != "secret-1" || list.Items[1].Name != "secret-5" { + t.Fatal(list) + } +} diff --git a/pkg/image/registry/imagestream/etcd/etcd.go b/pkg/image/registry/imagestream/etcd/etcd.go index 5d789c84128f..9a8bf66ec17f 100644 --- a/pkg/image/registry/imagestream/etcd/etcd.go +++ b/pkg/image/registry/imagestream/etcd/etcd.go @@ -48,7 +48,9 @@ func NewREST(s storage.Interface, defaultRegistry imagestream.DefaultRegistry, s statusStore.UpdateStrategy = imagestream.NewStatusStrategy(strategy) internalStore := store - internalStore.UpdateStrategy = imagestream.NewInternalStrategy(strategy) + internalStrategy := imagestream.NewInternalStrategy(strategy) + internalStore.CreateStrategy = internalStrategy + internalStore.UpdateStrategy = internalStrategy store.CreateStrategy = strategy store.UpdateStrategy = strategy @@ -122,6 +124,11 @@ func (r *InternalREST) New() runtime.Object { return &api.ImageStream{} } +// Create alters both the spec and status of the object. +func (r *InternalREST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { + return r.store.Create(ctx, obj) +} + // Update alters both the spec and status of the object. func (r *InternalREST) Update(ctx kapi.Context, obj runtime.Object) (runtime.Object, bool, error) { return r.store.Update(ctx, obj) diff --git a/pkg/image/registry/imagestream/strategy.go b/pkg/image/registry/imagestream/strategy.go index 3b62ed70b067..81ff7f64200f 100644 --- a/pkg/image/registry/imagestream/strategy.go +++ b/pkg/image/registry/imagestream/strategy.go @@ -60,6 +60,11 @@ func (s Strategy) PrepareForCreate(obj runtime.Object) { DockerImageRepository: s.dockerImageRepository(stream), Tags: make(map[string]api.TagEventList), } + stream.Generation = 1 + for tag, ref := range stream.Spec.Tags { + ref.Generation = &stream.Generation + stream.Spec.Tags[tag] = ref + } } // Validate validates a new image stream. @@ -152,6 +157,11 @@ func (s Strategy) tagsChanged(old, stream *api.ImageStream) fielderrors.Validati continue } + glog.V(5).Infof("Detected changed tag %s in %s/%s", tag, stream.Namespace, stream.Name) + + generation := stream.Generation + tagRef.Generation = &generation + if tagRef.From.Kind == "DockerImage" && len(tagRef.From.Name) > 0 { if tagRef.Reference { event, err := tagReferenceToTagEvent(stream, tagRef, "") @@ -159,6 +169,7 @@ func (s Strategy) tagsChanged(old, stream *api.ImageStream) fielderrors.Validati errs = append(errs, fielderrors.NewFieldInvalid(fmt.Sprintf("spec.tags[%s].from", tag), tagRef.From, err.Error())) continue } + stream.Spec.Tags[tag] = tagRef api.AddTagEventToImageStream(stream, tag, *event) } continue @@ -200,12 +211,11 @@ func (s Strategy) tagsChanged(old, stream *api.ImageStream) fielderrors.Validati continue } + stream.Spec.Tags[tag] = tagRef api.AddTagEventToImageStream(stream, tag, *event) } - if old != nil { - api.UpdateChangedTrackingTags(stream, old) - } + api.UpdateChangedTrackingTags(stream, old) // use a consistent timestamp on creation if old == nil && !stream.CreationTimestamp.IsZero() { @@ -221,22 +231,34 @@ func (s Strategy) tagsChanged(old, stream *api.ImageStream) fielderrors.Validati } func tagReferenceToTagEvent(stream *api.ImageStream, tagRef api.TagReference, tagOrID string) (*api.TagEvent, error) { + var ( + event *api.TagEvent + err error + ) switch tagRef.From.Kind { case "DockerImage": - return &api.TagEvent{ + event = &api.TagEvent{ Created: unversioned.Now(), DockerImageReference: tagRef.From.Name, - }, nil + } case "ImageStreamImage": - return api.ResolveImageID(stream, tagOrID) + event, err = api.ResolveImageID(stream, tagOrID) case "ImageStreamTag": - return api.LatestTaggedImage(stream, tagOrID), nil + event, err = api.LatestTaggedImage(stream, tagOrID), nil default: - return nil, fmt.Errorf("invalid from.kind %q: it must be DockerImage, ImageStreamImage or ImageStreamTag", tagRef.From.Kind) + err = fmt.Errorf("invalid from.kind %q: it must be DockerImage, ImageStreamImage or ImageStreamTag", tagRef.From.Kind) } + if err != nil { + return nil, err + } + if event != nil && tagRef.Generation != nil { + event.Generation = *tagRef.Generation + } + return event, nil } +// tagRefChanged returns true if the tag ref changed between two spec updates. func tagRefChanged(old, next api.TagReference, streamNamespace string) bool { if next.From == nil { // both fields in next are empty @@ -264,7 +286,94 @@ func tagRefChanged(old, next api.TagReference, streamNamespace string) bool { if oldFrom.Name != next.From.Name { return true } - return false + return tagRefGenerationChanged(old, next) +} + +// tagRefGenerationChanged returns true if and only the values were set and the new generation +// is at zero. +func tagRefGenerationChanged(old, next api.TagReference) bool { + switch { + case old.Generation != nil && next.Generation != nil: + if *old.Generation == *next.Generation { + return false + } + if *next.Generation == 0 { + return true + } + return false + default: + return false + } +} + +func tagEventChanged(old, next api.TagEvent) bool { + return old.Image != next.Image || old.DockerImageReference != next.DockerImageReference || old.Generation > next.Generation +} + +// updateSpecTagGenerationsForUpdate ensures that new spec updates always have a generation set, and that the value +// cannot be updated by an end user (except by setting generation 0). +func updateSpecTagGenerationsForUpdate(stream, oldStream *api.ImageStream) { + for tag, ref := range stream.Spec.Tags { + if ref.Generation != nil && *ref.Generation == 0 { + continue + } + if oldRef, ok := oldStream.Spec.Tags[tag]; ok { + ref.Generation = oldRef.Generation + stream.Spec.Tags[tag] = ref + } + } +} + +// ensureSpecTagGenerationsAreSet ensures that all spec tags have a generation set to either 0 or the +// current stream value. +func ensureSpecTagGenerationsAreSet(stream, oldStream *api.ImageStream) { + oldTags := map[string]api.TagReference{} + if oldStream != nil && oldStream.Spec.Tags != nil { + oldTags = oldStream.Spec.Tags + } + + // set the generation for any spec tags that have changed, are nil, or are zero + for tag, ref := range stream.Spec.Tags { + if oldRef, ok := oldTags[tag]; !ok || tagRefChanged(oldRef, ref, stream.Namespace) { + ref.Generation = nil + } + + if ref.Generation != nil && *ref.Generation != 0 { + continue + } + ref.Generation = &stream.Generation + stream.Spec.Tags[tag] = ref + } +} + +// updateObservedGenerationForStatusUpdate ensures every status item has a generation set. +func updateObservedGenerationForStatusUpdate(stream, oldStream *api.ImageStream) { + for tag, newer := range stream.Status.Tags { + if len(newer.Items) == 0 || newer.Items[0].Generation != 0 { + // generation is set, continue + continue + } + + older := oldStream.Status.Tags[tag] + if len(older.Items) == 0 || !tagEventChanged(older.Items[0], newer.Items[0]) { + // if the tag wasn't changed by the status update + newer.Items[0].Generation = stream.Generation + stream.Status.Tags[tag] = newer + continue + } + + spec, ok := stream.Spec.Tags[tag] + if !ok || spec.Generation == nil { + // if the spec tag has no generation + newer.Items[0].Generation = stream.Generation + stream.Status.Tags[tag] = newer + continue + } + + // set the status tag from the spec tag generation + newer.Items[0].Generation = *spec.Generation + stream.Status.Tags[tag] = newer + } } type TagVerifier struct { @@ -317,12 +426,33 @@ func (v *TagVerifier) Verify(old, stream *api.ImageStream, user user.Info) field return errors } -func (s Strategy) PrepareForUpdate(obj, old runtime.Object) { +func (s Strategy) prepareForUpdate(obj, old runtime.Object, resetStatus bool) { oldStream := old.(*api.ImageStream) stream := obj.(*api.ImageStream) - stream.Status = oldStream.Status + stream.Generation = oldStream.Generation + if resetStatus { + stream.Status = oldStream.Status + } stream.Status.DockerImageRepository = s.dockerImageRepository(stream) + + // ensure that users cannot change spec tag generation to any value except 0 + updateSpecTagGenerationsForUpdate(stream, oldStream) + + // Any changes to the spec increment the generation number. + // + // TODO: Any changes to a part of the object that represents desired state (labels, + // annotations etc) should also increment the generation. + if !kapi.Semantic.DeepEqual(oldStream.Spec, stream.Spec) || stream.Generation == 0 { + stream.Generation = oldStream.Generation + 1 + } + + // default spec tag generations afterwards (to avoid updating the generation for legacy objects) + ensureSpecTagGenerationsAreSet(stream, oldStream) +} + +func (s Strategy) PrepareForUpdate(obj, old runtime.Object) { + s.prepareForUpdate(obj, old, true) } // ValidateUpdate is the default update validation for an end user. @@ -361,10 +491,13 @@ func NewStatusStrategy(strategy Strategy) StatusStrategy { } func (StatusStrategy) PrepareForUpdate(obj, old runtime.Object) { -} + oldStream := old.(*api.ImageStream) + stream := obj.(*api.ImageStream) -func (StatusStrategy) AllowUnconditionalUpdate() bool { - return false + stream.Spec.Tags = oldStream.Spec.Tags + stream.Spec.DockerImageRepository = oldStream.Spec.DockerImageRepository + + updateObservedGenerationForStatusUpdate(stream, oldStream) } func (StatusStrategy) ValidateUpdate(ctx kapi.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { @@ -409,13 +542,17 @@ func NewInternalStrategy(strategy Strategy) InternalStrategy { return InternalStrategy{strategy} } -func (InternalStrategy) PrepareForUpdate(obj, old runtime.Object) { -} +func (s InternalStrategy) PrepareForCreate(obj runtime.Object) { + stream := obj.(*api.ImageStream) -func (InternalStrategy) AllowUnconditionalUpdate() bool { - return false + stream.Status.DockerImageRepository = s.dockerImageRepository(stream) + stream.Generation = 1 + for tag, ref := range stream.Spec.Tags { + ref.Generation = &stream.Generation + stream.Spec.Tags[tag] = ref + } } -func (InternalStrategy) ValidateUpdate(ctx kapi.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { - return validation.ValidateImageStreamUpdate(obj.(*api.ImageStream), old.(*api.ImageStream)) +func (s InternalStrategy) PrepareForUpdate(obj, old runtime.Object) { + s.prepareForUpdate(obj, old, false) } diff --git a/pkg/image/registry/imagestreamimage/rest.go b/pkg/image/registry/imagestreamimage/rest.go index 030a37fc5067..8b96c6e9e8bb 100644 --- a/pkg/image/registry/imagestreamimage/rest.go +++ b/pkg/image/registry/imagestreamimage/rest.go @@ -78,10 +78,10 @@ func (r *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { if err != nil { return nil, err } - imageWithMetadata, err := api.ImageWithMetadata(*image) - if err != nil { + if err := api.ImageWithMetadata(image); err != nil { return nil, err } + image.DockerImageManifest = "" if d, err := digest.ParseDigest(imageName); err == nil { imageName = d.Hex() @@ -95,7 +95,7 @@ func (r *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { Namespace: kapi.NamespaceValue(ctx), Name: fmt.Sprintf("%s@%s", name, imageName), }, - Image: *imageWithMetadata, + Image: *image, } return &isi, nil diff --git a/pkg/image/registry/imagestreamimport/rest.go b/pkg/image/registry/imagestreamimport/rest.go new file mode 100644 index 000000000000..f1ab8f487f01 --- /dev/null +++ b/pkg/image/registry/imagestreamimport/rest.go @@ -0,0 +1,338 @@ +package imagestreamimport + +import ( + "fmt" + "net/http" + "time" + + "github.com/golang/glog" + gocontext "golang.org/x/net/context" + + kapi "k8s.io/kubernetes/pkg/api" + kapierrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/api/unversioned" + kclient "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util" + + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/dockerregistry" + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/importer" + "github.com/openshift/origin/pkg/image/registry/imagestream" +) + +// ImporterFunc returns an instance of the importer that should be used per invocation. +type ImporterFunc func(r importer.RepositoryRetriever) importer.Interface + +// ImporterDockerRegistryFunc returns an instance of a docker client that should be used per invocation of import, +// may be nil if no legacy import capability is required. +type ImporterDockerRegistryFunc func() dockerregistry.Client + +// REST implements the RESTStorage interface for ImageStreamImport +type REST struct { + importFn ImporterFunc + streams imagestream.Registry + internalStreams rest.CreaterUpdater + images rest.Creater + secrets client.ImageStreamSecretsNamespacer + transport http.RoundTripper + clientFn ImporterDockerRegistryFunc +} + +// NewREST returns a REST storage implementation that handles importing images. The clientFn argument is optional +// if v1 Docker Registry importing is not required +func NewREST(importFn ImporterFunc, streams imagestream.Registry, internalStreams rest.CreaterUpdater, + images rest.Creater, secrets client.ImageStreamSecretsNamespacer, clientFn ImporterDockerRegistryFunc) *REST { + rt, err := kclient.TransportFor(&kclient.Config{}) + // TODO: will be refactored upstream, or take this as input? + if err != nil { + panic(err) + } + return &REST{ + importFn: importFn, + streams: streams, + internalStreams: internalStreams, + images: images, + secrets: secrets, + transport: rt, + clientFn: clientFn, + } +} + +// New is only implemented to make REST implement RESTStorage +func (r *REST) New() runtime.Object { + return &api.ImageStreamImport{} +} + +func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { + isi, ok := obj.(*api.ImageStreamImport) + if !ok { + return nil, kapierrors.NewBadRequest(fmt.Sprintf("obj is not an ImageStreamImport: %#v", obj)) + } + + inputMeta := isi.ObjectMeta + + if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil { + return nil, err + } + + namespace, ok := kapi.NamespaceFrom(ctx) + if !ok { + return nil, kapierrors.NewBadRequest("a namespace must be specified to import images") + } + + secrets, err := r.secrets.ImageStreamSecrets(namespace).Secrets(isi.Name, kapi.ListOptions{}) + if err != nil { + util.HandleError(fmt.Errorf("unable to load secrets for namespace %q: %v", namespace, err)) + secrets = &kapi.SecretList{} + } + + if r.clientFn != nil { + if client := r.clientFn(); client != nil { + ctx = kapi.WithValue(ctx, importer.ContextKeyV1RegistryClient, client) + } + } + credentials := importer.NewCredentialsForSecrets(secrets.Items) + importCtx := importer.NewContext(r.transport).WithCredentials(credentials) + + imports := r.importFn(importCtx) + if err := imports.Import(ctx.(gocontext.Context), isi); err != nil { + return nil, kapierrors.NewInternalError(err) + } + + // TODO: perform the transformation of the image stream and return it with the ISI if import is false + // so that clients can see what the resulting object would look like. + if !isi.Spec.Import { + clearManifests(isi) + return isi, nil + } + + create := false + stream, err := r.streams.GetImageStream(ctx, isi.Name) + if err != nil { + if !kapierrors.IsNotFound(err) { + return nil, err + } + // consistency check, stream must exist + if len(inputMeta.ResourceVersion) > 0 || len(inputMeta.UID) > 0 { + return nil, err + } + create = true + stream = &api.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Name: isi.Name, + Namespace: namespace, + Generation: 0, + }, + } + } else { + if len(inputMeta.ResourceVersion) > 0 && inputMeta.ResourceVersion != stream.ResourceVersion { + glog.V(4).Infof("DEBUG: mismatch between requested UID %s and located UID %s", inputMeta.UID, stream.UID) + return nil, kapierrors.NewConflict("imageStream", inputMeta.Name, fmt.Errorf("the image stream was updated from %q to %q", inputMeta.ResourceVersion, stream.ResourceVersion)) + } + if len(inputMeta.UID) > 0 && inputMeta.UID != stream.UID { + glog.V(4).Infof("DEBUG: mismatch between requested UID %s and located UID %s", inputMeta.UID, stream.UID) + return nil, kapierrors.NewNotFound("imageStream", inputMeta.Name) + } + } + + if stream.Annotations == nil { + stream.Annotations = make(map[string]string) + } + now := unversioned.Now() + stream.Annotations[api.DockerImageRepositoryCheckAnnotation] = now.UTC().Format(time.RFC3339) + gen := stream.Generation + 1 + zero := int64(0) + + importedImages := make(map[string]error) + updatedImages := make(map[string]*api.Image) + if spec := isi.Spec.Repository; spec != nil { + for i, imageStatus := range isi.Status.Repository.Images { + image := imageStatus.Image + if image == nil { + continue + } + + // update the spec tag + ref, err := api.ParseDockerImageReference(image.DockerImageReference) + if err != nil { + // ??? + continue + } + tag := ref.Tag + if len(imageStatus.Tag) > 0 { + tag = imageStatus.Tag + } + if _, ok := stream.Spec.Tags[tag]; !ok { + if stream.Spec.Tags == nil { + stream.Spec.Tags = make(map[string]api.TagReference) + } + stream.Spec.Tags[tag] = api.TagReference{ + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: image.DockerImageReference, + }, + Generation: &gen, + ImportPolicy: api.TagImportPolicy{Insecure: spec.ImportPolicy.Insecure}, + } + } + + // import or reuse the image + importErr, imported := importedImages[image.Name] + if importErr != nil { + api.SetTagConditions(stream, tag, newImportFailedCondition(err, gen, now)) + } + + pullSpec, _ := api.MostAccuratePullSpec(image.DockerImageReference, image.Name, "") + api.AddTagEventToImageStream(stream, tag, api.TagEvent{ + Created: now, + DockerImageReference: pullSpec, + Image: image.Name, + Generation: gen, + }) + + if imported { + if updatedImage, ok := updatedImages[image.Name]; ok { + isi.Status.Repository.Images[i].Image = updatedImage + } + continue + } + + // establish the image into the store + updated, err := r.images.Create(ctx, image) + switch { + case kapierrors.IsAlreadyExists(err): + if err := api.ImageWithMetadata(image); err != nil { + glog.V(4).Infof("Unable to update image metadata during image import when image already exists %q: err", image.Name, err) + } + updated = image + fallthrough + case err == nil: + updatedImage := updated.(*api.Image) + updatedImages[image.Name] = updatedImage + isi.Status.Repository.Images[i].Image = updatedImage + importedImages[image.Name] = nil + default: + importedImages[image.Name] = err + } + } + } + + for i, spec := range isi.Spec.Images { + if spec.To == nil { + continue + } + tag := spec.To.Name + + if stream.Spec.Tags == nil { + stream.Spec.Tags = make(map[string]api.TagReference) + } + specTag := stream.Spec.Tags[tag] + from := spec.From + specTag.From = &from + specTag.Generation = &zero + specTag.ImportPolicy.Insecure = spec.ImportPolicy.Insecure + stream.Spec.Tags[tag] = specTag + + status := isi.Status.Images[i] + if status.Image == nil || status.Status.Status == unversioned.StatusFailure { + message := status.Status.Message + if len(message) == 0 { + message = "unknown error prevented import" + } + api.SetTagConditions(stream, tag, api.TagEventCondition{ + Type: api.ImportSuccess, + Status: kapi.ConditionFalse, + Message: message, + Reason: string(status.Status.Reason), + Generation: gen, + + LastTransitionTime: now, + }) + continue + } + image := status.Image + + importErr, imported := importedImages[image.Name] + if importErr != nil { + api.SetTagConditions(stream, tag, newImportFailedCondition(err, gen, now)) + } + + pullSpec, _ := api.MostAccuratePullSpec(image.DockerImageReference, image.Name, "") + api.AddTagEventToImageStream(stream, tag, api.TagEvent{ + Created: now, + DockerImageReference: pullSpec, + Image: image.Name, + Generation: gen, + }) + + if imported { + continue + } + _, err = r.images.Create(ctx, image) + if kapierrors.IsAlreadyExists(err) { + err = nil + } + importedImages[image.Name] = err + } + + // TODO: should we allow partial failure? + for _, err := range importedImages { + if err != nil { + return nil, err + } + } + + clearManifests(isi) + + if create { + obj, err = r.internalStreams.Create(ctx, stream) + } else { + obj, _, err = r.internalStreams.Update(ctx, stream) + } + if err != nil { + return nil, err + } + isi.Status.Import = obj.(*api.ImageStream) + return isi, nil +} + +// clearManifests unsets the manifest for each object that does not request it +func clearManifests(isi *api.ImageStreamImport) { + for i := range isi.Status.Images { + if !isi.Spec.Images[i].IncludeManifest { + if isi.Status.Images[i].Image != nil { + isi.Status.Images[i].Image.DockerImageManifest = "" + } + } + } + if isi.Spec.Repository != nil && !isi.Spec.Repository.IncludeManifest { + for i := range isi.Status.Repository.Images { + if isi.Status.Repository.Images[i].Image != nil { + isi.Status.Repository.Images[i].Image.DockerImageManifest = "" + } + } + } +} + +func newImportFailedCondition(err error, gen int64, now unversioned.Time) api.TagEventCondition { + c := api.TagEventCondition{ + Type: api.ImportSuccess, + Status: kapi.ConditionFalse, + Message: err.Error(), + Generation: gen, + + LastTransitionTime: now, + } + if status, ok := err.(kclient.APIStatus); ok { + s := status.Status() + c.Reason, c.Message = string(s.Reason), s.Message + } + return c +} + +func invalidStatus(kind, position string, errs ...error) unversioned.Status { + return kapierrors.NewInvalid(kind, position, errs).(kclient.APIStatus).Status() +} diff --git a/pkg/image/registry/imagestreamimport/strategy.go b/pkg/image/registry/imagestreamimport/strategy.go new file mode 100644 index 000000000000..93cafa125870 --- /dev/null +++ b/pkg/image/registry/imagestreamimport/strategy.go @@ -0,0 +1,35 @@ +package imagestreamimport + +import ( + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/fielderrors" + + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/api/validation" +) + +// strategy implements behavior for ImageStreamImports. +type strategy struct { + runtime.ObjectTyper +} + +var Strategy = &strategy{kapi.Scheme} + +func (s *strategy) NamespaceScoped() bool { + return true +} + +func (s *strategy) GenerateName(string) string { + return "" +} + +func (s *strategy) PrepareForCreate(obj runtime.Object) { + newIST := obj.(*api.ImageStreamImport) + newIST.Status = api.ImageStreamImportStatus{} +} + +func (s *strategy) Validate(ctx kapi.Context, obj runtime.Object) fielderrors.ValidationErrorList { + isi := obj.(*api.ImageStreamImport) + return validation.ValidateImageStreamImport(isi) +} diff --git a/pkg/image/registry/imagestreammapping/rest.go b/pkg/image/registry/imagestreammapping/rest.go index 62c3306e3a8a..e7853a63deb3 100644 --- a/pkg/image/registry/imagestreammapping/rest.go +++ b/pkg/image/registry/imagestreammapping/rest.go @@ -101,6 +101,9 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err err = wait.ExponentialBackoff(wait.Backoff{Steps: maxRetriesOnConflict}, func() (bool, error) { lastEvent := api.LatestTaggedImage(stream, tag) + + next.Generation = stream.Generation + if !api.AddTagEventToImageStream(stream, tag, next) { // nothing actually changed return true, nil @@ -119,12 +122,25 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err if findLatestErr != nil { return false, findLatestErr } + + // no previous tag + if lastEvent == nil { + // The tag hasn't changed, so try again with the updated stream. + stream = latestStream + return false, nil + } + + // check for tag change newerEvent := api.LatestTaggedImage(latestStream, tag) - if lastEvent == nil || kapi.Semantic.DeepEqual(lastEvent, newerEvent) { + // generation and creation time differences are ignored + lastEvent.Generation = newerEvent.Generation + lastEvent.Created = newerEvent.Created + if kapi.Semantic.DeepEqual(lastEvent, newerEvent) { // The tag hasn't changed, so try again with the updated stream. stream = latestStream return false, nil } + // The tag changed, so return the conflict error back to the client. return false, err }) diff --git a/pkg/image/registry/imagestreamtag/rest.go b/pkg/image/registry/imagestreamtag/rest.go index 42e87d7b051e..ca5799984789 100644 --- a/pkg/image/registry/imagestreamtag/rest.go +++ b/pkg/image/registry/imagestreamtag/rest.go @@ -232,11 +232,11 @@ func newISTag(tag string, imageStream *api.ImageStream, image *api.Image) (*api. } if image != nil { - imageWithMetadata, err := api.ImageWithMetadata(*image) - if err != nil { + if err := api.ImageWithMetadata(image); err != nil { return nil, err } - ist.Image = *imageWithMetadata + image.DockerImageManifest = "" + ist.Image = *image } else { ist.Image = api.Image{} ist.Image.Name = event.Image diff --git a/pkg/image/registry/imagestreamtag/rest_test.go b/pkg/image/registry/imagestreamtag/rest_test.go index c511992fd47b..e7c7d0987c6e 100644 --- a/pkg/image/registry/imagestreamtag/rest_test.go +++ b/pkg/image/registry/imagestreamtag/rest_test.go @@ -363,8 +363,9 @@ func TestDeleteImageStreamTag(t *testing.T) { "happy path": { repo: &api.ImageStream{ ObjectMeta: kapi.ObjectMeta{ - Namespace: "default", - Name: "test", + Namespace: "default", + Name: "test", + Generation: 2, }, Spec: api.ImageStreamSpec{ Tags: map[string]api.TagReference{ @@ -390,6 +391,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -398,6 +400,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -406,6 +409,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -414,6 +418,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -458,19 +463,21 @@ func TestDeleteImageStreamTag(t *testing.T) { } expectedStatus := &unversioned.Status{Status: unversioned.StatusSuccess} if e, a := expectedStatus, obj; !reflect.DeepEqual(e, a) { - t.Errorf("%s: expected %#v, got %#v", name, e, a) + t.Errorf("%s:\nexpect=%#v\nactual=%#v", name, e, a) } updatedRepo := &api.ImageStream{} if err := helper.Get(kapi.NewDefaultContext(), "/imagestreams/default/test", updatedRepo, false); err != nil { t.Fatalf("%s: error retrieving updated repo: %s", name, err) } + three := int64(3) expectedStreamSpec := map[string]api.TagReference{ "another": { From: &kapi.ObjectReference{ Kind: "ImageStreamTag", Name: "test:foo", }, + Generation: &three, }, } expectedStreamStatus := map[string]api.TagEventList{ @@ -479,6 +486,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -487,6 +495,7 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, @@ -495,16 +504,20 @@ func TestDeleteImageStreamTag(t *testing.T) { { DockerImageReference: "registry.default.local/default/test@sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", Image: "sha256:381151ac5b7f775e8371e489f3479b84a4c004c90ceddb2ad80b6877215a892f", + Generation: 2, }, }, }, } + if updatedRepo.Generation != 3 { + t.Errorf("%s: unexpected generation: %d", name, updatedRepo.Generation) + } if e, a := expectedStreamStatus, updatedRepo.Status.Tags; !reflect.DeepEqual(e, a) { - t.Errorf("%s: stream status: expected\n%v\ngot\n%v\n", name, e, a) + t.Errorf("%s: stream spec:\nexpect=%#v\nactual=%#v", name, e, a) } if e, a := expectedStreamSpec, updatedRepo.Spec.Tags; !reflect.DeepEqual(e, a) { - t.Errorf("%s: stream spec: expected\n%v\ngot\n%v\n", name, e, a) + t.Errorf("%s: stream spec:\nexpect=%#v\nactual=%#v", name, e, a) } } diff --git a/pkg/image/registry/imagestreamtag/strategy.go b/pkg/image/registry/imagestreamtag/strategy.go index 927b7d204394..177db8ee253c 100644 --- a/pkg/image/registry/imagestreamtag/strategy.go +++ b/pkg/image/registry/imagestreamtag/strategy.go @@ -49,6 +49,7 @@ func (s *strategy) PrepareForUpdate(obj, old runtime.Object) { oldIST := old.(*api.ImageStreamTag) newIST.SelfLink = oldIST.SelfLink + newIST.Image = oldIST.Image } func (s *strategy) ValidateUpdate(ctx kapi.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { diff --git a/test/cmd/builds.sh b/test/cmd/builds.sh index b3bc35fd6965..1db58c650a01 100755 --- a/test/cmd/builds.sh +++ b/test/cmd/builds.sh @@ -56,7 +56,8 @@ os::cmd::expect_success 'oc delete is/origin' os::cmd::expect_success "oc new-build -D \$'FROM openshift/origin:v1.1\nENV ok=1' --to origin-name-test --name origin-test2" os::cmd::expect_success_and_text "oc get bc/origin-test2 --template '${template}'" '^ImageStreamTag origin-name-test:latest$' -os::cmd::expect_failure_and_text 'oc new-build centos/ruby-22-centos7~https://github.com/openshift/ruby-ex centos/php-56-centos7~https://github.com/openshift/cakephp-ex --to invalid/argument' 'error: only one component or source repository can be used when specifying an output image reference' +os::cmd::try_until_text 'oc get is ruby-22-centos7' 'latest' +os::cmd::expect_failure_and_text 'oc new-build ruby-22-centos7~https://github.com/openshift/ruby-ex ruby-22-centos7~https://github.com/openshift/ruby-ex --to invalid/argument' 'error: only one component or source repository can be used when specifying an output image reference' os::cmd::expect_success 'oc delete all --all' @@ -72,7 +73,7 @@ os::cmd::expect_success 'oc get buildConfigs' os::cmd::expect_success 'oc get bc' os::cmd::expect_success 'oc get builds' -# make sure the imagestream has the latest tag before starting a build or the build will immediately fail. +# make sure the imagestream has the latest tag before trying to test it or start a build with it os::cmd::try_until_text 'oc get is ruby-22-centos7' 'latest' REAL_OUTPUT_TO=$(oc get bc/ruby-sample-build --template='{{ .spec.output.to.name }}') diff --git a/test/cmd/images.sh b/test/cmd/images.sh index eb3618053f51..c0d884a92b55 100755 --- a/test/cmd/images.sh +++ b/test/cmd/images.sh @@ -78,9 +78,23 @@ os::cmd::expect_success_and_text "oc describe ${imagename}" 'Image Created:' os::cmd::expect_success_and_text "oc describe ${imagename}" 'Image Name:' echo "imageStreams: ok" -os::cmd::expect_failure 'oc import-image mysql --from=mysql' -os::cmd::expect_success_and_text 'oc import-image mysql --from=mysql --confirm' 'sha256:' +# should follow the latest reference to 5.6 and update that, and leave latest unchanged +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 1).from.kind}}'" 'DockerImage' +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 2).from.kind}}'" 'ImageStreamTag' +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 2).from.name}}'" '5.6' +os::cmd::expect_success_and_text 'oc import-image mysql' 'sha256:' +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 1).from.kind}}'" 'DockerImage' +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 2).from.kind}}'" 'ImageStreamTag' +os::cmd::expect_success_and_text "oc get is/mysql --template='{{(index .spec.tags 2).from.name}}'" '5.6' +# should prevent changing source +os::cmd::expect_failure_and_text 'oc import-image mysql --from=mysql' "use the 'tag' command if you want to change the source" os::cmd::expect_success 'oc describe is/mysql' +os::cmd::expect_success_and_text 'oc import-image mysql:5.6' "sha256:" +os::cmd::expect_success_and_text 'oc import-image mysql:latest' "sha256:" +os::cmd::expect_success 'oc delete is/mysql' +os::cmd::expect_failure_and_text 'oc import-image mysql --from=mysql --all' '\-\-confirm' +os::cmd::expect_success_and_text 'oc import-image mysql --from=mysql --all --confirm' 'sha256:' +name=$(oc get istag/mysql:latest --template='{{ .image.metadata.name }}') echo "import-image: ok" # oc tag @@ -114,6 +128,7 @@ os::cmd::expect_success_and_text "oc get is/tagtest2 --template='{{(index .spec. os::cmd::expect_success 'oc tag registry-1.docker.io/openshift/origin:v1.0.4 newrepo:latest' os::cmd::expect_success_and_text "oc get is/newrepo --template='{{(index .spec.tags 0).from.kind}}'" 'DockerImage' +os::cmd::try_until_success 'oc get istag/mysql:5.5' os::cmd::expect_success 'oc tag mysql:5.5 newrepo:latest' os::cmd::expect_success_and_text "oc get is/newrepo --template='{{(index .spec.tags 0).from.kind}}'" 'ImageStreamImage' os::cmd::expect_success 'oc tag mysql:5.5 newrepo:latest --alias' diff --git a/test/fixtures/bootstrappolicy/bootstrap_cluster_roles.yaml b/test/fixtures/bootstrappolicy/bootstrap_cluster_roles.yaml index c85eb1fd9c20..1a83a9143a3e 100644 --- a/test/fixtures/bootstrappolicy/bootstrap_cluster_roles.yaml +++ b/test/fixtures/bootstrappolicy/bootstrap_cluster_roles.yaml @@ -57,6 +57,7 @@ items: - identities - images - imagestreamimages + - imagestreamimports - imagestreammappings - imagestreams - imagestreams/status @@ -165,6 +166,7 @@ items: - endpoints - generatedeploymentconfigs - imagestreamimages + - imagestreamimports - imagestreammappings - imagestreams - imagestreamtags @@ -280,6 +282,7 @@ items: - endpoints - generatedeploymentconfigs - imagestreamimages + - imagestreamimports - imagestreammappings - imagestreams - imagestreamtags @@ -386,6 +389,7 @@ items: - events - generatedeploymentconfigs - imagestreamimages + - imagestreamimports - imagestreammappings - imagestreams - imagestreams/status diff --git a/test/fixtures/bootstrappolicy/bootstrap_openshift_roles.yaml b/test/fixtures/bootstrappolicy/bootstrap_openshift_roles.yaml index 07645cfcf3aa..0fbfd6bf282f 100644 --- a/test/fixtures/bootstrappolicy/bootstrap_openshift_roles.yaml +++ b/test/fixtures/bootstrappolicy/bootstrap_openshift_roles.yaml @@ -11,6 +11,7 @@ items: attributeRestrictions: null resources: - imagestreamimages + - imagestreamimports - imagestreammappings - imagestreams - imagestreamtags diff --git a/test/fixtures/image-secrets.json b/test/fixtures/image-secrets.json new file mode 100644 index 000000000000..937c8daf88b8 --- /dev/null +++ b/test/fixtures/image-secrets.json @@ -0,0 +1,68 @@ +{ + "kind": "SecretList", + "apiVersion": "v1", + "metadata": { + "selfLink": "/oapi/v1/namespaces/default/imagesecrets", + "resourceVersion": "11909" + }, + "items": [ + { + "metadata": { + "name": "builder-dockercfg-hnq87", + "namespace": "default", + "selfLink": "/oapi/v1/namespaces/default/imagesecrets/builder-dockercfg-hnq87", + "uid": "3ba3eb6f-a1e7-11e5-9d93-080027c5bfa9", + "resourceVersion": "224", + "creationTimestamp": "2015-12-13T22:17:01Z", + "annotations": { + "kubernetes.io/service-account.name": "builder", + "kubernetes.io/service-account.uid": "3b6872be-a1e7-11e5-9d93-080027c5bfa9", + "openshift.io/token-secret.name": "builder-token-xvsbw" + } + }, + "data": { + ".dockercfg": "eyIxNzIuMzAuMjEzLjExMjo1MDAwIjp7InVzZXJuYW1lIjoic2VydmljZWFjY291bnQiLCJwYXNzd29yZCI6ImV5SmhiR2NpT2lKU1V6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpwYzNNaU9pSnJkV0psY201bGRHVnpMM05sY25acFkyVmhZMk52ZFc1MElpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl1WVcxbGMzQmhZMlVpT2lKa1pXWmhkV3gwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXpaV055WlhRdWJtRnRaU0k2SW1KMWFXeGtaWEl0ZEc5clpXNHRlSFp6WW5jaUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNXVZVzFsSWpvaVluVnBiR1JsY2lJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZ5ZG1salpTMWhZMk52ZFc1MExuVnBaQ0k2SWpOaU5qZzNNbUpsTFdFeFpUY3RNVEZsTlMwNVpEa3pMVEE0TURBeU4yTTFZbVpoT1NJc0luTjFZaUk2SW5ONWMzUmxiVHB6WlhKMmFXTmxZV05qYjNWdWREcGtaV1poZFd4ME9tSjFhV3hrWlhJaWZRLmpMT1BITUl3alI2Rzl6cm1NWDduQ0JFYzJwNXZyalNvemVIMzJRVnFUbkZEcjZseHo4NFU5UzNYdmVWNTA0cEhVSDhTN3NlbFFfMW04amRORm91VnBBODd4OFdiU2pnWEFRX0hzazdMYlhiaVMxcTdFcEszVV9hclVLNVpLLWZFZDB4Z2t0dnN4U2JXT0dKVUpBMGM0ckhCazRFa09CRzVsMTlucWhjSHcxdV9kNUw5MzFvYWNTejJOZzVjSVhKZENlb1phTHJpU0NYa3dINWlLM2NJY0pJNWl6MU5wQ2hCWDBWNDgtc25BZVpXVjM2SWZQYmtSekxlUmZwVHhnV1ZBN0pRN0RuV3YzdXdPUV9nN01hNE9fUjJpaThZOXEyQmFDMzlwbVJlUXpZemZDSzZZYjYtWnRsR0FaSkNnUFZELVZpaU9kRVh1QjkxSGNfMGdoSTVjZyIsImVtYWlsIjoic2VydmljZWFjY291bnRAZXhhbXBsZS5vcmciLCJhdXRoIjoiYzJWeWRtbGpaV0ZqWTI5MWJuUTZaWGxLYUdKSFkybFBhVXBUVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U25Cak0wMXBUMmxLY21SWFNteGpiVFZzWkVkV2Vrd3pUbXhqYmxwd1dUSldhRmt5VG5aa1Z6VXdTV2wzYVdFelZtbGFXRXAxV2xoU2JHTjVOWEJpZVRsNldsaEtNbUZYVG14WlYwNXFZak5XZFdSRE9YVlpWekZzWXpOQ2FGa3lWV2xQYVVwcldsZGFhR1JYZURCSmFYZHBZVE5XYVZwWVNuVmFXRkpzWTNrMWNHSjVPWHBhV0VveVlWZE9iRmxYVG1waU0xWjFaRU01ZWxwWFRubGFXRkYxWW0xR2RGcFRTVFpKYlVveFlWZDRhMXBZU1hSa1J6bHlXbGMwZEdWSVducFpibU5wVEVOS2NtUlhTbXhqYlRWc1pFZFdla3h0YkhaTU0wNXNZMjVhY0ZreVZtaFpNazUyWkZjMU1Fd3pUbXhqYmxwd1dUSlZkRmxYVG1waU0xWjFaRU0xZFZsWE1XeEphbTlwV1c1V2NHSkhVbXhqYVVselNXMTBNVmx0Vm5saWJWWXdXbGhOZFdGWE9IWmpNbFo1Wkcxc2FscFhSbXBaTWpreFltNVJkbU15Vm5sa2JXeHFXbE14YUZreVRuWmtWelV3VEc1V2NGcERTVFpKYWs1cFRtcG5NMDF0U214TVYwVjRXbFJqZEUxVVJteE9VekExV2tScmVreFVRVFJOUkVGNVRqSk5NVmx0V21oUFUwbHpTVzVPTVZscFNUWkpiazQxWXpOU2JHSlVjSHBhV0VveVlWZE9iRmxYVG1waU0xWjFaRVJ3YTFwWFdtaGtWM2d3VDIxS01XRlhlR3RhV0VscFpsRXVha3hQVUVoTlNYZHFValpIT1hweWJVMVlOMjVEUWtWak1uQTFkbkpxVTI5NlpVZ3pNbEZXY1ZSdVJrUnlObXg0ZWpnMFZUbFRNMWgyWlZZMU1EUndTRlZJT0ZNM2MyVnNVVjh4YlRocVpFNUdiM1ZXY0VFNE4zZzRWMkpUYW1kWVFWRmZTSE5yTjB4aVdHSnBVekZ4TjBWd1N6TlZYMkZ5VlVzMVdrc3Raa1ZrTUhobmEzUjJjM2hUWWxkUFIwcFZTa0V3WXpSeVNFSnJORVZyVDBKSE5Xd3hPVzV4YUdOSWR6RjFYMlExVERrek1XOWhZMU42TWs1bk5XTkpXRXBrUTJWdldtRk1jbWxUUTFocmQwZzFhVXN6WTBsalNrazFhWG94VG5CRGFFSllNRlkwT0MxemJrRmxXbGRXTXpaSlpsQmlhMUo2VEdWU1puQlVlR2RYVmtFM1NsRTNSRzVYZGpOMWQwOVJYMmMzVFdFMFQxOVNNbWxwT0ZrNWNUSkNZVU16T1hCdFVtVlJlbGw2WmtOTE5sbGlOaTFhZEd4SFFWcEtRMmRRVmtRdFZtbHBUMlJGV0hWQ09URklZMTh3WjJoSk5XTm4ifX0=" + }, + "type": "kubernetes.io/dockercfg" + }, + { + "metadata": { + "name": "default-dockercfg-x4mac", + "namespace": "default", + "selfLink": "/oapi/v1/namespaces/default/imagesecrets/default-dockercfg-x4mac", + "uid": "3bb3373e-a1e7-11e5-9d93-080027c5bfa9", + "resourceVersion": "227", + "creationTimestamp": "2015-12-13T22:17:01Z", + "annotations": { + "kubernetes.io/service-account.name": "default", + "kubernetes.io/service-account.uid": "3b5df0de-a1e7-11e5-9d93-080027c5bfa9", + "openshift.io/token-secret.name": "default-token-6jl9m" + } + }, + "data": { + ".dockercfg": "eyIxNzIuMzAuMjEzLjExMjo1MDAwIjp7InVzZXJuYW1lIjoic2VydmljZWFjY291bnQiLCJwYXNzd29yZCI6ImV5SmhiR2NpT2lKU1V6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpwYzNNaU9pSnJkV0psY201bGRHVnpMM05sY25acFkyVmhZMk52ZFc1MElpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl1WVcxbGMzQmhZMlVpT2lKa1pXWmhkV3gwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXpaV055WlhRdWJtRnRaU0k2SW1SbFptRjFiSFF0ZEc5clpXNHRObXBzT1cwaUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNXVZVzFsSWpvaVpHVm1ZWFZzZENJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZ5ZG1salpTMWhZMk52ZFc1MExuVnBaQ0k2SWpOaU5XUm1NR1JsTFdFeFpUY3RNVEZsTlMwNVpEa3pMVEE0TURBeU4yTTFZbVpoT1NJc0luTjFZaUk2SW5ONWMzUmxiVHB6WlhKMmFXTmxZV05qYjNWdWREcGtaV1poZFd4ME9tUmxabUYxYkhRaWZRLkw1SGMwZUhqWkhvNUJHZUFJX1NlSE1TcFl5NFdLNF9iU3NibS00VUdCb3l0aTdXT2hUbEpjQWdVRlRnRUd1MW14RFJtTnRFQS14WGR2MGpYZTM3N1E1MkM3M09saTBadUFuTGdrQmJMM3duSWtXS1VPWnZyYmNEdzVoSmFlVGRoVXZyaTVfWmtDNGtiTlh3SktwQUloOE1vbk9mVWpubVk3aFFiSVNNTGlyaElqX29yQUtNcWw5blFiUVRPZk80Z29BcXNjTk1SSHNKcVlUQ25lTUJ1V2JPMmFwWlg1dC0tSlR5Y2dzeGRNZWptczRYQ2JTZzBtX2pqbVd5Qkp0TTNCSTNfazBrVTRtRER2M3JZX1hIZmxjb0ducEZPVnMzQmpoQ0pPWVIwaDdCTmtlNElZcnRhN1hHYzg4T2FzYklaMW0tVWJNUWFQdmZlaHQwdDlJZ2tvUSIsImVtYWlsIjoic2VydmljZWFjY291bnRAZXhhbXBsZS5vcmciLCJhdXRoIjoiYzJWeWRtbGpaV0ZqWTI5MWJuUTZaWGxLYUdKSFkybFBhVXBUVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U25Cak0wMXBUMmxLY21SWFNteGpiVFZzWkVkV2Vrd3pUbXhqYmxwd1dUSldhRmt5VG5aa1Z6VXdTV2wzYVdFelZtbGFXRXAxV2xoU2JHTjVOWEJpZVRsNldsaEtNbUZYVG14WlYwNXFZak5XZFdSRE9YVlpWekZzWXpOQ2FGa3lWV2xQYVVwcldsZGFhR1JYZURCSmFYZHBZVE5XYVZwWVNuVmFXRkpzWTNrMWNHSjVPWHBhV0VveVlWZE9iRmxYVG1waU0xWjFaRU01ZWxwWFRubGFXRkYxWW0xR2RGcFRTVFpKYlZKc1dtMUdNV0pJVVhSa1J6bHlXbGMwZEU1dGNITlBWekJwVEVOS2NtUlhTbXhqYlRWc1pFZFdla3h0YkhaTU0wNXNZMjVhY0ZreVZtaFpNazUyWkZjMU1Fd3pUbXhqYmxwd1dUSlZkRmxYVG1waU0xWjFaRU0xZFZsWE1XeEphbTlwV2tkV2JWbFlWbk5rUTBselNXMTBNVmx0Vm5saWJWWXdXbGhOZFdGWE9IWmpNbFo1Wkcxc2FscFhSbXBaTWpreFltNVJkbU15Vm5sa2JXeHFXbE14YUZreVRuWmtWelV3VEc1V2NGcERTVFpKYWs1cFRsZFNiVTFIVW14TVYwVjRXbFJqZEUxVVJteE9VekExV2tScmVreFVRVFJOUkVGNVRqSk5NVmx0V21oUFUwbHpTVzVPTVZscFNUWkpiazQxWXpOU2JHSlVjSHBhV0VveVlWZE9iRmxYVG1waU0xWjFaRVJ3YTFwWFdtaGtWM2d3VDIxU2JGcHRSakZpU0ZGcFpsRXVURFZJWXpCbFNHcGFTRzgxUWtkbFFVbGZVMlZJVFZOd1dYazBWMHMwWDJKVGMySnRMVFJWUjBKdmVYUnBOMWRQYUZSc1NtTkJaMVZHVkdkRlIzVXhiWGhFVW0xT2RFVkJMWGhZWkhZd2FsaGxNemMzVVRVeVF6Y3pUMnhwTUZwMVFXNU1aMnRDWWt3emQyNUphMWRMVlU5YWRuSmlZMFIzTldoS1lXVlVaR2hWZG5KcE5WOWFhME0wYTJKT1dIZEtTM0JCU1dnNFRXOXVUMlpWYW01dFdUZG9VV0pKVTAxTWFYSm9TV3BmYjNKQlMwMXhiRGx1VVdKUlZFOW1UelJuYjBGeGMyTk9UVkpJYzBweFdWUkRibVZOUW5WWFlrOHlZWEJhV0RWMExTMUtWSGxqWjNONFpFMWxhbTF6TkZoRFlsTm5NRzFmYW1wdFYzbENTblJOTTBKSk0xOXJNR3RWTkcxRVJIWXpjbGxmV0VobWJHTnZSMjV3Ums5V2N6TkNhbWhEU2s5WlVqQm9OMEpPYTJVMFNWbHlkR0UzV0Vkak9EaFBZWE5pU1ZveGJTMVZZazFSWVZCMlptVm9kREIwT1VsbmEyOVIifX0=" + }, + "type": "kubernetes.io/dockercfg" + }, + { + "metadata": { + "name": "deployer-dockercfg-xnoyu", + "namespace": "default", + "selfLink": "/oapi/v1/namespaces/default/imagesecrets/deployer-dockercfg-xnoyu", + "uid": "3bc23446-a1e7-11e5-9d93-080027c5bfa9", + "resourceVersion": "229", + "creationTimestamp": "2015-12-13T22:17:01Z", + "annotations": { + "kubernetes.io/service-account.name": "deployer", + "kubernetes.io/service-account.uid": "3b6b9528-a1e7-11e5-9d93-080027c5bfa9", + "openshift.io/token-secret.name": "deployer-token-rmko7" + } + }, + "data": { + ".dockercfg": "eyIxNzIuMzAuMjEzLjExMjo1MDAwIjp7InVzZXJuYW1lIjoic2VydmljZWFjY291bnQiLCJwYXNzd29yZCI6ImV5SmhiR2NpT2lKU1V6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpwYzNNaU9pSnJkV0psY201bGRHVnpMM05sY25acFkyVmhZMk52ZFc1MElpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl1WVcxbGMzQmhZMlVpT2lKa1pXWmhkV3gwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXpaV055WlhRdWJtRnRaU0k2SW1SbGNHeHZlV1Z5TFhSdmEyVnVMWEp0YTI4M0lpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVibUZ0WlNJNkltUmxjR3h2ZVdWeUlpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVkV2xrSWpvaU0ySTJZamsxTWpndFlURmxOeTB4TVdVMUxUbGtPVE10TURnd01ESTNZelZpWm1FNUlpd2ljM1ZpSWpvaWMzbHpkR1Z0T25ObGNuWnBZMlZoWTJOdmRXNTBPbVJsWm1GMWJIUTZaR1Z3Ykc5NVpYSWlmUS5tazcwVUNWMEtWeExVSUlOQjJRM2loUGRFVHJLT01IYkZVRU9qNS1tQXRUT29VZ0MtM1IwWHcxaW5UVXItU0E3VjgyWF9kUmU0NTg4T0xUTDJGUExDMm9fSDQ4RWJOZ2F1d3l5OWVfTGZBUjF6eTlGa2tCblBVemV3a0RRMkVHVUZDMFhEZnotdnVwTHNacXdnVzVIY2ZEQVVmNm9wRlgxV2IyRlBxaGdqVW9UWWZlcm1rX21NUk1fSkJsWXo1ZjdybDd1QXp6c1RYNFA3b0psQmFJc3I5OFo4QmZtNmdKWXRoZU5WYVlLNk1JM1p0SjVpN2FYdFZRbVZLdEVUMUtlSURoaF9LUmE1RkE5RkYzcGFOMlNzUUc5TnVCV3lUWUdwNGVaMWJUYWtjdk9VT01qeHc1R29GVVhXTWZ4UGIxOTJpNVRCNlpReG9HT0dxUl9ERE0ybHciLCJlbWFpbCI6InNlcnZpY2VhY2NvdW50QGV4YW1wbGUub3JnIiwiYXV0aCI6ImMyVnlkbWxqWldGalkyOTFiblE2WlhsS2FHSkhZMmxQYVVwVFZYcEpNVTVwU1hOSmJsSTFZME5KTmtscmNGaFdRMG81TG1WNVNuQmpNMDFwVDJsS2NtUlhTbXhqYlRWc1pFZFdla3d6VG14amJscHdXVEpXYUZreVRuWmtWelV3U1dsM2FXRXpWbWxhV0VwMVdsaFNiR041TlhCaWVUbDZXbGhLTW1GWFRteFpWMDVxWWpOV2RXUkRPWFZaVnpGc1l6TkNhRmt5VldsUGFVcHJXbGRhYUdSWGVEQkphWGRwWVROV2FWcFlTblZhV0ZKc1kzazFjR0o1T1hwYVdFb3lZVmRPYkZsWFRtcGlNMVoxWkVNNWVscFhUbmxhV0ZGMVltMUdkRnBUU1RaSmJWSnNZMGQ0ZG1WWFZubE1XRkoyWVRKV2RVeFlTblJoTWpnelNXbDNhV0V6Vm1sYVdFcDFXbGhTYkdONU5YQmllVGw2V2xoS01tRlhUbXhaVjA1cVlqTldkV1JET1hwYVdFb3lZVmRPYkV4WFJtcFpNamt4WW01UmRXSnRSblJhVTBrMlNXMVNiR05IZUhabFYxWjVTV2wzYVdFelZtbGFXRXAxV2xoU2JHTjVOWEJpZVRsNldsaEtNbUZYVG14WlYwNXFZak5XZFdSRE9YcGFXRW95WVZkT2JFeFhSbXBaTWpreFltNVJkV1JYYkd0SmFtOXBUVEpKTWxscWF6Rk5hbWQwV1ZSR2JFNTVNSGhOVjFVeFRGUnNhMDlVVFhSTlJHZDNUVVJKTTFsNlZtbGFiVVUxU1dsM2FXTXpWbWxKYW05cFl6TnNlbVJIVm5SUGJrNXNZMjVhY0ZreVZtaFpNazUyWkZjMU1FOXRVbXhhYlVZeFlraFJObHBIVm5kaVJ6azFXbGhKYVdaUkxtMXJOekJWUTFZd1MxWjRURlZKU1U1Q01sRXphV2hRWkVWVWNrdFBUVWhpUmxWRlQybzFMVzFCZEZSUGIxVm5ReTB6VWpCWWR6RnBibFJWY2kxVFFUZFdPREpZWDJSU1pUUTFPRGhQVEZSTU1rWlFURU15YjE5SU5EaEZZazVuWVhWM2VYazVaVjlNWmtGU01YcDVPVVpyYTBKdVVGVjZaWGRyUkZFeVJVZFZSa013V0VSbWVpMTJkWEJNYzFweGQyZFhOVWhqWmtSQlZXWTJiM0JHV0RGWFlqSkdVSEZvWjJwVmIxUlpabVZ5Yld0ZmJVMVNUVjlLUW14WmVqVm1OM0pzTjNWQmVucHpWRmcwVURkdlNteENZVWx6Y2prNFdqaENabTAyWjBwWmRHaGxUbFpoV1VzMlRVa3pXblJLTldrM1lWaDBWbEZ0Vmt0MFJWUXhTMlZKUkdob1gwdFNZVFZHUVRsR1JqTndZVTR5VTNOUlJ6bE9kVUpYZVZSWlIzQTBaVm94WWxSaGEyTjJUMVZQVFdwNGR6VkhiMFpWV0ZkTlpuaFFZakU1TW1rMVZFSTJXbEY0YjBkUFIzRlNYMFJFVFRKc2R3PT0ifX0=" + }, + "type": "kubernetes.io/dockercfg" + } + ] +} + diff --git a/test/integration/dockerregistryclient_test.go b/test/integration/dockerregistryclient_test.go index a48924596c58..73d0abc5aa21 100644 --- a/test/integration/dockerregistryclient_test.go +++ b/test/integration/dockerregistryclient_test.go @@ -83,7 +83,7 @@ func retryWhenUnreachable(t *testing.T, f func() error, errorPatterns ...string) } func TestRegistryClientConnect(t *testing.T) { - c := dockerregistry.NewClient(10 * time.Second) + c := dockerregistry.NewClient(10*time.Second, true) conn, err := c.Connect("docker.io", false) if err != nil { t.Fatal(err) @@ -110,7 +110,7 @@ func TestRegistryClientConnect(t *testing.T) { } func TestRegistryClientConnectPulpRegistry(t *testing.T) { - c := dockerregistry.NewClient(10 * time.Second) + c := dockerregistry.NewClient(10*time.Second, true) conn, err := c.Connect(pulpRegistryName, false) if err != nil { t.Fatal(err) @@ -133,7 +133,7 @@ func TestRegistryClientConnectPulpRegistry(t *testing.T) { } func TestRegistryClientDockerHubV2(t *testing.T) { - c := dockerregistry.NewClient(10 * time.Second) + c := dockerregistry.NewClient(10*time.Second, true) conn, err := c.Connect(dockerHubV2RegistryName, false) if err != nil { t.Fatal(err) @@ -153,7 +153,7 @@ func TestRegistryClientDockerHubV2(t *testing.T) { } func TestRegistryClientDockerHubV1(t *testing.T) { - c := dockerregistry.NewClient(10 * time.Second) + c := dockerregistry.NewClient(10*time.Second, true) // a v1 only path conn, err := c.Connect(dockerHubV1RegistryName, false) if err != nil { @@ -174,7 +174,7 @@ func TestRegistryClientDockerHubV1(t *testing.T) { } func TestRegistryClientRegistryNotFound(t *testing.T) { - conn, err := dockerregistry.NewClient(10*time.Second).Connect("localhost:65000", false) + conn, err := dockerregistry.NewClient(10*time.Second, true).Connect("localhost:65000", false) if err != nil { t.Fatal(err) } @@ -184,7 +184,7 @@ func TestRegistryClientRegistryNotFound(t *testing.T) { } func doTestRegistryClientImage(t *testing.T, registry, version string) { - conn, err := dockerregistry.NewClient(10*time.Second).Connect(registry, false) + conn, err := dockerregistry.NewClient(10*time.Second, true).Connect(registry, false) if err != nil { t.Fatal(err) } @@ -234,7 +234,7 @@ func TestRegistryClientImageV1(t *testing.T) { } func TestRegistryClientQuayIOImage(t *testing.T) { - conn, err := dockerregistry.NewClient(10*time.Second).Connect("quay.io", false) + conn, err := dockerregistry.NewClient(10*time.Second, true).Connect("quay.io", false) if err != nil { t.Fatal(err) } diff --git a/test/integration/imageimporter_test.go b/test/integration/imageimporter_test.go new file mode 100644 index 000000000000..1325f951e60c --- /dev/null +++ b/test/integration/imageimporter_test.go @@ -0,0 +1,300 @@ +// +build integration,etcd + +package integration + +import ( + "reflect" + "strings" + "testing" + "time" + + gocontext "golang.org/x/net/context" + + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + kclient "k8s.io/kubernetes/pkg/client/unversioned" + + "github.com/openshift/origin/pkg/dockerregistry" + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/importer" + testutil "github.com/openshift/origin/test/util" + testserver "github.com/openshift/origin/test/util/server" + + "github.com/davecgh/go-spew/spew" +) + +func init() { + testutil.RequireEtcd() +} + +func TestImageStreamImport(t *testing.T) { + _, clusterAdminKubeConfig, err := testserver.StartTestMaster() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + c, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + err = testutil.CreateNamespace(clusterAdminKubeConfig, testutil.Namespace()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // can't give invalid image specs, should be invalid + isi, err := c.ImageStreams(testutil.Namespace()).Import(&api.ImageStreamImport{ + ObjectMeta: kapi.ObjectMeta{ + Name: "doesnotexist", + }, + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "a/a/a/a/a/redis:latest"}, To: &kapi.LocalObjectReference{Name: "tag"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "redis:latest"}}, + }, + }, + }) + if err == nil || isi != nil || !errors.IsInvalid(err) { + t.Fatalf("unexpected responses: %#v %#v %#v", err, isi, isi.Status.Import) + } + // does not create stream + if _, err := c.ImageStreams(testutil.Namespace()).Get("doesnotexist"); err == nil || !errors.IsNotFound(err) { + t.Fatal(err) + } + + // import without committing + isi, err = c.ImageStreams(testutil.Namespace()).Import(&api.ImageStreamImport{ + ObjectMeta: kapi.ObjectMeta{ + Name: "doesnotexist", + }, + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "redis:latest"}, To: &kapi.LocalObjectReference{Name: "other"}}, + }, + }, + }) + if err != nil || isi == nil || isi.Status.Import != nil { + t.Fatalf("unexpected responses: %v %#v %#v", err, isi, isi.Status.Import) + } + // does not create stream + if _, err := c.ImageStreams(testutil.Namespace()).Get("doesnotexist"); err == nil || !errors.IsNotFound(err) { + t.Fatal(err) + } + + // import with commit + isi, err = c.ImageStreams(testutil.Namespace()).Import(&api.ImageStreamImport{ + ObjectMeta: kapi.ObjectMeta{ + Name: "doesnotexist", + }, + Spec: api.ImageStreamImportSpec{ + Import: true, + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "redis:latest"}, To: &kapi.LocalObjectReference{Name: "other"}}, + }, + }, + }) + if err != nil || isi == nil || isi.Status.Import == nil { + t.Fatalf("unexpected responses: %v %#v %#v", err, isi, isi.Status.Import) + } + + if isi.Status.Images[0].Image == nil || isi.Status.Images[0].Image.DockerImageMetadata.Size == 0 || len(isi.Status.Images[0].Image.DockerImageLayers) == 0 { + t.Fatalf("unexpected image output: %#v", isi.Status.Images[0].Image) + } + + stream := isi.Status.Import + if _, ok := stream.Annotations[api.DockerImageRepositoryCheckAnnotation]; !ok { + t.Fatalf("unexpected stream: %#v", stream) + } + if stream.Generation != 1 || len(stream.Spec.Tags) != 1 || len(stream.Status.Tags) != 1 { + t.Fatalf("unexpected stream: %#v", stream) + } + for tag, ref := range stream.Spec.Tags { + if ref.Generation == nil || *ref.Generation != stream.Generation || tag != "other" || ref.From == nil || + ref.From.Name != "redis:latest" || ref.From.Kind != "DockerImage" { + t.Fatalf("unexpected stream: %#v", stream) + } + event := stream.Status.Tags[tag] + if len(event.Conditions) > 0 || len(event.Items) != 1 || event.Items[0].Generation != stream.Generation || strings.HasPrefix(event.Items[0].DockerImageReference, "docker.io/library/redis@sha256:") { + t.Fatalf("unexpected stream: %#v", stream) + } + } + + // stream should not have changed + stream2, err := c.ImageStreams(testutil.Namespace()).Get("doesnotexist") + if err != nil { + t.Fatal(err) + } + if stream.Generation != stream2.Generation || !reflect.DeepEqual(stream.Spec, stream2.Spec) || + !reflect.DeepEqual(stream.Status, stream2.Status) || !reflect.DeepEqual(stream.Annotations, stream2.Annotations) { + t.Errorf("streams changed: %#v %#v", stream, stream2) + } +} + +/* +// re-add this test as an integration test +func TestOpenShiftRegistry(t *testing.T) { + token := `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tNmpsOW0iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjNiNWRmMGRlLWExZTctMTFlNS05ZDkzLTA4MDAyN2M1YmZhOSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.L5Hc0eHjZHo5BGeAI_SeHMSpYy4WK4_bSsbm-4UGBoyti7WOhTlJcAgUFTgEGu1mxDRmNtEA-xXdv0jXe377Q52C73Oli0ZuAnLgkBbL3wnIkWKUOZvrbcDw5hJaeTdhUvri5_ZkC4kbNXwJKpAIh8MonOfUjnmY7hQbISMLirhIj_orAKMql9nQbQTOfO4goAqscNMRHsJqYTCneMBuWbO2apZX5t--JTycgsxdMejms4XCbSg0m_jjmWyBJtM3BI3_k0kU4mDDv3rY_XHflcoGnpFOVs3BjhCJOYR0h7BNke4IYrta7XGc88OasbIZ1m-UbMQaPvfeht0t9IgkoQ` + creds := NewBasicCredentials() + creds.Add(&url.URL{}, "anything", token) + rt, _ := client.TransportFor(&client.Config{}) + importCtx := NewContext(rt, kapi.NewContext()).WithCredentials(creds) + + imports := &api.ImageStreamImport{ + Images: []api.ImageImport{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "172.30.213.112:5000/default/redis:test"}, Insecure: true}, + }, + } + NewImageStreamImporter(100, nil).Import(importCtx, imports) + d := imports.Images[0].ImageImportStatus + if d.Image == nil || len(d.Image.DockerImageManifest) > 0 || d.Image.DockerImageReference != "172.30.213.112:5000/default/redis:test" || len(d.Image.DockerImageMetadata.ID) == 0 { + t.Errorf("unexpected object: %#v", d.Image) + } + t.Logf("image: %#v\nstatus: %#v", d.Image, d.Status) +} +*/ + +func TestImportImageDockerHub(t *testing.T) { + rt, _ := kclient.TransportFor(&kclient.Config{}) + importCtx := importer.NewContext(rt).WithCredentials(importer.NoCredentials) + + imports := &api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Repository: &api.RepositoryImportSpec{ + From: kapi.ObjectReference{Kind: "DockerImage", Name: "mongo"}, + }, + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "redis"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "mysql"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "redis:latest"}}, + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "mysql/doesnotexistinanyform"}}, + }, + }, + } + + i := importer.NewImageStreamImporter(importCtx, 3, nil) + if err := i.Import(gocontext.Background(), imports); err != nil { + t.Fatal(err) + } + + if imports.Status.Repository.Status.Status != unversioned.StatusSuccess || len(imports.Status.Repository.Images) != 3 || len(imports.Status.Repository.AdditionalTags) < 1 { + t.Errorf("unexpected repository: %#v", imports.Status.Repository) + } + if len(imports.Status.Images) != 4 { + t.Fatalf("unexpected response: %#v", imports.Status.Images) + } + d := imports.Status.Images[0] + if d.Image == nil || len(d.Image.DockerImageManifest) == 0 || !strings.HasPrefix(d.Image.DockerImageReference, "redis@") || len(d.Image.DockerImageMetadata.ID) == 0 || len(d.Image.DockerImageLayers) == 0 { + t.Errorf("unexpected object: %#v", d.Image) + } + d = imports.Status.Images[1] + if d.Image == nil || len(d.Image.DockerImageManifest) == 0 || !strings.HasPrefix(d.Image.DockerImageReference, "mysql@") || len(d.Image.DockerImageMetadata.ID) == 0 || len(d.Image.DockerImageLayers) == 0 { + t.Errorf("unexpected object: %#v", d.Image) + } + d = imports.Status.Images[2] + if d.Image == nil || len(d.Image.DockerImageManifest) == 0 || !strings.HasPrefix(d.Image.DockerImageReference, "redis@") || len(d.Image.DockerImageMetadata.ID) == 0 || len(d.Image.DockerImageLayers) == 0 { + t.Errorf("unexpected object: %#v", d.Image) + } + d = imports.Status.Images[3] + if d.Image != nil || d.Status.Status != unversioned.StatusFailure || d.Status.Reason != "Unauthorized" { + t.Errorf("unexpected object: %#v", d) + } +} + +func TestImportImageQuayIO(t *testing.T) { + rt, _ := kclient.TransportFor(&kclient.Config{}) + importCtx := importer.NewContext(rt).WithCredentials(importer.NoCredentials) + + imports := &api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "quay.io/coreos/etcd"}}, + }, + }, + } + + i := importer.NewImageStreamImporter(importCtx, 3, nil) + if err := i.Import(gocontext.Background(), imports); err != nil { + t.Fatal(err) + } + + if imports.Status.Repository != nil { + t.Errorf("unexpected repository: %#v", imports.Status.Repository) + } + if len(imports.Status.Images) != 1 { + t.Fatalf("unexpected response: %#v", imports.Status.Images) + } + d := imports.Status.Images[0] + if d.Status.Status != unversioned.StatusSuccess { + if d.Status.Reason == "NotV2Registry" { + t.Skipf("the server did not report as a v2 registry: %#v", d.Status) + } + t.Fatalf("unexpected error: %#v", d.Status) + } + if d.Image == nil || len(d.Image.DockerImageManifest) == 0 || !strings.HasPrefix(d.Image.DockerImageReference, "quay.io/coreos/etcd@") || len(d.Image.DockerImageMetadata.ID) == 0 || len(d.Image.DockerImageLayers) == 0 { + t.Errorf("unexpected object: %#v", d.Image) + s := spew.ConfigState{ + Indent: " ", + // Extra deep spew. + DisableMethods: true, + } + t.Logf("import: %s", s.Sdump(d)) + } +} + +func TestImportImageRedHatRegistry(t *testing.T) { + rt, _ := kclient.TransportFor(&kclient.Config{}) + importCtx := importer.NewContext(rt).WithCredentials(importer.NoCredentials) + + // test without the client on the context + imports := &api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "registry.access.redhat.com/rhel7"}}, + }, + }, + } + + i := importer.NewImageStreamImporter(importCtx, 3, nil) + if err := i.Import(gocontext.Background(), imports); err != nil { + t.Fatal(err) + } + + if imports.Status.Repository != nil { + t.Errorf("unexpected repository: %#v", imports.Status.Repository) + } + if len(imports.Status.Images) != 1 { + t.Fatalf("unexpected response: %#v", imports.Status.Images) + } + d := imports.Status.Images[0] + if d.Image != nil || d.Status.Status != unversioned.StatusFailure || d.Status.Reason != "NotV2Registry" { + t.Errorf("unexpected object: %#v", d.Status) + } + + // test with the client on the context + imports = &api.ImageStreamImport{ + Spec: api.ImageStreamImportSpec{ + Images: []api.ImageImportSpec{ + {From: kapi.ObjectReference{Kind: "DockerImage", Name: "registry.access.redhat.com/rhel7"}}, + }, + }, + } + context := gocontext.WithValue(gocontext.Background(), importer.ContextKeyV1RegistryClient, dockerregistry.NewClient(20*time.Second, false)) + importCtx = importer.NewContext(rt).WithCredentials(importer.NoCredentials) + i = importer.NewImageStreamImporter(importCtx, 3, nil) + if err := i.Import(context, imports); err != nil { + t.Fatal(err) + } + + if imports.Status.Repository != nil { + t.Errorf("unexpected repository: %#v", imports.Status.Repository) + } + if len(imports.Status.Images) != 1 { + t.Fatalf("unexpected response: %#v", imports.Status.Images) + } + d = imports.Status.Images[0] + if d.Image == nil || len(d.Image.DockerImageManifest) != 0 || d.Image.DockerImageReference != "registry.access.redhat.com/rhel7:latest" || len(d.Image.DockerImageMetadata.ID) == 0 || len(d.Image.DockerImageLayers) != 0 { + t.Errorf("unexpected object: %#v", d.Status) + t.Logf("imports: %#v", imports.Status.Images[0].Image) + } +}