@@ -2,11 +2,21 @@ package release
2
2
3
3
import (
4
4
"archive/tar"
5
+ "bytes"
6
+ "errors"
5
7
"fmt"
6
8
"io"
9
+ "net/url"
7
10
"os"
11
+ "os/exec"
12
+ "path"
13
+ "path/filepath"
14
+ "regexp"
15
+ "strings"
8
16
"time"
9
17
18
+ "github.com/golang/glog"
19
+
10
20
"github.com/spf13/cobra"
11
21
12
22
digest "github.com/opencontainers/go-digest"
@@ -38,6 +48,12 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
38
48
debugging. Update images contain manifests and metadata about the operators that
39
49
must be installed on the cluster for a given version.
40
50
51
+ Instead of extracting the manifests, you can specify --git=DIR to perform a Git
52
+ checkout of the source code that comprises the release. A warning will be printed
53
+ if the component is not associated with source code. The command will not perform
54
+ any destructive actions on your behalf except for executing a 'git checkout' which
55
+ may change the current branch.
56
+
41
57
Experimental: This command is under active development and may change without notice.
42
58
` ),
43
59
Run : func (cmd * cobra.Command , args []string ) {
@@ -47,6 +63,7 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
47
63
}
48
64
flags := cmd .Flags ()
49
65
flags .StringVarP (& o .RegistryConfig , "registry-config" , "a" , o .RegistryConfig , "Path to your registry credentials (defaults to ~/.docker/config.json)" )
66
+ flags .StringVar (& o .GitPath , "git" , o .GitPath , "Check out the sources that created this release into the provided dir. Repos will be created at <dir>/<host>/<path>. Requires 'git' on your path." )
50
67
flags .StringVar (& o .From , "from" , o .From , "Image containing the release payload." )
51
68
flags .StringVar (& o .File , "file" , o .File , "Extract a single file from the payload to standard output." )
52
69
flags .StringVar (& o .Directory , "to" , o .Directory , "Directory to write release contents to, defaults to the current directory." )
@@ -58,6 +75,9 @@ type ExtractOptions struct {
58
75
59
76
From string
60
77
78
+ // GitPath is the path of a root directory to extract the source of a release to.
79
+ GitPath string
80
+
61
81
Directory string
62
82
File string
63
83
@@ -67,6 +87,14 @@ type ExtractOptions struct {
67
87
}
68
88
69
89
func (o * ExtractOptions ) Complete (cmd * cobra.Command , args []string ) error {
90
+ switch {
91
+ case len (args ) == 0 && len (o .From ) == 0 :
92
+ return fmt .Errorf ("must specify an image containing a release payload with --from" )
93
+ case len (args ) == 1 && len (o .From ) > 0 , len (args ) > 1 :
94
+ return fmt .Errorf ("you may on specify a single image via --from or argument" )
95
+ case len (args ) == 1 :
96
+ o .From = args [0 ]
97
+ }
70
98
return nil
71
99
}
72
100
@@ -78,6 +106,10 @@ func (o *ExtractOptions) Run() error {
78
106
return fmt .Errorf ("only one of --to and --file may be set" )
79
107
}
80
108
109
+ if len (o .GitPath ) > 0 {
110
+ return o .extractGit (o .GitPath )
111
+ }
112
+
81
113
dir := o .Directory
82
114
if err := os .MkdirAll (dir , 0755 ); err != nil {
83
115
return err
@@ -147,3 +179,159 @@ func (o *ExtractOptions) Run() error {
147
179
return opts .Run ()
148
180
}
149
181
}
182
+
183
+ func (o * ExtractOptions ) extractGit (dir string ) error {
184
+ if err := os .MkdirAll (dir , 0750 ); err != nil {
185
+ return err
186
+ }
187
+
188
+ release , err := NewInfoOptions (o .IOStreams ).LoadReleaseInfo (o .From )
189
+ if err != nil {
190
+ return err
191
+ }
192
+
193
+ cloner := & git {}
194
+
195
+ hadErrors := false
196
+ alreadyExtracted := make (map [string ]string )
197
+ for _ , ref := range release .References .Spec .Tags {
198
+ repo := ref .Annotations ["io.openshift.build.source-location" ]
199
+ commit := ref .Annotations ["io.openshift.build.commit.id" ]
200
+ if len (repo ) == 0 || len (commit ) == 0 {
201
+ glog .V (2 ).Infof ("Tag %s has no source info" , ref .Name )
202
+ continue
203
+ }
204
+ if oldCommit , ok := alreadyExtracted [repo ]; ok {
205
+ if oldCommit != commit {
206
+ fmt .Fprintf (o .ErrOut , "warning: Repo %s referenced more than once with different commits, only checking out the first reference\n " , repo )
207
+ }
208
+ continue
209
+ }
210
+ alreadyExtracted [repo ] = commit
211
+
212
+ u , err := sourceLocationAsURL (repo )
213
+ if err != nil {
214
+ return err
215
+ }
216
+ gitPath := u .Path
217
+ if strings .HasSuffix (gitPath , ".git" ) {
218
+ gitPath = strings .TrimSuffix (gitPath , ".git" )
219
+ }
220
+ gitPath = path .Clean (gitPath )
221
+ basePath := filepath .Join (dir , u .Host , filepath .FromSlash (gitPath ))
222
+
223
+ var git * git
224
+ fi , err := os .Stat (basePath )
225
+ if err != nil {
226
+ if ! os .IsNotExist (err ) {
227
+ return err
228
+ }
229
+ if err := os .MkdirAll (basePath , 0750 ); err != nil {
230
+ return err
231
+ }
232
+ } else {
233
+ if ! fi .IsDir () {
234
+ return fmt .Errorf ("repo path %s is not a directory" , basePath )
235
+ }
236
+ }
237
+ git , err = cloner .ChangeContext (basePath )
238
+ if err != nil {
239
+ if err != noSuchRepo {
240
+ return err
241
+ }
242
+ glog .V (2 ).Infof ("Cloning %s ..." , repo )
243
+ if err := git .Clone (repo , o .Out , o .ErrOut ); err != nil {
244
+ hadErrors = true
245
+ fmt .Fprintf (o .ErrOut , "error: cloning %s: %v\n " , repo , err )
246
+ continue
247
+ }
248
+ }
249
+ glog .V (2 ).Infof ("Checkout %s from %s ..." , commit , repo )
250
+ if err := git .CheckoutCommit (repo , commit ); err != nil {
251
+ hadErrors = true
252
+ fmt .Fprintf (o .ErrOut , "error: checking out commit for %s: %v\n " , repo , err )
253
+ continue
254
+ }
255
+ }
256
+ if hadErrors {
257
+ return kcmdutil .ErrExit
258
+ }
259
+ return nil
260
+ }
261
+
262
+ type git struct {
263
+ path string
264
+ }
265
+
266
+ var noSuchRepo = errors .New ("location is not a git repo" )
267
+
268
+ func (g * git ) exec (command ... string ) (string , error ) {
269
+ buf := & bytes.Buffer {}
270
+ bufErr := & bytes.Buffer {}
271
+ cmd := exec .Command ("git" , append ([]string {"-C" , g .path }, command ... )... )
272
+ glog .V (5 ).Infof ("Executing git: %v\n " , cmd .Args )
273
+ cmd .Stdout = buf
274
+ cmd .Stderr = bufErr
275
+ err := cmd .Run ()
276
+ if err != nil {
277
+ return bufErr .String (), err
278
+ }
279
+ return buf .String (), nil
280
+ }
281
+
282
+ func (g * git ) streamExec (out , errOut io.Writer , command ... string ) error {
283
+ cmd := exec .Command ("git" , append ([]string {"-C" , g .path }, command ... )... )
284
+ cmd .Stdout = out
285
+ cmd .Stderr = errOut
286
+ return cmd .Run ()
287
+ }
288
+
289
+ func (g * git ) ChangeContext (path string ) (* git , error ) {
290
+ location := & git {path : path }
291
+ if errOut , err := location .exec ("rev-parse" , "--git-dir" ); err != nil {
292
+ if strings .Contains (errOut , "not a git repository" ) {
293
+ return location , noSuchRepo
294
+ }
295
+ return location , err
296
+ }
297
+ return location , nil
298
+ }
299
+
300
+ func (g * git ) Clone (repository string , out , errOut io.Writer ) error {
301
+ return (& git {}).streamExec (out , errOut , "clone" , repository , g .path )
302
+ }
303
+
304
+ func (g * git ) parent () * git {
305
+ return & git {path : filepath .Dir (g .path )}
306
+ }
307
+
308
+ func (g * git ) basename () string {
309
+ return filepath .Base (g .path )
310
+ }
311
+
312
+ func (g * git ) CheckoutCommit (repo , commit string ) error {
313
+ _ , err := g .exec ("rev-parse" , commit )
314
+ if err == nil {
315
+ return nil
316
+ }
317
+
318
+ // try to fetch by URL
319
+ if _ , err := g .exec ("fetch" , repo ); err == nil {
320
+ if _ , err := g .exec ("rev-parse" , commit ); err == nil {
321
+ return nil
322
+ }
323
+ }
324
+
325
+ // TODO: what if that transport URL does not exist?
326
+
327
+ return fmt .Errorf ("could not locate commit %s" , commit )
328
+ }
329
+
330
+ var reMatch = regexp .MustCompile (`^([a-zA-Z0-9\-\_]+)@([^:]+):(.+)$` )
331
+
332
+ func sourceLocationAsURL (location string ) (* url.URL , error ) {
333
+ if matches := reMatch .FindStringSubmatch (location ); matches != nil {
334
+ return & url.URL {Scheme : "git" , User : url .UserPassword (matches [1 ], "" ), Host : matches [2 ], Path : matches [3 ]}, nil
335
+ }
336
+ return url .Parse (location )
337
+ }
0 commit comments