pkg/cmd/cli/cmd/startbuild.go
8c556b6d
 package cmd
 
 import (
25189a16
 	"bytes"
e7387750
 	"encoding/json"
14f0c3cb
 	"fmt"
8c556b6d
 	"io"
e7387750
 	"io/ioutil"
25189a16
 	"net/http"
 	"net/url"
e7387750
 	"os"
 	"strings"
8c556b6d
 
e7387750
 	"github.com/golang/glog"
c431087f
 	"github.com/spf13/cobra"
 
83c702b4
 	kapi "k8s.io/kubernetes/pkg/api"
 	"k8s.io/kubernetes/pkg/client"
 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
 	"k8s.io/kubernetes/pkg/util"
25189a16
 
41de07a4
 	buildapi "github.com/openshift/origin/pkg/build/api"
260558d2
 	osclient "github.com/openshift/origin/pkg/client"
cfe9d1ef
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
e7387750
 	"github.com/openshift/origin/pkg/generate/git"
8c556b6d
 )
 
1558f2d9
 const (
c124965f
 	startBuildLong = `
 Start a build
8c556b6d
 
821ddfce
 This command starts a build for the provided BuildConfig or re-runs an existing build using
1558f2d9
 --from-build=<name>. You may pass the --follow flag to see output from the build.`
8c556b6d
 
821ddfce
 	startBuildExample = `  // Starts build from BuildConfig matching the name "3bd2ug53b"
14c0fea1
   $ %[1]s start-build 3bd2ug53b
0df64392
 
1558f2d9
   // Starts build from build matching the name "3bd2ug53b"
14c0fea1
   $ %[1]s start-build --from-build=3bd2ug53b
50f134ea
 
821ddfce
   // Starts build from BuildConfig matching the name "3bd2ug53b" and watches the logs until the build
1558f2d9
   // completes or fails
   $ %[1]s start-build 3bd2ug53b --follow`
 )
0c5b8196
 
13eb76ee
 // NewCmdStartBuild implements the OpenShift cli start-build command
0c5b8196
 func NewCmdStartBuild(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
260558d2
 	webhooks := util.StringFlag{}
 	webhooks.Default("none")
 
0c5b8196
 	cmd := &cobra.Command{
1558f2d9
 		Use:     "start-build (BUILDCONFIG | --from-build=BUILD)",
ebf8e4ef
 		Short:   "Starts a new build",
c36413f1
 		Long:    startBuildLong,
 		Example: fmt.Sprintf(startBuildExample, fullName),
8c556b6d
 		Run: func(cmd *cobra.Command, args []string) {
260558d2
 			err := RunStartBuild(f, out, cmd, args, webhooks)
af00209a
 			cmdutil.CheckErr(err)
 		},
 	}
 	cmd.Flags().String("from-build", "", "Specify the name of a build which should be re-run")
 	cmd.Flags().Bool("follow", false, "Start a build and watch its logs until it completes or fails")
821ddfce
 	cmd.Flags().Var(&webhooks, "list-webhooks", "List the webhooks for the specified BuildConfig or build; accepts 'all', 'generic', or 'github'")
 	cmd.Flags().String("from-webhook", "", "Specify a webhook URL for an existing BuildConfign to trigger")
25189a16
 	cmd.Flags().String("git-post-receive", "", "The contents of the post-receive hook to trigger a build")
e7387750
 	cmd.Flags().String("git-repository", "", "The path to the git repository for post-receive; defaults to the current directory")
af00209a
 	return cmd
 }
8c556b6d
 
13eb76ee
 // RunStartBuild contains all the necessary functionality for the OpenShift cli start-build command
