소스 검색

temporary deploy flow: download remote repository + docker builds

Alexander Belanger 5 년 전
부모
커밋
1f6b4bfe32

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

@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"net/http"
 
 
 	"github.com/porter-dev/porter/internal/models"
 	"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
 // ListGitRepoResponse is the list of Git repo integrations for a project
@@ -39,3 +40,43 @@ func (c *Client) ListGitRepos(
 
 
 	return *bodyResp, nil
 	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,
 	ctx context.Context,
 	projectID, clusterID uint,
 	projectID, clusterID uint,
 	namespace, name string,
 	namespace, name string,
-) (GetReleaseResponse, error) {
+) (*GetReleaseResponse, error) {
 	cl := fmt.Sprintf("%d", clusterID)
 	cl := fmt.Sprintf("%d", clusterID)
 
 
 	req, err := http.NewRequest(
 	req, err := http.NewRequest(
@@ -124,7 +124,7 @@ func (c *Client) GetRelease(
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return *bodyResp, nil
+	return bodyResp, nil
 }
 }
 
 
 // GetReleaseAllPodsResponse is the list of all pods for a given Helm release
 // 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())
 	user, _ := client.AuthCheck(context.Background())
 
 
 	if user != nil {
 	if user != nil {
-		color.Yellow(getToken())
 		color.Yellow("You are already logged in. If you'd like to log out, run \"porter auth logout\".")
 		color.Yellow("You are already logged in. If you'd like to log out, run \"porter auth logout\".")
 		return nil
 		return nil
 	}
 	}

+ 282 - 9
cli/cmd/deploy.go

@@ -1,10 +1,17 @@
 package cmd
 package cmd
 
 
 import (
 import (
+	"context"
+	"errors"
+	"fmt"
+	"io/ioutil"
 	"os"
 	"os"
+	"path/filepath"
+	"strings"
 
 
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/github"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
@@ -13,7 +20,7 @@ var app = ""
 // deployCmd represents the "porter deploy" base command when called
 // deployCmd represents the "porter deploy" base command when called
 // without any subcommands
 // without any subcommands
 var deployCmd = &cobra.Command{
 var deployCmd = &cobra.Command{
-	Use:   "app deploy",
+	Use:   "deploy",
 	Short: "Builds and deploys a specified application given by the --app flag.",
 	Short: "Builds and deploys a specified application given by the --app flag.",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, deploy)
 		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() {
 func init() {
 	rootCmd.AddCommand(deployCmd)
 	rootCmd.AddCommand(deployCmd)
 
 
@@ -33,25 +91,240 @@ func init() {
 		"",
 		"",
 		"Application in the Porter dashboard",
 		"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)
 	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
 // deployInit first reads the release given by the --app or the --job flag. It then
 // configures docker with the registries linked to the project.
 // configures docker with the registries linked to the project.
 func deployInit(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
 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)
 	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",
 		EntityID:            "porter-dev",
 		RepoName:            "porter",
 		RepoName:            "porter",
 		IsPlatformDependent: true,
 		IsPlatformDependent: true,
+		Downloader: &github.ZIPDownloader{
+			ZipFolderDest:   filepath.Join(home, ".porter"),
+			AssetFolderDest: "/usr/local/bin",
+			ZipName:         "docker-credential-porter_latest.zip",
+		},
 	}
 	}
 
 
 	return z.GetRelease(Version)
 	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
 	// If the asset is platform dependent
 	IsPlatformDependent bool
 	IsPlatformDependent bool
+
+	// The downloader/unzipper
+	Downloader *ZIPDownloader
 }
 }
 
 
 // GetLatestRelease downloads the latest .zip release from a given Github repository
 // 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 {
 func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
 	fmt.Printf("getting release %s\n", releaseURL)
 	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))
 	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)
 	fmt.Printf("unzipping %s to %s\n", z.AssetName, z.AssetFolderDest)
 
 
-	err = z.unzipToDir()
+	err = z.Downloader.UnzipToDir()
 
 
 	return err
 	return err
 }
 }
