Skip to content

Commit 9759545

Browse files
Support --from-image-stream-file on release new for archival purposes
It should be possible to snapshot a release stream with `oc get is ... -o yaml` and then build a release from it later for archival and historical backup. Support providing an image stream file instead of requiring a live lookup. Refactor the reference loading code to allow spec tags with image IDs to not require a successful import recorded in status, and specifically error when we encounter that condition.
1 parent afb8414 commit 9759545

File tree

3 files changed

+170
-63
lines changed
  • contrib/completions
  • pkg/oc/cli/admin/release

3 files changed

+170
-63
lines changed

contrib/completions/bash/oc

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/completions/zsh/oc

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/oc/cli/admin/release/new.go

Lines changed: 164 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,23 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions
6565
6666
OpenShift uses long-running active management processes called "operators" to
6767
keep the cluster running and manage component lifecycle. This command
68-
composes a set of images and operator definitions into a single update payload
69-
that can be used to update a cluster.
68+
composes a set of images with operator definitions into a single update payload
69+
that can be used to install or update a cluster.
7070
7171
Operators are expected to host the config they need to be installed to a cluster
7272
in the '/manifests' directory in their image. This command iterates over a set of
7373
operator images and extracts those manifests into a single, ordered list of
7474
Kubernetes objects that can then be iteratively updated on a cluster by the
7575
cluster version operator when it is time to perform an update. Manifest files are
76-
renamed to '99_<image_name>_<filename>' by default, and an operator author that
76+
renamed to '0000_70_<image_name>_<filename>' by default, and an operator author that
7777
needs to provide a global-ordered file (before or after other operators) should
78-
prepend '0000_' to their filename, which instructs the release builder to not
79-
assign a component prefix. Only images with the label
80-
'release.openshift.io/operator=true' are considered to be included.
78+
prepend '0000_NN_<component>_' to their filename, which instructs the release builder
79+
to not assign a component prefix. Only images in the input that have the image label
80+
'io.openshift.release.operator=true' will have manifests loaded.
81+
82+
If an image is in the input but is not referenced by an operator's image-references
83+
file, the image will not be included in the final release image unless
84+
--include=NAME is provided.
8185
8286
Mappings specified via SRC=DST positional arguments allows overriding particular
8387
operators with a specific image. For example:
@@ -86,13 +90,19 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions
8690
8791
will override the default cluster-version-operator image with one pulled from
8892
registry.example.com.
89-
90-
Experimental: This command is under active development and may change without notice.
9193
`),
9294
Example: templates.Examples(fmt.Sprintf(`
9395
# Create a release from the latest origin images and push to a DockerHub repo
9496
%[1]s new --from-image-stream=origin-v4.0 -n openshift --to-image docker.io/mycompany/myrepo:latest
95-
`, parentName)),
97+
98+
# Create a new release with updated metadata from a previous release
99+
%[1]s new --from-release registry.svc.ci.openshift.org/openshift/origin-release:v4.0 --name 4.0.1 \
100+
--previous 4.0.0 --metadata ... --to-image docker.io/mycompany/myrepo:latest
101+
102+
# Create a new release and override a single image
103+
%[1]s new --from-release registry.svc.ci.openshift.org/openshift/origin-release:v4.0 \
104+
cli=docker.io/mycompany/cli:latest
105+
`, parentName)),
96106
Run: func(cmd *cobra.Command, args []string) {
97107
kcmdutil.CheckErr(o.Complete(f, cmd, args))
98108
kcmdutil.CheckErr(o.Run())
@@ -101,8 +111,9 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions
101111
flags := cmd.Flags()
102112

103113
// image inputs
104-
flags.StringSliceVarP(&o.Filenames, "filename", "f", o.Filenames, "A file defining a mapping of input images to use to build the release")
114+
flags.StringSliceVar(&o.Filenames, "filename", o.Filenames, "A file defining a mapping of input images to use to build the release")
105115
flags.StringVar(&o.FromImageStream, "from-image-stream", o.FromImageStream, "Look at all tags in the provided image stream and build a release payload from them.")
116+
flags.StringVarP(&o.FromImageStreamFile, "from-image-stream-file", "f", o.FromImageStreamFile, "Take the provided image stream on disk and build a release payload from it.")
106117
flags.StringVar(&o.FromDirectory, "from-dir", o.FromDirectory, "Use this directory as the source for the release payload.")
107118
flags.StringVar(&o.FromReleaseImage, "from-release", o.FromReleaseImage, "Use an existing release image as input.")
108119
flags.StringVar(&o.ReferenceMode, "reference-mode", o.ReferenceMode, "By default, the image reference from an image stream points to the public registry for the stream and the image digest. Pass 'source' to build references to the originating image.")
@@ -150,9 +161,10 @@ type NewOptions struct {
150161

151162
FromReleaseImage string
152163

153-
FromImageStream string
154-
Namespace string
155-
ReferenceMode string
164+
FromImageStream string
165+
FromImageStreamFile string
166+
Namespace string
167+
ReferenceMode string
156168

157169
ExtraComponentVersions string
158170
AllowedComponents []string
@@ -232,18 +244,25 @@ type imageData struct {
232244
Directory string
233245
}
234246

235-
func findStatusTagEvent(tags []imageapi.NamedTagEventList, name string) *imageapi.TagEvent {
236-
for _, tag := range tags {
247+
func findStatusTagEvents(tags []imageapi.NamedTagEventList, name string) *imageapi.NamedTagEventList {
248+
for i := range tags {
249+
tag := &tags[i]
237250
if tag.Tag != name {
238251
continue
239252
}
240-
if len(tag.Items) == 0 {
241-
return nil
242-
}
243-
return &tag.Items[0]
253+
return tag
244254
}
245255
return nil
246256
}
257+
258+
func findStatusTagEvent(tags []imageapi.NamedTagEventList, name string) *imageapi.TagEvent {
259+
events := findStatusTagEvents(tags, name)
260+
if events == nil || len(events.Items) == 0 {
261+
return nil
262+
}
263+
return &events.Items[0]
264+
}
265+
247266
func findSpecTag(tags []imageapi.TagReference, name string) *imageapi.TagReference {
248267
for i, tag := range tags {
249268
if tag.Name != name {
@@ -273,12 +292,25 @@ func (o *NewOptions) cleanup() {
273292
func (o *NewOptions) Run() error {
274293
defer o.cleanup()
275294

276-
if len(o.FromImageStream) > 0 && len(o.FromDirectory) > 0 {
277-
return fmt.Errorf("only one of --from-image-stream and --from-dir may be specified")
295+
sources := 0
296+
if len(o.FromImageStream) > 0 {
297+
sources++
298+
}
299+
if len(o.FromImageStreamFile) > 0 {
300+
sources++
301+
}
302+
if len(o.FromReleaseImage) > 0 {
303+
sources++
304+
}
305+
if len(o.FromDirectory) > 0 {
306+
sources++
307+
}
308+
if sources > 1 {
309+
return fmt.Errorf("only one of --from-image-stream, --from-image-stream-file, --from-release, or --from-dir may be specified")
278310
}
279-
if len(o.FromDirectory) == 0 && len(o.FromImageStream) == 0 && len(o.FromReleaseImage) == 0 {
311+
if sources == 0 {
280312
if len(o.Mappings) == 0 {
281-
return fmt.Errorf("must specify image mappings")
313+
return fmt.Errorf("must specify image mappings when no other source is defined")
282314
}
283315
}
284316
if len(o.Mirror) > 0 && o.ReferenceMode != "" && o.ReferenceMode != "public" {
@@ -438,16 +470,37 @@ func (o *NewOptions) Run() error {
438470

439471
fmt.Fprintf(o.ErrOut, "info: Found %d images in release\n", len(is.Spec.Tags))
440472

441-
case len(o.FromImageStream) > 0:
473+
case len(o.FromImageStream) > 0, len(o.FromImageStreamFile) > 0:
442474
is = &imageapi.ImageStream{}
443475
is.Annotations = map[string]string{}
444476
if len(o.FromImageStream) > 0 && len(o.Namespace) > 0 {
445477
is.Annotations[annotationReleaseFromImageStream] = fmt.Sprintf("%s/%s", o.Namespace, o.FromImageStream)
446478
}
447479

448-
inputIS, err := o.ImageClient.ImageV1().ImageStreams(o.Namespace).Get(o.FromImageStream, metav1.GetOptions{})
449-
if err != nil {
450-
return err
480+
var inputIS *imageapi.ImageStream
481+
if len(o.FromImageStreamFile) > 0 {
482+
data, err := ioutil.ReadFile(o.FromImageStreamFile)
483+
if os.IsNotExist(err) {
484+
return err
485+
}
486+
if err != nil {
487+
return fmt.Errorf("unable to read input image stream file: %v", err)
488+
}
489+
is := &imageapi.ImageStream{}
490+
if err := yaml.Unmarshal(data, &is); err != nil {
491+
return fmt.Errorf("unable to load input image stream file: %v", err)
492+
}
493+
if is.Kind != "ImageStream" || is.APIVersion != "image.openshift.io/v1" {
494+
return fmt.Errorf("unrecognized input image stream file, must be an ImageStream in image.openshift.io/v1")
495+
}
496+
inputIS = is
497+
498+
} else {
499+
is, err := o.ImageClient.ImageV1().ImageStreams(o.Namespace).Get(o.FromImageStream, metav1.GetOptions{})
500+
if err != nil {
501+
return err
502+
}
503+
inputIS = is
451504
}
452505

453506
if inputIS.Annotations == nil {
@@ -662,52 +715,101 @@ func resolveImageStreamTagsToReferenceMode(inputIS, is *imageapi.ImageStream, re
662715
if forceExternal && len(external) == 0 {
663716
return fmt.Errorf("only image streams or releases with public image repositories can be the source for releases when using the default --reference-mode")
664717
}
665-
for _, tag := range inputIS.Status.Tags {
666-
if exclude.Has(tag.Tag) {
667-
glog.V(2).Infof("Excluded status tag %s", tag.Tag)
718+
719+
externalFn := func(source, image string) string {
720+
// filter source URLs
721+
if len(source) > 0 && len(internal) > 0 && strings.HasPrefix(source, internal) {
722+
glog.V(2).Infof("Can't use source %s because it points to the internal registry", source)
723+
source = ""
724+
}
725+
// default to the external registry name
726+
if (forceExternal || len(source) == 0) && len(external) > 0 {
727+
return external + "@" + image
728+
}
729+
return source
730+
}
731+
732+
covered := sets.NewString()
733+
for _, ref := range inputIS.Spec.Tags {
734+
if exclude.Has(ref.Name) {
735+
glog.V(2).Infof("Excluded spec tag %s", ref.Name)
668736
continue
669737
}
670-
if len(tag.Items) == 0 {
738+
739+
if ref.From != nil && ref.From.Kind == "DockerImage" {
740+
switch from, err := imagereference.Parse(ref.From.Name); {
741+
case err != nil:
742+
return err
743+
744+
case len(from.ID) > 0:
745+
source := externalFn(ref.From.Name, from.ID)
746+
if len(source) == 0 {
747+
glog.V(2).Infof("Can't use spec tag %q because we cannot locate or calculate a source location", ref.Name)
748+
continue
749+
}
750+
751+
ref := ref.DeepCopy()
752+
ref.From = &corev1.ObjectReference{Kind: "DockerImage", Name: source}
753+
is.Spec.Tags = append(is.Spec.Tags, *ref)
754+
covered.Insert(ref.Name)
755+
756+
case len(from.Tag) > 0:
757+
tag := findStatusTagEvents(inputIS.Status.Tags, ref.Name)
758+
if tag == nil {
759+
continue
760+
}
761+
if len(tag.Items) == 0 {
762+
for _, condition := range tag.Conditions {
763+
if condition.Type == imageapi.ImportSuccess && condition.Status != metav1.StatusSuccess {
764+
return fmt.Errorf("the tag %q in the source input stream has not been imported yet", tag.Tag)
765+
}
766+
}
767+
continue
768+
}
769+
if ref.Generation != nil && *ref.Generation != tag.Items[0].Generation {
770+
return fmt.Errorf("the tag %q in the source input stream has not been imported yet", tag.Tag)
771+
}
772+
if len(tag.Items[0].Image) == 0 {
773+
return fmt.Errorf("the tag %q in the source input stream has no image id", tag.Tag)
774+
}
775+
776+
source := externalFn(tag.Items[0].DockerImageReference, tag.Items[0].Image)
777+
ref := ref.DeepCopy()
778+
ref.From = &corev1.ObjectReference{Kind: "DockerImage", Name: source}
779+
is.Spec.Tags = append(is.Spec.Tags, *ref)
780+
covered.Insert(ref.Name)
781+
}
671782
continue
672783
}
784+
// TODO: support ImageStreamTag and ImageStreamImage
785+
}
673786

674-
// attempt to identify the source image
675-
source := tag.Items[0].DockerImageReference
676-
if len(tag.Items[0].Image) == 0 {
677-
glog.V(2).Infof("Ignored tag %q because it had no image id or reference", tag.Tag)
787+
for _, tag := range inputIS.Status.Tags {
788+
if covered.Has(tag.Tag) {
678789
continue
679790
}
680-
// eliminate status tag references that point to the outside
681-
if len(source) > 0 {
682-
if len(internal) > 0 && strings.HasPrefix(tag.Items[0].DockerImageReference, internal) {
683-
glog.V(2).Infof("Can't use tag %q source %s because it points to the internal registry", tag.Tag, source)
684-
source = ""
685-
}
791+
if exclude.Has(tag.Tag) {
792+
glog.V(2).Infof("Excluded status tag %s", tag.Tag)
793+
continue
686794
}
687-
ref := findSpecTag(inputIS.Spec.Tags, tag.Tag)
688-
if ref == nil {
689-
ref = &imageapi.TagReference{Name: tag.Tag}
690-
} else {
691-
// prevent unimported images from being skipped
692-
if ref.Generation != nil && *ref.Generation != tag.Items[0].Generation {
693-
return fmt.Errorf("the tag %q in the source input stream has not been imported yet", tag.Tag)
694-
}
695-
// use the tag ref as the source
696-
if ref.From != nil && ref.From.Kind == "DockerImage" && !strings.HasPrefix(ref.From.Name, internal) {
697-
if from, err := imagereference.Parse(ref.From.Name); err == nil {
698-
from.Tag = ""
699-
from.ID = tag.Items[0].Image
700-
source = from.Exact()
701-
} else {
702-
glog.V(2).Infof("Can't use tag %q from %s because it isn't a valid image reference", tag.Tag, ref.From.Name)
795+
796+
// error if we haven't imported anything to this tag, or skip otherwise
797+
if len(tag.Items) == 0 {
798+
for _, condition := range tag.Conditions {
799+
if condition.Type == imageapi.ImportSuccess && condition.Status != metav1.StatusSuccess {
800+
return fmt.Errorf("the tag %q in the source input stream has not been imported yet", tag.Tag)
703801
}
704802
}
705-
ref = ref.DeepCopy()
803+
continue
706804
}
707-
// default to the external registry name
708-
if (forceExternal || len(source) == 0) && len(external) > 0 {
709-
source = external + "@" + tag.Items[0].Image
805+
// skip rather than error (user created a reference spec tag, then deleted it)
806+
if len(tag.Items[0].Image) == 0 {
807+
glog.V(2).Infof("the tag %q in the source input stream has no image id", tag.Tag)
808+
continue
710809
}
810+
811+
// attempt to identify the source image
812+
source := externalFn(tag.Items[0].DockerImageReference, tag.Items[0].Image)
711813
if len(source) == 0 {
712814
glog.V(2).Infof("Can't use tag %q because we cannot locate or calculate a source location", tag.Tag)
713815
continue
@@ -720,6 +822,7 @@ func resolveImageStreamTagsToReferenceMode(inputIS, is *imageapi.ImageStream, re
720822
sourceRef.ID = tag.Items[0].Image
721823
source = sourceRef.Exact()
722824

825+
ref := &imageapi.TagReference{Name: tag.Tag}
723826
ref.From = &corev1.ObjectReference{Kind: "DockerImage", Name: source}
724827
is.Spec.Tags = append(is.Spec.Tags, *ref)
725828
}

0 commit comments

Comments
 (0)