Skip to content

Commit a0a7be0

Browse files
Teach oc adm release extract to checkout Git repos
Accept `--git=DIR` and clone the repositories to that path, then checkout the correct commit.
1 parent 4229b7a commit a0a7be0

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

contrib/completions/bash/oc

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/completions/zsh/oc

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ package release
22

33
import (
44
"archive/tar"
5+
"bytes"
6+
"errors"
57
"fmt"
68
"io"
9+
"net/url"
710
"os"
11+
"os/exec"
12+
"path"
13+
"path/filepath"
14+
"regexp"
15+
"strings"
816
"time"
917

18+
"github.com/golang/glog"
19+
1020
"github.com/spf13/cobra"
1121

1222
digest "github.com/opencontainers/go-digest"
@@ -38,6 +48,12 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
3848
debugging. Update images contain manifests and metadata about the operators that
3949
must be installed on the cluster for a given version.
4050
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+
4157
Experimental: This command is under active development and may change without notice.
4258
`),
4359
Run: func(cmd *cobra.Command, args []string) {
@@ -47,6 +63,7 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
4763
}
4864
flags := cmd.Flags()
4965
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.")
5067
flags.StringVar(&o.From, "from", o.From, "Image containing the release payload.")
5168
flags.StringVar(&o.File, "file", o.File, "Extract a single file from the payload to standard output.")
5269
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 {
5875

5976
From string
6077

78+
// GitPath is the path of a root directory to extract the source of a release to.
79+
GitPath string
80+
6181
Directory string
6282
File string
6383

@@ -67,6 +87,14 @@ type ExtractOptions struct {
6787
}
6888

6989
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+
}
7098
return nil
7199
}
72100

@@ -78,6 +106,10 @@ func (o *ExtractOptions) Run() error {
78106
return fmt.Errorf("only one of --to and --file may be set")
79107
}
80108

109+
if len(o.GitPath) > 0 {
110+
return o.extractGit(o.GitPath)
111+
}
112+
81113
dir := o.Directory
82114
if err := os.MkdirAll(dir, 0755); err != nil {
83115
return err
@@ -147,3 +179,159 @@ func (o *ExtractOptions) Run() error {
147179
return opts.Run()
148180
}
149181
}
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

Comments
 (0)