Skip to content

Commit 15292e0

Browse files
When no argument passed to oc adm release new, dry run the output
Print an output consistent with oc adm release info --verify if no arguments are specified or if an image is pushed. Other modes bypass the image creation and so do not have this output. Make --dry-run on oc image append calculate the SHA instead of just doing nothing. Clean up some incremental output from append and in general make the entire experience more pleasant.
1 parent fd0fb08 commit 15292e0

2 files changed

Lines changed: 196 additions & 71 deletions

File tree

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

Lines changed: 124 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,18 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions
9696
`),
9797
Example: templates.Examples(fmt.Sprintf(`
9898
# Create a release from the latest origin images and push to a DockerHub repo
99-
%[1]s new --from-image-stream=origin-v4.0 -n openshift --to-image docker.io/mycompany/myrepo:latest
99+
%[1]s new --from-image-stream=4.1 -n origin --to-image docker.io/mycompany/myrepo:latest
100100
101101
# Create a new release with updated metadata from a previous release
102-
%[1]s new --from-release registry.svc.ci.openshift.org/openshift/origin-release:v4.0 --name 4.0.1 \
103-
--previous 4.0.0 --metadata ... --to-image docker.io/mycompany/myrepo:latest
102+
%[1]s new --from-release registry.svc.ci.openshift.org/origin/release:v4.1 --name 4.1.1 \
103+
--previous 4.1.0 --metadata ... --to-image docker.io/mycompany/myrepo:latest
104104
105105
# Create a new release and override a single image
106-
%[1]s new --from-release registry.svc.ci.openshift.org/openshift/origin-release:v4.0 \
107-
cli=docker.io/mycompany/cli:latest
106+
%[1]s new --from-release registry.svc.ci.openshift.org/origin/release:v4.1 \
107+
cli=docker.io/mycompany/cli:latest --to-image docker.io/mycompany/myrepo:latest
108+
109+
# Run a verification pass to ensure the release can be reproduced
110+
%[1]s new --from-release registry.svc.ci.openshift.org/origin/release:v4.1
108111
`, parentName)),
109112
Run: func(cmd *cobra.Command, args []string) {
110113
kcmdutil.CheckErr(o.Complete(f, cmd, args))
@@ -202,6 +205,8 @@ type NewOptions struct {
202205

203206
ImageClient imageclient.Interface
204207

208+
VerifyOutputFn func(dgst digest.Digest) error
209+
205210
cleanupFns []func()
206211
}
207212

@@ -330,52 +335,27 @@ func (o *NewOptions) cleanup() {
330335
func (o *NewOptions) Run() error {
331336
defer o.cleanup()
332337

338+
// check parameters
333339
extraComponentVersions, err := parseComponentVersionsLabel(o.ExtraComponentVersions)
334340
if err != nil {
335341
return fmt.Errorf("--component-versions is invalid: %v", err)
336342
}
337-
338-
now := time.Now().UTC()
339-
name := o.Name
340-
if len(name) == 0 {
341-
name = "0.0.1-" + now.Format("2006-01-02-150405")
342-
}
343-
344-
var cm *CincinnatiMetadata
345-
// TODO: remove this once all code creates semantic versions
346-
if _, err := semver.Parse(name); err == nil {
347-
o.ForceManifest = true
348-
}
349-
if len(o.PreviousVersions) > 0 || len(o.ReleaseMetadata) > 0 || o.ForceManifest {
350-
cm = &CincinnatiMetadata{Kind: "cincinnati-metadata-v0"}
351-
semverName, err := semver.Parse(name)
352-
if err != nil {
353-
return fmt.Errorf("when release metadata is added, the --name must be a semantic version")
343+
if len(o.Name) > 0 {
344+
if _, err := semver.Parse(o.Name); err != nil {
345+
return fmt.Errorf("--name must be a semantic version: %v", err)
354346
}
355-
cm.Version = semverName.String()
356347
}
357348
if len(o.ReleaseMetadata) > 0 {
358-
if err := json.Unmarshal([]byte(o.ReleaseMetadata), &cm.Metadata); err != nil {
349+
if err := json.Unmarshal([]byte(o.ReleaseMetadata), &CincinnatiMetadata{}); err != nil {
359350
return fmt.Errorf("invalid --metadata: %v", err)
360351
}
361352
}
362-
if cm != nil {
363-
for _, previous := range o.PreviousVersions {
364-
if len(previous) == 0 {
365-
continue
366-
}
367-
v, err := semver.Parse(previous)
368-
if err != nil {
369-
return fmt.Errorf("%q is not a valid semantic version: %v", previous, err)
370-
}
371-
cm.Previous = append(cm.Previous, v.String())
372-
}
373-
sort.Strings(cm.Previous)
374-
if cm.Previous == nil {
375-
cm.Previous = []string{}
376-
}
377-
}
378-
klog.V(4).Infof("Release metadata:\n%s", toJSONString(cm))
353+
354+
hasMetadataOverrides := len(o.Name) > 0 &&
355+
len(o.ReleaseMetadata) > 0 &&
356+
len(o.PreviousVersions) > 0 &&
357+
len(o.ToImageBase) > 0 &&
358+
len(o.ExtraComponentVersions) > 0
379359

380360
exclude := sets.NewString()
381361
for _, s := range o.Exclude {
@@ -385,6 +365,7 @@ func (o *NewOptions) Run() error {
385365
metadata := make(map[string]imageData)
386366
var ordered []string
387367
var is *imageapi.ImageStream
368+
now := time.Now().UTC().Truncate(time.Second)
388369

389370
switch {
390371
case len(o.FromReleaseImage) > 0:
@@ -394,6 +375,7 @@ func (o *NewOptions) Run() error {
394375
}
395376

396377
verifier := imagemanifest.NewVerifier()
378+
var releaseDigest digest.Digest
397379
var baseDigest string
398380
var imageReferencesData, releaseMetadata []byte
399381

@@ -409,6 +391,7 @@ func (o *NewOptions) Run() error {
409391
}
410392
extractOpts.ImageMetadataCallback = func(m *extract.Mapping, dgst, contentDigest digest.Digest, config *docker10.DockerImageConfig) {
411393
verifier.Verify(dgst, contentDigest)
394+
releaseDigest = contentDigest
412395
if config.Config != nil {
413396
baseDigest = config.Config.Labels[annotationReleaseBaseImageDigest]
414397
klog.V(4).Infof("Release image was built on top of %s", baseDigest)
@@ -451,16 +434,12 @@ func (o *NewOptions) Run() error {
451434
if err != nil {
452435
return fmt.Errorf("unable to load image-references from release contents: %v", err)
453436
}
454-
is = inputIS.DeepCopy()
455-
if is.Annotations == nil {
456-
is.Annotations = map[string]string{}
437+
var cm CincinnatiMetadata
438+
if err := json.Unmarshal(releaseMetadata, &cm); err != nil {
439+
return fmt.Errorf("unable to load release-metadata from release contents: %v", err)
457440
}
458-
is.Annotations[annotationReleaseFromRelease] = o.FromReleaseImage
459441

460-
if inputIS.Annotations == nil {
461-
inputIS.Annotations = make(map[string]string)
462-
}
463-
inputIS.Annotations[annotationBuildVersions] = extraComponentVersions.String()
442+
is = inputIS.DeepCopy()
464443

465444
for _, tag := range is.Spec.Tags {
466445
ordered = append(ordered, tag.Name)
@@ -489,8 +468,42 @@ func (o *NewOptions) Run() error {
489468
if len(o.Name) == 0 {
490469
o.Name = is.Name
491470
}
471+
if len(o.ReleaseMetadata) == 0 && cm.Metadata != nil {
472+
data, err := json.Marshal(cm.Metadata)
473+
if err != nil {
474+
return fmt.Errorf("unable to marshal release metadata: %v", err)
475+
}
476+
o.ReleaseMetadata = string(data)
477+
}
478+
if o.PreviousVersions == nil {
479+
o.PreviousVersions = cm.Previous
480+
}
492481

493-
fmt.Fprintf(o.ErrOut, "info: Found %d images in release\n", len(is.Spec.Tags))
482+
if hasMetadataOverrides {
483+
if is.Annotations == nil {
484+
is.Annotations = map[string]string{}
485+
}
486+
is.Annotations[annotationReleaseFromRelease] = o.FromReleaseImage
487+
fmt.Fprintf(o.ErrOut, "info: Found %d images in release\n", len(is.Spec.Tags))
488+
489+
} else {
490+
klog.V(2).Infof("No metadata changes, building canonical release")
491+
now = is.CreationTimestamp.Time.UTC()
492+
if o.VerifyOutputFn == nil {
493+
o.VerifyOutputFn = func(actual digest.Digest) error {
494+
// TODO: check contents, digests, image stream, the layers, and the manifest
495+
if actual != releaseDigest {
496+
return fmt.Errorf("the release could not be reproduced from its inputs")
497+
}
498+
return nil
499+
}
500+
}
501+
if len(ref.Tag) > 0 {
502+
fmt.Fprintf(o.ErrOut, "info: Release %s built from %d images\n", releaseDigest, len(is.Spec.Tags))
503+
} else {
504+
fmt.Fprintf(o.ErrOut, "info: Release built from %d images\n", len(is.Spec.Tags))
505+
}
506+
}
494507

495508
case len(o.FromImageStream) > 0, len(o.FromImageStreamFile) > 0:
496509
is = &imageapi.ImageStream{}
@@ -529,7 +542,6 @@ func (o *NewOptions) Run() error {
529542
inputIS.Annotations = make(map[string]string)
530543
}
531544
inputIS.Annotations[annotationBuildVersions] = extraComponentVersions.String()
532-
533545
if err := resolveImageStreamTagsToReferenceMode(inputIS, is, o.ReferenceMode, exclude); err != nil {
534546
return err
535547
}
@@ -584,6 +596,38 @@ func (o *NewOptions) Run() error {
584596
}
585597
}
586598

599+
name := o.Name
600+
if len(name) == 0 {
601+
name = "0.0.1-" + now.Format("2006-01-02-150405")
602+
}
603+
604+
cm := &CincinnatiMetadata{Kind: "cincinnati-metadata-v0"}
605+
semverName, err := semver.Parse(name)
606+
if err != nil {
607+
return fmt.Errorf("--name must be a semantic version")
608+
}
609+
cm.Version = semverName.String()
610+
if len(o.ReleaseMetadata) > 0 {
611+
if err := json.Unmarshal([]byte(o.ReleaseMetadata), &cm.Metadata); err != nil {
612+
return fmt.Errorf("invalid --metadata: %v", err)
613+
}
614+
}
615+
for _, previous := range o.PreviousVersions {
616+
if len(previous) == 0 {
617+
continue
618+
}
619+
v, err := semver.Parse(previous)
620+
if err != nil {
621+
return fmt.Errorf("%q is not a valid semantic version: %v", previous, err)
622+
}
623+
cm.Previous = append(cm.Previous, v.String())
624+
}
625+
sort.Strings(cm.Previous)
626+
if cm.Previous == nil {
627+
cm.Previous = []string{}
628+
}
629+
klog.V(4).Infof("Release metadata:\n%s", toJSONString(cm))
630+
587631
if is == nil {
588632
is = &imageapi.ImageStream{
589633
ObjectMeta: metav1.ObjectMeta{},
@@ -723,8 +767,6 @@ func (o *NewOptions) Run() error {
723767
case operators == nil:
724768
case len(operators) == 0:
725769
fmt.Fprintf(o.ErrOut, "warning: No operator metadata was found, no operators will be part of the release.\n")
726-
default:
727-
fmt.Fprintf(o.ErrOut, "info: Built release image from %d operators\n", len(operators))
728770
}
729771

730772
return nil
@@ -988,7 +1030,7 @@ func (o *NewOptions) extractManifests(is *imageapi.ImageStream, name string, met
9881030
if err != nil {
9891031
return err
9901032
}
991-
if err := ioutil.WriteFile(filepath.Join(dir, "image-references"), data, 0640); err != nil {
1033+
if err := ioutil.WriteFile(filepath.Join(dir, "image-references"), data, 0644); err != nil {
9921034
return err
9931035
}
9941036
}
@@ -1036,6 +1078,7 @@ func (o *NewOptions) mirrorImages(is *imageapi.ImageStream) error {
10361078
}
10371079

10381080
func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time) error {
1081+
var exitErr error
10391082
switch {
10401083
case len(o.ToDir) > 0:
10411084
klog.V(4).Infof("Writing release contents to directory %s", o.ToDir)
@@ -1062,7 +1105,8 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
10621105
if strings.Count(name, "/") > 0 || name == "." || name == ".." || len(name) == 0 {
10631106
continue
10641107
}
1065-
f, err := os.OpenFile(filepath.Join(o.ToDir, name), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
1108+
itemPath := filepath.Join(o.ToDir, name)
1109+
f, err := os.OpenFile(itemPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
10661110
if err != nil {
10671111
return err
10681112
}
@@ -1073,6 +1117,9 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
10731117
if err := f.Close(); err != nil {
10741118
return err
10751119
}
1120+
if err := os.Chtimes(itemPath, hdr.ModTime, hdr.ModTime); err != nil {
1121+
klog.V(2).Infof("Unable to update extracted file time: %v", err)
1122+
}
10761123
}
10771124
case len(o.ToFile) > 0:
10781125
klog.V(4).Infof("Writing release contents to file %s", o.ToFile)
@@ -1098,7 +1145,11 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
10981145
klog.V(2).Infof("Unable to set timestamps on output file: %v", err)
10991146
}
11001147
}
1101-
case len(o.ToImage) > 0:
1148+
default:
1149+
if len(o.ToImage) == 0 {
1150+
o.DryRun = true
1151+
o.ToImage = "release:latest"
1152+
}
11021153
klog.V(4).Infof("Writing release contents to image %s", o.ToImage)
11031154
toRef, err := imagereference.Parse(o.ToImage)
11041155
if err != nil {
@@ -1123,7 +1174,7 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
11231174
}
11241175

11251176
verifier := imagemanifest.NewVerifier()
1126-
options := imageappend.NewAppendImageOptions(genericclioptions.IOStreams{Out: o.Out, ErrOut: o.ErrOut})
1177+
options := imageappend.NewAppendImageOptions(genericclioptions.IOStreams{Out: ioutil.Discard, ErrOut: o.ErrOut})
11271178
options.SecurityOptions = o.SecurityOptions
11281179
options.DryRun = o.DryRun
11291180
options.From = toImageBase
@@ -1145,7 +1196,7 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
11451196
// explicitly set release info
11461197
config.Config.Labels["io.openshift.release"] = is.Name
11471198
config.History = []docker10.DockerConfigHistory{
1148-
{Comment: "Release image for OpenShift", Created: is.CreationTimestamp.Time},
1199+
{Comment: "Release image for OpenShift", Created: now},
11491200
}
11501201
if len(dgst) > 0 {
11511202
config.Config.Labels[annotationReleaseBaseImageDigest] = dgst.String()
@@ -1165,8 +1216,19 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
11651216
}
11661217
fmt.Fprintf(o.ErrOut, "warning: %v\n", err)
11671218
}
1219+
if !o.DryRun {
1220+
fmt.Fprintf(o.ErrOut, "info: Pushed to %s\n", o.ToImage)
1221+
}
1222+
1223+
if o.VerifyOutputFn != nil {
1224+
if err := o.VerifyOutputFn(options.ToDigest); err != nil {
1225+
if o.DryRun {
1226+
return err
1227+
}
1228+
exitErr = err
1229+
}
1230+
}
11681231

1169-
// TODO: support a dry run that doesn't push the image, but requires append to have a dry-run mode
11701232
toRefWithDigest := toRef
11711233
toRefWithDigest.Tag = ""
11721234
toRefWithDigest.ID = options.ToDigest.String()
@@ -1182,13 +1244,9 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time)
11821244
klog.V(2).Infof("Signature for output:\n%s", string(msg))
11831245
}
11841246

1185-
default:
1186-
fmt.Fprintf(o.ErrOut, "info: Extracting operator contents to disk without building a release artifact\n")
1187-
if _, err := io.Copy(ioutil.Discard, r); err != nil {
1188-
return err
1189-
}
1247+
fmt.Fprintf(o.Out, "%s %s %s\n", options.ToDigest.String(), is.Name, is.CreationTimestamp.Format(time.RFC3339))
11901248
}
1191-
return nil
1249+
return exitErr
11921250
}
11931251

11941252
func toJSONString(obj interface{}) string {
@@ -1245,7 +1303,6 @@ func writePayload(w io.Writer, is *imageapi.ImageStream, cm *CincinnatiMetadata,
12451303
return nil, err
12461304
}
12471305
newest = newest.UTC().Truncate(time.Second)
1248-
is.CreationTimestamp.Time = newest
12491306
klog.V(4).Infof("Most recent content has date %s", newest.Format(time.RFC3339))
12501307

12511308
gw := gzip.NewWriter(w)
@@ -1511,7 +1568,7 @@ func pruneUnreferencedImageStreams(out io.Writer, is *imageapi.ImageStream, meta
15111568
updated = append(updated, tag)
15121569
}
15131570
if len(updated) != len(is.Spec.Tags) {
1514-
fmt.Fprintf(out, "info: Included %d images in the release\n", len(updated))
1571+
fmt.Fprintf(out, "info: Included %d images from %d input operators into the release\n", len(updated), len(metadata))
15151572
is.Spec.Tags = updated
15161573
}
15171574
return nil

0 commit comments

Comments
 (0)