260558d2
 func RunStartBuild(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, args []string, webhooks util.StringFlag) error {
25189a16
 	webhook := cmdutil.GetFlagString(cmd, "from-webhook")
260558d2
 	buildName := cmdutil.GetFlagString(cmd, "from-build")
 	follow := cmdutil.GetFlagBool(cmd, "follow")
 
 	switch {
 	case len(webhook) > 0:
 		if len(args) > 0 || len(buildName) > 0 {
 			return cmdutil.UsageError(cmd, "The '--from-webhook' flag is incompatible with arguments or '--from-build'")
 		}
e7387750
 		path := cmdutil.GetFlagString(cmd, "git-repository")
 		postReceivePath := cmdutil.GetFlagString(cmd, "git-post-receive")
 		repo := git.NewRepository()
 		return RunStartBuildWebHook(f, out, webhook, path, postReceivePath, repo)
260558d2
 	case len(args) != 1 && len(buildName) == 0:
821ddfce
 		return cmdutil.UsageError(cmd, "Must pass a name of a BuildConfig or specify build name with '--from-build' flag")
25189a16
 	}
 
260558d2
 	name := buildName
 	isBuild := true
 	if len(name) == 0 {
 		name = args[0]
 		isBuild = false
 	}
 
 	if webhooks.Provided() {
 		return RunListBuildWebHooks(f, out, cmd.Out(), name, isBuild, webhooks.String())
af00209a
 	}
8c556b6d
 
af00209a
 	client, _, err := f.Clients()
 	if err != nil {
 		return err
 	}
14f0c3cb
 
ab7d732c
 	namespace, _, err := f.DefaultNamespace()
af00209a
 	if err != nil {
 		return err
 	}
0f50aa4f
 
260558d2
 	request := &buildapi.BuildRequest{
 		ObjectMeta: kapi.ObjectMeta{Name: name},
 	}
af00209a
 	var newBuild *buildapi.Build
260558d2
 	if isBuild {
 		if newBuild, err = client.Builds(namespace).Clone(request); err != nil {
af00209a
 			return err
 		}
 	} else {
260558d2
 		if newBuild, err = client.BuildConfigs(namespace).Instantiate(request); err != nil {
af00209a
 			return err
 		}
 	}
5ce468b4
 	fmt.Fprintf(out, "%s\n", newBuild.Name)
50f134ea
 
af00209a
 	if follow {
5ce468b4
 		opts := buildapi.BuildLogOptions{
 			Follow: true,
 			NoWait: false,
 		}
 		rd, err := client.BuildLogs(namespace).Get(newBuild.Name, opts).Stream()
af00209a
 		if err != nil {
5ce468b4
 			return fmt.Errorf("error getting logs: %v", err)
af00209a
 		}
5ce468b4
 		defer rd.Close()
 		_, err = io.Copy(out, rd)
 		if err != nil {
 			return fmt.Errorf("error streaming logs: %v", err)
af00209a
 		}
8c556b6d
 	}
af00209a
 	return nil
8c556b6d
 }
25189a16
 
260558d2
 // RunListBuildWebHooks prints the webhooks for the provided build config.
 func RunListBuildWebHooks(f *clientcmd.Factory, out, errOut io.Writer, name string, isBuild bool, webhookFilter string) error {
 	generic, github := false, false
 	prefix := false
 	switch webhookFilter {
 	case "all":
 		generic, github = true, true
 		prefix = true
 	case "generic":
 		generic = true
 	case "github":
 		github = true
 	default:
 		return fmt.Errorf("--list-webhooks must be 'all', 'generic', or 'github'")
 	}
 	client, _, err := f.Clients()
 	if err != nil {
 		return err
 	}
ab7d732c
 	namespace, _, err := f.DefaultNamespace()
260558d2
 	if err != nil {
 		return err
 	}
 
 	if isBuild {
 		build, err := client.Builds(namespace).Get(name)
 		if err != nil {
 			return err
 		}
8933ecc1
 		ref := build.Status.Config
260558d2
 		if ref == nil {
821ddfce
 			return fmt.Errorf("the provided Build %q was not created from a BuildConfig and cannot have webhooks", name)
260558d2
 		}
 		if len(ref.Namespace) > 0 {
 			namespace = ref.Namespace
 		}
 		name = ref.Name
 	}
 	config, err := client.BuildConfigs(namespace).Get(name)
 	if err != nil {
 		return err
 	}
 
8933ecc1
 	for _, t := range config.Spec.Triggers {
260558d2
 		hookType := ""
 		switch {
 		case t.GenericWebHook != nil && generic:
 			if prefix {
 				hookType = "generic "
 			}
e65dbda2
 		case t.GitHubWebHook != nil && github:
260558d2
 			if prefix {
 				hookType = "github "
 			}
 		default:
 			continue
 		}
 		url, err := client.BuildConfigs(namespace).WebHookURL(name, &t)
 		if err != nil {
 			if err != osclient.ErrTriggerIsNotAWebHook {
 				fmt.Fprintf(errOut, "error: unable to get webhook for %s: %v", name, err)
 			}
 			continue
 		}
 		fmt.Fprintf(out, "%s%s\n", hookType, url.String())
 	}
 	return nil
 }
 
25189a16
 // RunStartBuildWebHook tries to trigger the provided webhook. It will attempt to utilize the current client
 // configuration if the webhook has the same URL.
