Преглед изворни кода

Merge pull request #500 from porter-dev/beta.3.cli-shell-access

CLI remote execution option
sunguroku пре 5 година
родитељ
комит
1f7380e10b

+ 79 - 0
cli/cmd/api/k8s.go

@@ -46,3 +46,82 @@ func (c *Client) GetK8sNamespaces(
 
 	return bodyResp, nil
 }
+
+// GetKubeconfigResponse is the list of namespaces returned when a
+// user has successfully authenticated
+type GetKubeconfigResponse struct {
+	Kubeconfig []byte `json:"kubeconfig"`
+}
+
+// GetK8sNamespaces gets a namespaces list in a k8s cluster
+func (c *Client) GetKubeconfig(
+	ctx context.Context,
+	projectID uint,
+	clusterID uint,
+) (*GetKubeconfigResponse, error) {
+	cl := fmt.Sprintf("%d", clusterID)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/k8s/kubeconfig?"+url.Values{
+			"cluster_id": []string{cl},
+		}.Encode(), c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &GetKubeconfigResponse{}
+
+	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
+}
+
+// GetReleaseAllPodsResponse is the list of all pods for a given Helm release
+type GetReleaseAllPodsResponse []v1.Pod
+
+// GetK8sAllPods gets all pods for a given release
+func (c *Client) GetK8sAllPods(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace, name string,
+) (GetReleaseAllPodsResponse, error) {
+	cl := fmt.Sprintf("%d", clusterID)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/releases/%s/0/pods/all?"+url.Values{
+			"cluster_id": []string{cl},
+			"namespace":  []string{namespace},
+			"storage":    []string{"secret"},
+		}.Encode(), c.BaseURL, projectID, name),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &GetReleaseAllPodsResponse{}
+
+	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
+}

+ 188 - 0
cli/cmd/run.go

@@ -0,0 +1,188 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/remotecommand"
+	"k8s.io/kubectl/pkg/util/term"
+)
+
+var namespace string
+
+// runCmd represents the "porter run" base command when called
+// without any subcommands
+var runCmd = &cobra.Command{
+	Use:   "run [release] -- COMMAND [args...]",
+	Args:  cobra.MinimumNArgs(2),
+	Short: "Runs a command inside a connected cluster container.",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, run)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(runCmd)
+
+	runCmd.PersistentFlags().StringVar(
+		&host,
+		"host",
+		getHost(),
+		"host url of Porter instance",
+	)
+
+	runCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"default",
+		"namespace of release to connect to",
+	)
+}
+
+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])
+
+	if err != nil {
+		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
+	}
+
+	// if length of pods is 0, throw error
+	pod := ""
+
+	if len(podNames) == 0 {
+		return fmt.Errorf("At least one pod must exist in this deployment.")
+	} else if len(podNames) == 1 {
+		pod = podNames[0]
+	} else {
+		pod, err = utils.PromptSelect("Select the pod:", podNames)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	restConf, err := getRESTConfig(client)
+
+	if err != nil {
+		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
+	}
+
+	return executeRun(restConf, namespace, pod, args[1:])
+}
+
+func getRESTConfig(client *api.Client) (*rest.Config, error) {
+	pID := getProjectID()
+	cID := getClusterID()
+
+	kubeResp, err := client.GetKubeconfig(context.TODO(), pID, cID)
+
+	if err != nil {
+		return nil, err
+	}
+
+	kubeBytes := kubeResp.Kubeconfig
+
+	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
+
+	if err != nil {
+		return nil, err
+	}
+
+	restConf, err := cmdConf.ClientConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	restConf.GroupVersion = &schema.GroupVersion{
+		Group:   "api",
+		Version: "v1",
+	}
+
+	restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
+
+	return restConf, nil
+}
+
+func getPods(client *api.Client, namespace, releaseName string) ([]string, error) {
+	pID := getProjectID()
+	cID := getClusterID()
+
+	resp, err := client.GetK8sAllPods(context.TODO(), pID, cID, namespace, releaseName)
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := make([]string, 0)
+
+	for _, pod := range resp {
+		res = append(res, pod.ObjectMeta.Name)
+	}
+
+	return res, nil
+}
+
+func executeRun(config *rest.Config, namespace, name string, args []string) error {
+	restClient, err := rest.RESTClientFor(config)
+
+	if err != nil {
+		return err
+	}
+
+	req := restClient.Post().
+		Resource("pods").
+		Name(name).
+		Namespace(namespace).
+		SubResource("exec")
+
+	// req.Param("container", "web")
+	for _, arg := range args {
+		req.Param("command", arg)
+	}
+	req.Param("stdin", "true")
+	req.Param("stdout", "true")
+	req.Param("tty", "true")
+
+	t := term.TTY{
+		In:  os.Stdin,
+		Out: os.Stdout,
+	}
+
+	fn := func() error {
+		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+
+		if err != nil {
+			return err
+		}
+
+		return exec.Stream(remotecommand.StreamOptions{
+			Stdin:  os.Stdin,
+			Stdout: os.Stdout,
+			Stderr: os.Stderr,
+			Tty:    true,
+		})
+	}
+
+	if err := t.Safe(fn); err != nil {
+		return err
+	}
+
+	return err
+}

