Przeglądaj źródła

temporary deploy flow: download remote repository + docker builds

Alexander Belanger 5 lat temu
rodzic
commit
1f6b4bfe32

+ 41 - 0
cli/cmd/api/git_repo.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/server/api"
 )
 
 // ListGitRepoResponse is the list of Git repo integrations for a project
@@ -39,3 +40,43 @@ func (c *Client) ListGitRepos(
 
 	return *bodyResp, nil
 }
+
+type GetRepoTarballDownloadURLResp api.HandleGetRepoZIPDownloadURLResp
+
+// ListGitRepos returns a list of Git repos for a project
+func (c *Client) GetRepoZIPDownloadURL(
+	ctx context.Context,
+	projectID uint,
+	gitActionConfig *models.GitActionConfigExternal,
+) (*GetRepoTarballDownloadURLResp, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf(
+			"%s/projects/%d/gitrepos/%d/repos/%s/%s/%s/tarball_url",
+			c.BaseURL,
+			projectID,
+			gitActionConfig.GitRepoID,
+			"github",
+			gitActionConfig.GitRepo,
+			gitActionConfig.GitBranch,
+		),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &GetRepoTarballDownloadURLResp{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}

+ 2 - 2
cli/cmd/api/k8s.go

@@ -96,7 +96,7 @@ func (c *Client) GetRelease(
 	ctx context.Context,
 	projectID, clusterID uint,
 	namespace, name string,
-) (GetReleaseResponse, error) {
+) (*GetReleaseResponse, error) {
 	cl := fmt.Sprintf("%d", clusterID)
 
 	req, err := http.NewRequest(
@@ -124,7 +124,7 @@ func (c *Client) GetRelease(
 		return nil, err
 	}
 
-	return *bodyResp, nil
+	return bodyResp, nil
 }
 
 // GetReleaseAllPodsResponse is the list of all pods for a given Helm release

+ 0 - 1
cli/cmd/auth.go

@@ -80,7 +80,6 @@ func login() error {
 	user, _ := client.AuthCheck(context.Background())
 
 	if user != nil {
-		color.Yellow(getToken())
 		color.Yellow("You are already logged in. If you'd like to log out, run \"porter auth logout\".")
 		return nil
 	}

+ 282 - 9
cli/cmd/deploy.go

@@ -1,10 +1,17 @@
 package cmd
 
 import (
+	"context"
+	"errors"
+	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
+	"strings"
 
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/github"
 	"github.com/spf13/cobra"
 )
 
@@ -13,7 +20,7 @@ var app = ""
 // deployCmd represents the "porter deploy" base command when called
 // without any subcommands
 var deployCmd = &cobra.Command{
-	Use:   "app deploy",
+	Use:   "deploy",
 	Short: "Builds and deploys a specified application given by the --app flag.",
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, deploy)
@@ -24,6 +31,57 @@ var deployCmd = &cobra.Command{
 	},
 }
 
+var deployInitCmd = &cobra.Command{
+	Use:   "init",
+	Short: "Initializes a deployment for a specified application given by the --app flag.",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deployInit)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var getEnvFileDest = ""
+
+var deployGetEnvCmd = &cobra.Command{
+	Use:   "get-env",
+	Short: "Gets environment variables for a deployment for a specified application given by the --app flag.",
+	Long: fmt.Sprintf(`Gets environment variables for a deployment for a specified application given by the --app flag. 
+By default, env variables are printed via stdout for use in downstream commands, for example:
+	
+  %s
+	
+Output can also be written to a dotenv file via the --file flag, which should specify the destination
+path for a .env file. For example:
+
+  %s
+`,
+		color.New(color.FgGreen).Sprintf("porter deploy get-env --app <app> | xargs"),
+		color.New(color.FgGreen).Sprintf("porter deploy get-env --app <app> --file .env"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deployGetEnv)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var deployBuildCmd = &cobra.Command{
+	Use:   "build",
+	Short: "TBD",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deployBuild)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(deployCmd)
 
@@ -33,25 +91,240 @@ func init() {
 		"",
 		"Application in the Porter dashboard",
 	)
+
+	deployCmd.AddCommand(deployInitCmd)
+	deployCmd.AddCommand(deployGetEnvCmd)
+
+	deployGetEnvCmd.PersistentFlags().StringVar(
+		&getEnvFileDest,
+		"file",
+		"",
+		"file destination for .env files",
+	)
+
+	deployCmd.AddCommand(deployBuildCmd)
 }
 
-func deploy(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
+func deploy(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
 	color.New(color.FgGreen).Println("Deploying app:", app)
 
-	return nil
+	return deployInit(resp, client, args)
 }
 
+var release *api.GetReleaseResponse = nil
+
 // deployInit first reads the release given by the --app or the --job flag. It then
 // configures docker with the registries linked to the project.
 func deployInit(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := config.Project
+	cID := config.Cluster
+
+	var err error
+
+	release, err = client.GetRelease(context.TODO(), pID, cID, namespace, app)
+
+	if err != nil {
+		return err
+	}
+
 	return dockerConfig(resp, client, args)
 }
 
-// deploySetEnv reads the build environment variables from a release and sets them using
-// os.SetEnv
-// func deploySetBuildEnv() error {
+// deployGetEnv retrieves the env from a release and outputs it to either a file
+// or stdout depending on getEnvFileDest
+func deployGetEnv(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	if release == nil {
+		err := deployInit(resp, client, args)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	prefix, err := deploySetEnv(client)
+
+	if err != nil {
+		return err
+	}
 
-// }
+	// join lines together
+	lines := make([]string, 0)
 
-// deployBuild uses the configuration stored in the release to
-// func deployBuild()
+	// use os.Environ to get output already formatted as KEY=value
+	for _, line := range os.Environ() {
+		// filter for PORTER_<RELEASE> and strip prefix
+		if strings.Contains(line, prefix+"_") {
+			lines = append(lines, strings.Split(line, prefix+"_")[1])
+		}
+	}
+
+	output := strings.Join(lines, "\n")
+
+	// case on output type
+	if getEnvFileDest != "" {
+		ioutil.WriteFile(getEnvFileDest, []byte(output), 0700)
+	} else {
+		fmt.Println(output)
+	}
+
+	return nil
+}
+
+func deploySetEnv(client *api.Client) (prefix string, err error) {
+	prefix = fmt.Sprintf("PORTER_%s", strings.Replace(
+		strings.ToUpper(app), "-", "_", -1,
+	))
+
+	envVars, err := getEnvFromRelease()
+
+	if err != nil {
+		return prefix, err
+	}
+
+	// iterate through env and set the environment variables for the process
+	// these are prefixed with PORTER_<RELEASE> to avoid collisions
+	for key, val := range envVars {
+		prefixedKey := fmt.Sprintf("%s_%s", prefix, key)
+
+		err := os.Setenv(prefixedKey, val)
+
+		if err != nil {
+			return prefix, err
+		}
+	}
+
+	return prefix, nil
+}
+
+func getEnvFromRelease() (map[string]string, error) {
+	envConfig, err := getNestedMap(release.Config, "container", "env", "normal")
+
+	// if the field is not found, set envConfig to an empty map; this release has no env set
+	if e := (&NestedMapFieldNotFoundError{}); errors.As(err, &e) {
+		envConfig = make(map[string]interface{})
+	} else if err != nil {
+		return nil, fmt.Errorf("could not get environment variables from release: %s", err.Error())
+	}
+
+	mapEnvConfig := make(map[string]string)
+
+	for key, val := range envConfig {
+		valStr, ok := val.(string)
+
+		if !ok {
+			return nil, fmt.Errorf("could not cast environment variables to object")
+		}
+
+		mapEnvConfig[key] = valStr
+	}
+
+	return mapEnvConfig, nil
+}
+
+type NestedMapFieldNotFoundError struct {
+	Field string
+}
+
+func (e *NestedMapFieldNotFoundError) Error() string {
+	return fmt.Sprintf("could not find field %s in configuration", e.Field)
+}
+
+func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
+	var res map[string]interface{}
+	curr := obj
+
+	for _, field := range fields {
+		objField, ok := curr[field]
+
+		if !ok {
+			return nil, &NestedMapFieldNotFoundError{field}
+		}
+
+		res, ok = objField.(map[string]interface{})
+
+		if !ok {
+			return nil, fmt.Errorf("%s is not a nested object", field)
+		}
+
+		curr = res
+	}
+
+	return res, nil
+}
+
+func deployBuild(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	if release == nil {
+		err := deployInit(resp, client, args)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	// download the repository from remote source into a temp directory
+	dst, err := downloadRepoToDir(client)
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("DEST IS", dst)
+
+	// agent, err := docker.NewAgentFromEnv()
+
+	// if err != nil {
+	// 	return err
+	// }
+
+	// return agent.BuildLocal("./docker/dev.Dockerfile", "test-porter:latest", "/Users/abelanger/porter/porter-server")
+	return nil
+}
+
+func downloadRepoToDir(client *api.Client) (string, error) {
+	resp, err := client.GetRepoZIPDownloadURL(
+		context.Background(),
+		config.Project,
+		release.GitActionConfig,
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	dstDir := filepath.Join(home, ".porter")
+
+	downloader := &github.ZIPDownloader{
+		ZipFolderDest:       dstDir,
+		AssetFolderDest:     dstDir,
+		ZipName:             fmt.Sprintf("%s.zip", strings.Replace(release.GitActionConfig.GitRepo, "/", "-", 1)),
+		RemoveAfterDownload: true,
+	}
+
+	err = downloader.DownloadToFile(resp.URLString)
+
+	if err != nil {
+		return "", fmt.Errorf("Error downloading to file: %s", err.Error())
+	}
+
+	err = downloader.UnzipToDir()
+
+	if err != nil {
+		return "", fmt.Errorf("Error unzipping to directory: %s", err.Error())
+	}
+
+	var res string
+
+	dstFiles, err := ioutil.ReadDir(dstDir)
+
+	for _, info := range dstFiles {
+		if info.Mode().IsDir() && strings.Contains(info.Name(), strings.Replace(release.GitActionConfig.GitRepo, "/", "-", 1)) {
+			res = filepath.Join(dstDir, info.Name())
+		}
+	}
+
+	if res == "" {
+		return "", fmt.Errorf("unzipped file not found on host")
+	}
+
+	return res, nil
+}

+ 5 - 0
cli/cmd/docker.go

@@ -197,6 +197,11 @@ func downloadCredMatchingRelease() error {
 		EntityID:            "porter-dev",
 		RepoName:            "porter",
 		IsPlatformDependent: true,
+		Downloader: &github.ZIPDownloader{
+			ZipFolderDest:   filepath.Join(home, ".porter"),
+			AssetFolderDest: "/usr/local/bin",
+			ZipName:         "docker-credential-porter_latest.zip",
+		},
 	}
 
 	return z.GetRelease(Version)

+ 70 - 0
cli/cmd/docker/builder.go

@@ -0,0 +1,70 @@
+package docker
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/pkg/archive"
+)
+
+// BuildLocal
+func (a *Agent) BuildLocal(dockerfilePath, tag, buildContext string) error {
+	tar, err := archive.TarWithOptions(buildContext, &archive.TarOptions{})
+
+	if err != nil {
+		return err
+	}
+
+	res, err := a.client.ImageBuild(context.Background(), tar, types.ImageBuildOptions{
+		Dockerfile: dockerfilePath,
+		Tags:       []string{tag},
+		Remove:     true,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return readBuildLogs(res.Body)
+}
+
+// TODO -- do something with these build logs (probably stream to Porter)
+type ErrorLine struct {
+	Error       string      `json:"error"`
+	ErrorDetail ErrorDetail `json:"errorDetail"`
+}
+
+type ErrorDetail struct {
+	Message string `json:"message"`
+}
+
+func readBuildLogs(rd io.ReadCloser) error {
+	var lastLine string
+
+	scanner := bufio.NewScanner(rd)
+
+	for scanner.Scan() {
+		lastLine = scanner.Text()
+		fmt.Println(scanner.Text())
+	}
+
+	errLine := &ErrorLine{}
+
+	json.Unmarshal([]byte(lastLine), errLine)
+
+	if errLine.Error != "" {
+		return errors.New(errLine.Error)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return err
+	}
+
+	return nil
+
+}

+ 17 - 40
cli/cmd/github/release.go

@@ -38,6 +38,9 @@ type ZIPReleaseGetter struct {
 
 	// If the asset is platform dependent
 	IsPlatformDependent bool
+
+	// The downloader/unzipper
+	Downloader *ZIPDownloader
 }
 
 // GetLatestRelease downloads the latest .zip release from a given Github repository
@@ -67,7 +70,7 @@ func (z *ZIPReleaseGetter) GetRelease(releaseTag string) error {
 func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
 	fmt.Printf("getting release %s\n", releaseURL)
 
-	err := z.downloadToFile(releaseURL)
+	err := z.Downloader.DownloadToFile(releaseURL)
 
 	fmt.Printf("downloaded release %s to file %s\n", z.AssetName, filepath.Join(z.ZipFolderDest, z.ZipName))
 
@@ -77,7 +80,7 @@ func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
 
 	fmt.Printf("unzipping %s to %s\n", z.AssetName, z.AssetFolderDest)
 
-	err = z.unzipToDir()
+	err = z.Downloader.UnzipToDir()
 
 	return err
 }
@@ -152,45 +155,15 @@ func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) {
 	return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*\.zip`, z.AssetName)), nil
 }
 
-// // DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
-// // it, and adds the binary to the porter directory
-// func DownloadLatestServerRelease(porterDir string) error {
-// 	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
-
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
-
-// 	err = downloadToFile(releaseURL, zipFile)
-
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	err = unzipToDir(zipFile, porterDir)
-
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	staticZipFile := filepath.Join(porterDir, "static_latest.zip")
-
-// 	err = downloadToFile(staticReleaseURL, staticZipFile)
-
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	staticDir := filepath.Join(porterDir, "static")
-
-// 	err = unzipToDir(staticZipFile, staticDir)
+type ZIPDownloader struct {
+	ZipFolderDest   string
+	ZipName         string
+	AssetFolderDest string
 
-// 	return err
-// }
+	RemoveAfterDownload bool
+}
 
-func (z *ZIPReleaseGetter) downloadToFile(url string) error {
+func (z *ZIPDownloader) DownloadToFile(url string) error {
 	// Get the data
 	resp, err := http.Get(url)
 
@@ -215,7 +188,7 @@ func (z *ZIPReleaseGetter) downloadToFile(url string) error {
 	return err
 }
 
-func (z *ZIPReleaseGetter) unzipToDir() error {
+func (z *ZIPDownloader) UnzipToDir() error {
 	r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 	if err != nil {
@@ -268,5 +241,9 @@ func (z *ZIPReleaseGetter) unzipToDir() error {
 		}
 	}
 
+	if z.RemoveAfterDownload {
+		os.Remove(filepath.Join(z.ZipFolderDest, z.ZipName))
+	}
+
 	return nil
 }

+ 56 - 11
cli/cmd/run.go

@@ -49,25 +49,55 @@ func init() {
 func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 	color.New(color.FgGreen).Println("Running", strings.Join(args[1:], " "), "for release", args[0])
 
-	podNames, err := getPods(client, namespace, args[0])
+	podsSimple, err := getPods(client, namespace, args[0])
 
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 	}
 
 	// if length of pods is 0, throw error
-	pod := ""
+	var selectedPod podSimple
 
-	if len(podNames) == 0 {
+	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
-	} else if len(podNames) == 1 {
-		pod = podNames[0]
+	} else if len(podsSimple) == 1 {
+		selectedPod = podsSimple[0]
 	} else {
-		pod, err = utils.PromptSelect("Select the pod:", podNames)
+		podNames := make([]string, 0)
+
+		for _, podSimple := range podsSimple {
+			podNames = append(podNames, podSimple.Name)
+		}
+
+		selectedPodName, err := utils.PromptSelect("Select the pod:", podNames)
 
 		if err != nil {
 			return err
 		}
+
+		// find selected pod
+		for _, podSimple := range podsSimple {
+			if selectedPodName == podSimple.Name {
+				selectedPod = podSimple
+			}
+		}
+	}
+
+	var selectedContainerName string
+
+	// if the selected pod has multiple container, spawn selector
+	if len(selectedPod.ContainerNames) == 0 {
+		return fmt.Errorf("At least one pod must exist in this deployment.")
+	} else if len(selectedPod.ContainerNames) == 1 {
+		selectedContainerName = selectedPod.ContainerNames[0]
+	} else {
+		selectedContainer, err := utils.PromptSelect("Select the container:", selectedPod.ContainerNames)
+
+		if err != nil {
+			return err
+		}
+
+		selectedContainerName = selectedContainer
 	}
 
 	restConf, err := getRESTConfig(client)
@@ -76,7 +106,7 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 
-	return executeRun(restConf, namespace, pod, args[1:])
+	return executeRun(restConf, namespace, selectedPod.Name, selectedContainerName, args[1:])
 }
 
 func getRESTConfig(client *api.Client) (*rest.Config, error) {
@@ -113,7 +143,12 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	return restConf, nil
 }
 
-func getPods(client *api.Client, namespace, releaseName string) ([]string, error) {
+type podSimple struct {
+	Name           string
+	ContainerNames []string
+}
+
+func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, error) {
 	pID := config.Project
 	cID := config.Cluster
 
@@ -123,16 +158,25 @@ func getPods(client *api.Client, namespace, releaseName string) ([]string, error
 		return nil, err
 	}
 
-	res := make([]string, 0)
+	res := make([]podSimple, 0)
 
 	for _, pod := range resp {
-		res = append(res, pod.ObjectMeta.Name)
+		containerNames := make([]string, 0)
+
+		for _, container := range pod.Spec.Containers {
+			containerNames = append(containerNames, container.Name)
+		}
+
+		res = append(res, podSimple{
+			Name:           pod.ObjectMeta.Name,
+			ContainerNames: containerNames,
+		})
 	}
 
 	return res, nil
 }
 
-func executeRun(config *rest.Config, namespace, name string, args []string) error {
+func executeRun(config *rest.Config, namespace, name, container string, args []string) error {
 	restClient, err := rest.RESTClientFor(config)
 
 	if err != nil {
@@ -152,6 +196,7 @@ func executeRun(config *rest.Config, namespace, name string, args []string) erro
 	req.Param("stdin", "true")
 	req.Param("stdout", "true")
 	req.Param("tty", "true")
+	req.Param("container", "sidecar")
 
 	t := term.TTY{
 		In:  os.Stdin,

+ 10 - 0
cli/cmd/server.go

@@ -255,6 +255,11 @@ func downloadMatchingRelease(porterDir string) error {
 		EntityID:            "porter-dev",
 		RepoName:            "porter",
 		IsPlatformDependent: true,
+		Downloader: &github.ZIPDownloader{
+			ZipFolderDest:   porterDir,
+			AssetFolderDest: porterDir,
+			ZipName:         "portersvr_latest.zip",
+		},
 	}
 
 	err := z.GetRelease(Version)
@@ -271,6 +276,11 @@ func downloadMatchingRelease(porterDir string) error {
 		EntityID:            "porter-dev",
 		RepoName:            "porter",
 		IsPlatformDependent: false,
+		Downloader: &github.ZIPDownloader{
+			ZipFolderDest:   porterDir,
+			AssetFolderDest: filepath.Join(porterDir, "static"),
+			ZipName:         "static_latest.zip",
+		},
 	}
 
 	return zStatic.GetRelease(Version)

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "0.2.0"
+var Version string = "v0.2.0"
 
 var versionCmd = &cobra.Command{
 	Use:     "version",

+ 1 - 0
dashboard/package.json

@@ -6,6 +6,7 @@
     "@material-ui/core": "^4.11.3",
     "@types/d3-array": "^2.9.0",
     "@types/d3-time-format": "^3.0.0",
+    "@types/js-yaml": "^4.0.1",
     "@types/lodash": "^4.14.165",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/material-ui": "^0.21.8",

+ 2 - 1
go.mod

@@ -41,6 +41,7 @@ require (
 	github.com/gorilla/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.4.2
+	github.com/hashicorp/go-getter v1.5.3
 	github.com/hashicorp/golang-lru v0.5.3 // indirect
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/itchyny/gojq v0.11.1
@@ -95,4 +96,4 @@ require (
 	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/aws-iam-authenticator v0.5.2
 	sigs.k8s.io/yaml v1.2.0
-)
+)

+ 20 - 0
go.sum

@@ -38,6 +38,7 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
 cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FWV9vssk=
@@ -144,6 +145,7 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
 github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.30.0 h1:7NDwnnQrI1Ivk0bXLzMmuX5ozzOwteHOsAs4druW7gI=
@@ -157,6 +159,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
@@ -182,6 +186,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
+github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -586,6 +591,10 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ3ROU=
+github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI=
 github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
@@ -593,10 +602,14 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
+github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -731,6 +744,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ=
+github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -826,6 +841,7 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -904,6 +920,7 @@ github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
 github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
 github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
@@ -1085,6 +1102,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
+github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -1611,6 +1630,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=

+ 4 - 0
internal/models/gitrepo.go

@@ -76,6 +76,9 @@ type GitActionConfigExternal struct {
 	// The git repo in ${owner}/${repo} form
 	GitRepo string `json:"git_repo"`
 
+	// The git branch to use
+	GitBranch string `json:"git_branch"`
+
 	// The complete image repository uri to pull from
 	ImageRepoURI string `json:"image_repo_uri"`
 
@@ -93,6 +96,7 @@ type GitActionConfigExternal struct {
 func (r *GitActionConfig) Externalize() *GitActionConfigExternal {
 	return &GitActionConfigExternal{
 		GitRepo:        r.GitRepo,
+		GitBranch:      r.GitBranch,
 		ImageRepoURI:   r.ImageRepoURI,
 		GitRepoID:      r.GitRepoID,
 		DockerfilePath: r.DockerfilePath,

+ 41 - 0
server/api/git_repo_handler.go

@@ -266,6 +266,47 @@ func (app *App) HandleGetProcfileContents(w http.ResponseWriter, r *http.Request
 	json.NewEncoder(w).Encode(parsedContents)
 }
 
+type HandleGetRepoZIPDownloadURLResp struct {
+	URLString string `json:"url"`
+}
+
+// HandleGetRepoZIPDownloadURL gets the URL for downloading a zip file from a Github
+// repository
+func (app *App) HandleGetRepoZIPDownloadURL(w http.ResponseWriter, r *http.Request) {
+	tok, err := app.githubTokenFromRequest(r)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
+	owner := chi.URLParam(r, "owner")
+	name := chi.URLParam(r, "name")
+	branch := chi.URLParam(r, "branch")
+
+	ghURL, _, err := client.Repositories.GetArchiveLink(
+		context.TODO(),
+		owner,
+		name,
+		github.Zipball,
+		&github.RepositoryContentGetOptions{
+			Ref: branch,
+		},
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	apiResp := HandleGetRepoZIPDownloadURLResp{
+		URLString: ghURL.String(),
+	}
+
+	json.NewEncoder(w).Encode(apiResp)
+}
+
 // finds the github token given the git repo id and the project id
 func (app *App) githubTokenFromRequest(
 	r *http.Request,

+ 16 - 4
server/api/release_handler.go

@@ -77,9 +77,10 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 // PorterRelease is a helm release with a form attached
 type PorterRelease struct {
 	*release.Release
-	Form          *models.FormYAML `json:"form"`
-	HasMetrics    bool             `json:"has_metrics"`
-	LatestVersion string           `json:"latest_version"`
+	Form            *models.FormYAML                `json:"form"`
+	HasMetrics      bool                            `json:"has_metrics"`
+	LatestVersion   string                          `json:"latest_version"`
+	GitActionConfig *models.GitActionConfigExternal `json:"git_action_config"`
 }
 
 var porterApplications = map[string]string{"web": "", "job": "", "worker": ""}
@@ -161,7 +162,7 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 		HelmRelease:   release,
 	}
 
-	res := &PorterRelease{release, nil, false, ""}
+	res := &PorterRelease{release, nil, false, "", nil}
 
 	for _, file := range release.Chart.Files {
 		if strings.Contains(file.Name, "form.yaml") {
@@ -212,6 +213,17 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	// if the release was created from this server,
+	modelRelease, err := app.Repo.Release.ReadRelease(form.Cluster.ID, release.Name, release.Namespace)
+
+	if modelRelease != nil {
+		gitAction := modelRelease.GitActionConfig
+
+		if gitAction.ID != 0 {
+			res.GitActionConfig = gitAction.Externalize()
+		}
+	}
+
 	if err := json.NewEncoder(w).Encode(res); err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return

+ 14 - 0
server/router/router.go

@@ -1056,6 +1056,20 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
+			r.Method(
+				"GET",
+				"/projects/{project_id}/gitrepos/{git_repo_id}/repos/{kind}/{owner}/{name}/{branch}/tarball_url",
+				auth.DoesUserHaveProjectAccess(
+					auth.DoesUserHaveGitRepoAccess(
+						requestlog.NewHandler(a.HandleGetRepoZIPDownloadURL, l),
+						mw.URLParam,
+						mw.URLParam,
+					),
+					mw.URLParam,
+					mw.ReadAccess,
+				),
+			)
+
 			// /api/projects/{project_id}/k8s routes
 			r.Method(
 				"GET",