e7387750
 func RunStartBuildWebHook(f *clientcmd.Factory, out io.Writer, webhook string, path, postReceivePath string, repo git.Repository) error {
 	hook, err := url.Parse(webhook)
 	if err != nil {
 		return err
25189a16
 	}
 
e7387750
 	event, err := hookEventFromPostReceive(repo, path, postReceivePath)
25189a16
 	if err != nil {
 		return err
 	}
e7387750
 
 	// TODO: should be a versioned struct
 	data, err := json.Marshal(event)
 	if err != nil {
 		return err
 	}
 
25189a16
 	httpClient := http.DefaultClient
 	// when using HTTPS, try to reuse the local config transport if possible to get a client cert
 	// TODO: search all configs
 	if hook.Scheme == "https" {
 		config, err := f.OpenShiftClientConfig.ClientConfig()
 		if err == nil {
 			if url, err := client.DefaultServerURL(config.Host, "", "test", true); err == nil {
 				if url.Host == hook.Host && url.Scheme == hook.Scheme {
 					if rt, err := client.TransportFor(config); err == nil {
 						httpClient = &http.Client{Transport: rt}
 					}
 				}
 			}
 		}
 	}
e7387750
 	glog.V(4).Infof("Triggering hook %s\n%s", hook, string(data))
 	resp, err := httpClient.Post(hook.String(), "application/json", bytes.NewBuffer(data))
 	if err != nil {
25189a16
 		return err
 	}
e7387750
 	switch {
 	case resp.StatusCode == 301 || resp.StatusCode == 302:
 		// TODO: follow redirect and display output
 	case resp.StatusCode < 200 || resp.StatusCode >= 300:
 		body, _ := ioutil.ReadAll(resp.Body)
 		return fmt.Errorf("server rejected our request %d\nremote: %s", resp.StatusCode, string(body))
 	}
25189a16
 	return nil
 }
e7387750
 
 // hookEventFromPostReceive creates a GenericWebHookEvent from the provided git repository and
 // post receive input. If no inputs are available will return an empty event.
 func hookEventFromPostReceive(repo git.Repository, path, postReceivePath string) (*buildapi.GenericWebHookEvent, error) {
 	// TODO: support other types of refs
 	event := &buildapi.GenericWebHookEvent{
 		Type: buildapi.BuildSourceGit,
 		Git:  &buildapi.GitInfo{},
 	}
 
 	// attempt to extract a post receive body
 	refs := []git.ChangedRef{}
 	switch receive := postReceivePath; {
 	case receive == "-":
 		r, err := git.ParsePostReceive(os.Stdin)
 		if err != nil {
 			return nil, err
 		}
 		refs = r
 	case len(receive) > 0:
 		file, err := os.Open(receive)
 		if err != nil {
 			return nil, fmt.Errorf("unable to open --git-post-receive argument as a file: %v", err)
 		}
 		defer file.Close()
 		r, err := git.ParsePostReceive(file)
 		if err != nil {
 			return nil, err
 		}
 		refs = r
 	}
 	for _, ref := range refs {
 		if len(ref.New) == 0 || ref.New == ref.Old {
 			continue
 		}
 		info, err := gitRefInfo(repo, path, ref.New)
 		if err != nil {
 			glog.V(4).Infof("Could not retrieve info for %s:%s: %v", ref.Ref, ref.New, err)
 		}
 		info.Ref = ref.Ref
 		info.Commit = ref.New
 		event.Git.Refs = append(event.Git.Refs, info)
 	}
 	return event, nil
 }
 
 // gitRefInfo extracts a buildapi.GitRefInfo from the specified repository or returns
 // an error.
 func gitRefInfo(repo git.Repository, dir, ref string) (buildapi.GitRefInfo, error) {
 	info := buildapi.GitRefInfo{}
 	if repo == nil {
 		return info, nil
 	}
 	out, err := repo.ShowFormat(dir, ref, "%an%n%ae%n%cn%n%ce%n%B")
 	if err != nil {
 		return info, err
 	}
 	lines := strings.SplitN(out, "\n", 5)
 	if len(lines) != 5 {
 		full := make([]string, 5)
 		copy(full, lines)
 		lines = full
 	}
 	info.Author.Name = lines[0]
 	info.Author.Email = lines[1]
 	info.Committer.Name = lines[2]
 	info.Committer.Email = lines[3]
 	info.Message = lines[4]
 	return info, nil
 }