Skip to content

Commit

Permalink
Teach oc adm release extract to checkout Git repos
Browse files Browse the repository at this point in the history
Accept `--git=DIR` and clone the repositories to that path, then
checkout the correct commit.
  • Loading branch information
smarterclayton committed Feb 12, 2019
1 parent 4229b7a commit a0a7be0
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 0 deletions.
2 changes: 2 additions & 0 deletions contrib/completions/bash/oc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions contrib/completions/zsh/oc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

188 changes: 188 additions & 0 deletions pkg/oc/cli/admin/release/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ package release

import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/golang/glog"

"github.com/spf13/cobra"

digest "github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -38,6 +48,12 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
debugging. Update images contain manifests and metadata about the operators that
must be installed on the cluster for a given version.
Instead of extracting the manifests, you can specify --git=DIR to perform a Git
checkout of the source code that comprises the release. A warning will be printed
if the component is not associated with source code. The command will not perform
any destructive actions on your behalf except for executing a 'git checkout' which
may change the current branch.
Experimental: This command is under active development and may change without notice.
`),
Run: func(cmd *cobra.Command, args []string) {
Expand All @@ -47,6 +63,7 @@ func NewExtract(f kcmdutil.Factory, parentName string, streams genericclioptions
}
flags := cmd.Flags()
flags.StringVarP(&o.RegistryConfig, "registry-config", "a", o.RegistryConfig, "Path to your registry credentials (defaults to ~/.docker/config.json)")
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.")
flags.StringVar(&o.From, "from", o.From, "Image containing the release payload.")
flags.StringVar(&o.File, "file", o.File, "Extract a single file from the payload to standard output.")
flags.StringVar(&o.Directory, "to", o.Directory, "Directory to write release contents to, defaults to the current directory.")
Expand All @@ -58,6 +75,9 @@ type ExtractOptions struct {

From string

// GitPath is the path of a root directory to extract the source of a release to.
GitPath string

Directory string
File string

Expand All @@ -67,6 +87,14 @@ type ExtractOptions struct {
}

func (o *ExtractOptions) Complete(cmd *cobra.Command, args []string) error {
switch {
case len(args) == 0 && len(o.From) == 0:
return fmt.Errorf("must specify an image containing a release payload with --from")
case len(args) == 1 && len(o.From) > 0, len(args) > 1:
return fmt.Errorf("you may on specify a single image via --from or argument")
case len(args) == 1:
o.From = args[0]
}
return nil
}

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

if len(o.GitPath) > 0 {
return o.extractGit(o.GitPath)
}

dir := o.Directory
if err := os.MkdirAll(dir, 0755); err != nil {
return err
Expand Down Expand Up @@ -147,3 +179,159 @@ func (o *ExtractOptions) Run() error {
return opts.Run()
}
}

func (o *ExtractOptions) extractGit(dir string) error {
if err := os.MkdirAll(dir, 0750); err != nil {
return err
}

release, err := NewInfoOptions(o.IOStreams).LoadReleaseInfo(o.From)
if err != nil {
return err
}

cloner := &git{}

hadErrors := false
alreadyExtracted := make(map[string]string)
for _, ref := range release.References.Spec.Tags {
repo := ref.Annotations["io.openshift.build.source-location"]
commit := ref.Annotations["io.openshift.build.commit.id"]
if len(repo) == 0 || len(commit) == 0 {
glog.V(2).Infof("Tag %s has no source info", ref.Name)
continue
}
if oldCommit, ok := alreadyExtracted[repo]; ok {
if oldCommit != commit {
fmt.Fprintf(o.ErrOut, "warning: Repo %s referenced more than once with different commits, only checking out the first reference\n", repo)
}
continue
}
alreadyExtracted[repo] = commit

u, err := sourceLocationAsURL(repo)
if err != nil {
return err
}
gitPath := u.Path
if strings.HasSuffix(gitPath, ".git") {
gitPath = strings.TrimSuffix(gitPath, ".git")
}
gitPath = path.Clean(gitPath)
basePath := filepath.Join(dir, u.Host, filepath.FromSlash(gitPath))

var git *git
fi, err := os.Stat(basePath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(basePath, 0750); err != nil {
return err
}
} else {
if !fi.IsDir() {
return fmt.Errorf("repo path %s is not a directory", basePath)
}
}
git, err = cloner.ChangeContext(basePath)
if err != nil {
if err != noSuchRepo {
return err
}
glog.V(2).Infof("Cloning %s ...", repo)
if err := git.Clone(repo, o.Out, o.ErrOut); err != nil {
hadErrors = true
fmt.Fprintf(o.ErrOut, "error: cloning %s: %v\n", repo, err)
continue
}
}
glog.V(2).Infof("Checkout %s from %s ...", commit, repo)
if err := git.CheckoutCommit(repo, commit); err != nil {
hadErrors = true
fmt.Fprintf(o.ErrOut, "error: checking out commit for %s: %v\n", repo, err)
continue
}
}
if hadErrors {
return kcmdutil.ErrExit
}
return nil
}

type git struct {
path string
}

var noSuchRepo = errors.New("location is not a git repo")

func (g *git) exec(command ...string) (string, error) {
buf := &bytes.Buffer{}
bufErr := &bytes.Buffer{}
cmd := exec.Command("git", append([]string{"-C", g.path}, command...)...)
glog.V(5).Infof("Executing git: %v\n", cmd.Args)
cmd.Stdout = buf
cmd.Stderr = bufErr
err := cmd.Run()
if err != nil {
return bufErr.String(), err
}
return buf.String(), nil
}

func (g *git) streamExec(out, errOut io.Writer, command ...string) error {
cmd := exec.Command("git", append([]string{"-C", g.path}, command...)...)
cmd.Stdout = out
cmd.Stderr = errOut
return cmd.Run()
}

func (g *git) ChangeContext(path string) (*git, error) {
location := &git{path: path}
if errOut, err := location.exec("rev-parse", "--git-dir"); err != nil {
if strings.Contains(errOut, "not a git repository") {
return location, noSuchRepo
}
return location, err
}
return location, nil
}

func (g *git) Clone(repository string, out, errOut io.Writer) error {
return (&git{}).streamExec(out, errOut, "clone", repository, g.path)
}

func (g *git) parent() *git {
return &git{path: filepath.Dir(g.path)}
}

func (g *git) basename() string {
return filepath.Base(g.path)
}

func (g *git) CheckoutCommit(repo, commit string) error {
_, err := g.exec("rev-parse", commit)
if err == nil {
return nil
}

// try to fetch by URL
if _, err := g.exec("fetch", repo); err == nil {
if _, err := g.exec("rev-parse", commit); err == nil {
return nil
}
}

// TODO: what if that transport URL does not exist?

return fmt.Errorf("could not locate commit %s", commit)
}

var reMatch = regexp.MustCompile(`^([a-zA-Z0-9\-\_]+)@([^:]+):(.+)$`)

func sourceLocationAsURL(location string) (*url.URL, error) {
if matches := reMatch.FindStringSubmatch(location); matches != nil {
return &url.URL{Scheme: "git", User: url.UserPassword(matches[1], ""), Host: matches[2], Path: matches[3]}, nil
}
return url.Parse(location)
}

0 comments on commit a0a7be0

Please sign in to comment.