@@ -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() {
330335func (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
10381080func (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
11941252func 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