+ 24 - 0
cli/cmd/utils/prompt.go

@@ -7,6 +7,7 @@ import (
 	"os"
 	"strings"
 
+	"github.com/AlecAivazis/survey/v2"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
@@ -55,3 +56,26 @@ func PromptPasswordWithConfirmation() (string, error) {
 
 	return pw, nil
 }
+
+type selectAnswer struct {
+	Response string `survey:"response"`
+}
+
+func PromptSelect(prompt string, options []string) (string, error) {
+	var qs = []*survey.Question{
+		{
+			Name: "response",
+			Prompt: &survey.Select{
+				Message: prompt,
+				Options: options,
+				Default: options[0],
+			},
+		},
+	}
+
+	ans := &selectAnswer{}
+
+	err := survey.Ask(qs, ans)
+
+	return ans.Response, err
+}

+ 2 - 0
go.mod

@@ -4,6 +4,7 @@ go 1.15
 
 require (
 	cloud.google.com/go v0.65.0
+	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/Azure/go-autorest/autorest v0.11.1 // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
@@ -88,6 +89,7 @@ require (
 	k8s.io/client-go v0.18.8
 	k8s.io/helm v2.16.12+incompatible
 	k8s.io/klog/v2 v2.2.0 // indirect
+	k8s.io/kubectl v0.18.8
 	k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect
 	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/aws-iam-authenticator v0.5.2

+ 15 - 2
go.sum

@@ -33,6 +33,9 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 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 v1.8.8 h1:Y4yypp763E8cbqb5RBqZhGgkCFLRFnbRBHrxnpMMsgQ=
+github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FWV9vssk=
+github.com/AlecAivazis/survey/v2 v2.2.9/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@@ -86,6 +89,7 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tT
 github.com/Microsoft/hcsshim v0.8.7 h1:ptnOoufxGSzauVTsdE+wMYnCWA301PdoN4xg5oRdZpg=
 github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
 github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -564,6 +568,7 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
 github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
@@ -671,6 +676,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -682,6 +689,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
@@ -751,6 +759,8 @@ github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5
 github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
@@ -926,14 +936,14 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE=
+github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
 github.com/sendgrid/rest v1.0.2 h1:xdfALkR1m9eqf41/zEnUmV0fw4b31ZzGZ4Dj5f2/w04=
 github.com/sendgrid/rest v2.6.3+incompatible h1:h/uruXAzKxVyDDIQX/MkQI73p/gsdpEnb5q2wxSvTsA=
 github.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
 github.com/sendgrid/sendgrid-go v1.2.0 h1:2K3teZdhaPe12ftFyFL4AWDH4QmNPc+sCi6mWFx5+oo=
 github.com/sendgrid/sendgrid-go v3.8.0+incompatible h1:7yoUFMwT+jDI2ArBpC6zvtuQj1RUyYfCDl7zZea3XV4=
 github.com/sendgrid/sendgrid-go v3.8.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
-github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE=
-github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
@@ -982,6 +992,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -1065,6 +1076,7 @@ golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -1205,6 +1217,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 2 - 2
internal/kubernetes/config.go

@@ -181,7 +181,7 @@ func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientCo
 		return clientcmd.NewClientConfigFromBytes(kubeAuth.Kubeconfig)
 	}
 
-	apiConfig, err := conf.createRawConfigFromCluster()
+	apiConfig, err := conf.CreateRawConfigFromCluster()
 
 	if err != nil {
 		return nil, err
@@ -200,7 +200,7 @@ func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientCo
 	return config, nil
 }
 
-func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error) {
+func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error) {
 	cluster := conf.Cluster
 
 	apiConfig := &api.Config{}

+ 16 - 3
internal/registry/registry.go

@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
@@ -408,11 +409,23 @@ func (r *Registry) createECRRepository(
 
 	svc := ecr.New(sess)
 
-	_, err = svc.CreateRepository(&ecr.CreateRepositoryInput{
-		RepositoryName: &name,
+	// determine if repository already exists
+	_, err = svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
+		RepositoryNames: []*string{&name},
 	})
 
-	return err
+	// if the repository was not found, create it
+	if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryNotFoundException {
+		_, err = svc.CreateRepository(&ecr.CreateRepositoryInput{
+			RepositoryName: &name,
+		})
+
+		return err
+	} else if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // ListImages lists the images for an image repository

+ 6 - 1
internal/templater/utils/values.go

@@ -24,8 +24,13 @@ func MergeYAML(base, override []byte) (map[string]interface{}, error) {
 
 // CoalesceValues replaces arrays and scalar values, merges maps
 func CoalesceValues(base, override map[string]interface{}) map[string]interface{} {
-	for key, val := range base {
+	if base == nil && override != nil {
+		return override
+	} else if override == nil {
+		return base
+	}
 
+	for key, val := range base {
 		if oVal, ok := override[key]; ok {
 			if oVal == nil {
 				delete(override, key)

+ 42 - 0
server/api/k8s_handler.go

@@ -13,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/tools/clientcmd"
 )
 
 // Enumeration of k8s API error codes, represented as int64
@@ -644,3 +645,44 @@ func (app *App) HandleGetPodMetrics(w http.ResponseWriter, r *http.Request) {
 
 	fmt.Fprint(w, string(rawQuery))
 }
+
+type KubeconfigResponse struct {
+	Kubeconfig []byte `json:"kubeconfig"`
+}
+
+func (app *App) HandleGetTemporaryKubeconfig(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// get the API config
+	apiConf, err := form.OutOfClusterConfig.CreateRawConfigFromCluster()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	bytes, err := clientcmd.Write(*apiConf)
+	res := &KubeconfigResponse{
+		Kubeconfig: bytes,
+	}
+
+	if err := json.NewEncoder(w).Encode(res); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+}

+ 152 - 0
server/api/release_handler.go

@@ -12,6 +12,8 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
 	"helm.sh/helm/v3/pkg/release"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
@@ -395,6 +397,156 @@ func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Reque
 	}
 }
 
+// HandleGetReleaseAllPods retrieves all pods that are associated with a given release.
+func (app *App) HandleGetReleaseAllPods(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+	revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
+
+	form := &forms.GetReleaseForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
+			},
+		},
+		Name:     name,
+		Revision: int(revision),
+	}
+
+	agent, err := app.getAgentFromQueryParams(
+		w,
+		r,
+		form.ReleaseForm,
+		form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
+	)
+
+	// errors are handled in app.getAgentFromQueryParams
+	if err != nil {
+		return
+	}
+
+	release, err := agent.GetRelease(form.Name, form.Revision)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusNotFound, HTTPError{
+			Code:   ErrReleaseReadData,
+			Errors: []string{"release not found"},
+		}, w)
+
+		return
+	}
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	k8sForm := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
+		},
+	}
+
+	k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+	k8sForm.DefaultNamespace = form.ReleaseForm.Namespace
+
+	// validate the form
+	if err := app.validator.Struct(k8sForm); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new kubernetes agent
+	var k8sAgent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		k8sAgent = app.TestAgents.K8sAgent
+	} else {
+		k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
+	}
+
+	yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
+	controllers := grapher.ParseControllers(yamlArr)
+	pods := make([]v1.Pod, 0)
+
+	// get current status of each controller
+	for _, c := range controllers {
+		var selector *metav1.LabelSelector
+
+		switch c.Kind {
+		case "Deployment":
+			rc, err := k8sAgent.GetDeployment(c)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			selector = rc.Spec.Selector
+		case "StatefulSet":
+			rc, err := k8sAgent.GetStatefulSet(c)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			selector = rc.Spec.Selector
+		case "DaemonSet":
+			rc, err := k8sAgent.GetDaemonSet(c)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			selector = rc.Spec.Selector
+		case "ReplicaSet":
+			rc, err := k8sAgent.GetReplicaSet(c)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			selector = rc.Spec.Selector
+		case "CronJob":
+			rc, err := k8sAgent.GetCronJob(c)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			selector = rc.Spec.JobTemplate.Spec.Selector
+		}
+
+		selectors := make([]string, 0)
+
+		for key, val := range selector.MatchLabels {
+			selectors = append(selectors, key+"="+val)
+		}
+
+		podList, err := k8sAgent.GetPodsByLabel(strings.Join(selectors, ","))
+
+		if err != nil {
+			app.handleErrorDataRead(err, w)
+			return
+		}
+
+		pods = append(pods, podList.Items...)
+	}
+
+	if err := json.NewEncoder(w).Encode(pods); err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+}
+
 // HandleListReleaseHistory retrieves a history of releases based on a release name
 func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")

+ 28 - 0
server/router/router.go

@@ -896,6 +896,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/releases/{name}/{revision}/pods/all",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleGetReleaseAllPods, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/releases/{name}/history",
@@ -1084,6 +1098,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/k8s/kubeconfig",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleGetTemporaryKubeconfig, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/k8s/prometheus/detect",