@@ -152,45 +155,15 @@ func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) {
 	return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*\.zip`, z.AssetName)), nil
 	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
 	// Get the data
 	resp, err := http.Get(url)
 	resp, err := http.Get(url)
 
 
@@ -215,7 +188,7 @@ func (z *ZIPReleaseGetter) downloadToFile(url string) error {
 	return err
 	return err
 }
 }
 
 
-func (z *ZIPReleaseGetter) unzipToDir() error {
+func (z *ZIPDownloader) UnzipToDir() error {
 	r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName))
 	r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 
 	if err != nil {
 	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
 	return nil
 }
 }

+ 56 - 11
cli/cmd/run.go

@@ -49,25 +49,55 @@ func init() {
 func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 	color.New(color.FgGreen).Println("Running", strings.Join(args[1:], " "), "for release", args[0])
 	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 {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 	}
 	}
 
 
 	// if length of pods is 0, throw 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.")
 		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 {
 	} 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 {
 		if err != nil {
 			return err
 			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)
 	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 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) {
 func getRESTConfig(client *api.Client) (*rest.Config, error) {
@@ -113,7 +143,12 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	return restConf, nil
 	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
 	pID := config.Project
 	cID := config.Cluster
 	cID := config.Cluster
 
 
@@ -123,16 +158,25 @@ func getPods(client *api.Client, namespace, releaseName string) ([]string, error
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	res := make([]string, 0)
+	res := make([]podSimple, 0)
 
 
 	for _, pod := range resp {
 	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
 	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)
 	restClient, err := rest.RESTClientFor(config)
 
 
 	if err != nil {
 	if err != nil {
@@ -152,6 +196,7 @@ func executeRun(config *rest.Config, namespace, name string, args []string) erro
 	req.Param("stdin", "true")
 	req.Param("stdin", "true")
 	req.Param("stdout", "true")
 	req.Param("stdout", "true")
 	req.Param("tty", "true")
 	req.Param("tty", "true")
+	req.Param("container", "sidecar")
 
 
 	t := term.TTY{
 	t := term.TTY{
 		In:  os.Stdin,
 		In:  os.Stdin,

+ 10 - 0
cli/cmd/server.go

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

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 )
 
 
 // Version will be linked by an ldflag during build
 // 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{
 var versionCmd = &cobra.Command{
 	Use:     "version",
 	Use:     "version",

+ 1 - 0
dashboard/package.json

@@ -6,6 +6,7 @@
     "@material-ui/core": "^4.11.3",
     "@material-ui/core": "^4.11.3",
     "@types/d3-array": "^2.9.0",
     "@types/d3-array": "^2.9.0",
     "@types/d3-time-format": "^3.0.0",
     "@types/d3-time-format": "^3.0.0",
+    "@types/js-yaml": "^4.0.1",
     "@types/lodash": "^4.14.165",
     "@types/lodash": "^4.14.165",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/material-ui": "^0.21.8",
     "@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/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.4.2
 	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/hashicorp/golang-lru v0.5.3 // indirect
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/itchyny/gojq v0.11.1
 	github.com/itchyny/gojq v0.11.1
@@ -95,4 +96,4 @@ require (
 	rsc.io/letsencrypt v0.0.3 // indirect
 	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/aws-iam-authenticator v0.5.2
 	sigs.k8s.io/aws-iam-authenticator v0.5.2
 	sigs.k8s.io/yaml v1.2.0
 	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.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.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.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=
 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=
 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=
 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/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-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.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 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 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=
 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.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 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/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/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 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
 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 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 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/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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 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/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 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 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 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 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=
 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-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 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 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-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-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.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-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-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 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=
 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.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 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/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.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.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=
 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.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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 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-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 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 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 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 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.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/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 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=
 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/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 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/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 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.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 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.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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 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
 	// The git repo in ${owner}/${repo} form
 	GitRepo string `json:"git_repo"`
 	GitRepo string `json:"git_repo"`
 
 
+	// The git branch to use
+	GitBranch string `json:"git_branch"`
+
 	// The complete image repository uri to pull from
 	// The complete image repository uri to pull from
 	ImageRepoURI string `json:"image_repo_uri"`
 	ImageRepoURI string `json:"image_repo_uri"`
 
 
@@ -93,6 +96,7 @@ type GitActionConfigExternal struct {
 func (r *GitActionConfig) Externalize() *GitActionConfigExternal {
 func (r *GitActionConfig) Externalize() *GitActionConfigExternal {
 	return &GitActionConfigExternal{
 	return &GitActionConfigExternal{
 		GitRepo:        r.GitRepo,
 		GitRepo:        r.GitRepo,
+		GitBranch:      r.GitBranch,
 		ImageRepoURI:   r.ImageRepoURI,
 		ImageRepoURI:   r.ImageRepoURI,
 		GitRepoID:      r.GitRepoID,
 		GitRepoID:      r.GitRepoID,
 		DockerfilePath: r.DockerfilePath,
 		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)
 	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
 // finds the github token given the git repo id and the project id
 func (app *App) githubTokenFromRequest(
 func (app *App) githubTokenFromRequest(
 	r *http.Request,
 	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
 // PorterRelease is a helm release with a form attached
 type PorterRelease struct {
 type PorterRelease struct {
 	*release.Release
 	*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": ""}
 var porterApplications = map[string]string{"web": "", "job": "", "worker": ""}
@@ -161,7 +162,7 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 		HelmRelease:   release,
 		HelmRelease:   release,
 	}
 	}
 
 
-	res := &PorterRelease{release, nil, false, ""}
+	res := &PorterRelease{release, nil, false, "", nil}
 
 
 	for _, file := range release.Chart.Files {
 	for _, file := range release.Chart.Files {
 		if strings.Contains(file.Name, "form.yaml") {
 		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 {
 	if err := json.NewEncoder(w).Encode(res); err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
 		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
 			// /api/projects/{project_id}/k8s routes
 			r.Method(
 			r.Method(
 				"GET",
 				"GET",