Przeglądaj źródła

working version of porter create

Alexander Belanger 4 lat temu
rodzic
commit
82d23910c0

+ 59 - 0
cli/cmd/api/deploy.go

@@ -119,3 +119,62 @@ func (c *Client) UpdateBatchImage(
 
 	return nil
 }
+
+type DeployTemplateGitAction struct {
+	GitRepo        string            `json:"git_repo"`
+	GitBranch      string            `json:"git_branch"`
+	ImageRepoURI   string            `json:"image_repo_uri"`
+	DockerfilePath string            `json:"dockerfile_path"`
+	FolderPath     string            `json:"folder_path"`
+	GitRepoID      uint              `json:"git_repo_id"`
+	BuildEnv       map[string]string `json:"env"`
+	RegistryID     uint              `json:"registry_id"`
+}
+
+type DeployTemplateRequest struct {
+	TemplateName string                   `json:"templateName"`
+	ImageURL     string                   `json:"imageURL"`
+	FormValues   map[string]interface{}   `json:"formValues"`
+	Namespace    string                   `json:"namespace"`
+	Name         string                   `json:"name"`
+	GitAction    *DeployTemplateGitAction `json:"github_action"`
+}
+
+func (c *Client) DeployTemplate(
+	ctx context.Context,
+	projID, clusterID uint,
+	templateName string,
+	templateVersion string,
+	deployReq *DeployTemplateRequest,
+) error {
+	data, err := json.Marshal(deployReq)
+
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/deploy/%s/%s?"+url.Values{
+			"cluster_id": []string{fmt.Sprintf("%d", clusterID)},
+			"storage":    []string{"secret"},
+		}.Encode(), c.BaseURL, projID, templateName, templateVersion),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}

+ 60 - 0
cli/cmd/api/domain.go

@@ -0,0 +1,60 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type CreateDNSRecordRequest struct {
+	ReleaseName string `json:"release_name"`
+}
+
+// CreateDNSRecordResponse is the DNS record that was created
+type CreateDNSRecordResponse models.DNSRecordExternal
+
+// CreateGithubAction creates a Github action with basic authentication
+func (c *Client) CreateDNSRecord(
+	ctx context.Context,
+	projectID, clusterID uint,
+	createDNS *CreateDNSRecordRequest,
+) (*CreateDNSRecordResponse, error) {
+	data, err := json.Marshal(createDNS)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf(
+			"%s/projects/%d/k8s/subdomain?cluster_id=%d",
+			c.BaseURL,
+			projectID,
+			clusterID,
+		),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+
+	res := &CreateDNSRecordResponse{}
+
+	if httpErr, err := c.sendRequest(req, res, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return res, nil
+}

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

@@ -80,3 +80,35 @@ func (c *Client) GetRepoZIPDownloadURL(
 
 	return bodyResp, nil
 }
+
+// ListGitRepoResponse is the list of Git repo integrations for a project
+type ListGithubReposResponse []*api.Repo
+
+// ListGitRepos returns a list of Git repos for a project
+func (c *Client) ListGithubRepos(
+	ctx context.Context,
+	projectID, gitRepoID uint,
+) (ListGithubReposResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/gitrepos/%d/repos", c.BaseURL, projectID, gitRepoID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListGithubReposResponse{}
+
+	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
+}

+ 9 - 4
cli/cmd/api/github_action.go

@@ -11,10 +11,15 @@ import (
 // CreateGithubActionRequest represents the accepted fields for creating
 // a Github action
 type CreateGithubActionRequest struct {
-	GitRepo        string `json:"git_repo"`
-	ImageRepoURI   string `json:"image_repo_uri"`
-	DockerfilePath string `json:"dockerfile_path"`
-	GitRepoID      uint   `json:"git_repo_id"`
+	ReleaseID      uint              `json:"release_id" form:"required"`
+	GitRepo        string            `json:"git_repo" form:"required"`
+	GitBranch      string            `json:"git_branch"`
+	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
+	DockerfilePath string            `json:"dockerfile_path"`
+	FolderPath     string            `json:"folder_path"`
+	GitRepoID      uint              `json:"git_repo_id" form:"required"`
+	BuildEnv       map[string]string `json:"env"`
+	RegistryID     uint              `json:"registry_id"`
 }
 
 // CreateGithubAction creates a Github action with basic authentication

+ 39 - 0
cli/cmd/api/registry.go

@@ -472,3 +472,42 @@ func (c *Client) ListImages(
 
 	return *bodyResp, nil
 }
+
+type CreateRepositoryRequest struct {
+	ImageRepoURI string `json:"image_repo_uri"`
+}
+
+// CreateECR creates an Elastic Container Registry integration
+func (c *Client) CreateRepository(
+	ctx context.Context,
+	projectID, regID uint,
+	createRepo *CreateRepositoryRequest,
+) error {
+	data, err := json.Marshal(createRepo)
+
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/registries/%d/repository", c.BaseURL, projectID, regID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, nil, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}

+ 66 - 0
cli/cmd/api/template.go

@@ -0,0 +1,66 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/models"
+)
+
+func (c *Client) ListTemplates(
+	ctx context.Context,
+) ([]*models.PorterChartList, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/templates", c.BaseURL),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+
+	bodyResp := make([]*models.PorterChartList, 0)
+
+	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
+}
+
+func (c *Client) GetTemplate(
+	ctx context.Context,
+	name, version string,
+) (*models.PorterChartRead, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/templates/%s/%s", c.BaseURL, name, version),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+
+	bodyResp := &models.PorterChartRead{}
+
+	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
+}

+ 233 - 0
cli/cmd/create.go

@@ -0,0 +1,233 @@
+package cmd
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/deploy"
+	"github.com/porter-dev/porter/cli/cmd/gitutils"
+	"github.com/spf13/cobra"
+	"sigs.k8s.io/yaml"
+)
+
+// createCmd represents the "porter create" base command when called
+// without any subcommands
+var createCmd = &cobra.Command{
+	Use:   "create [kind]",
+	Args:  cobra.ExactArgs(1),
+	Short: "TODO.",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, createFull)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var name string
+var values string
+var source string
+
+func init() {
+	rootCmd.AddCommand(createCmd)
+
+	createCmd.PersistentFlags().StringVar(
+		&name,
+		"name",
+		"",
+		"Name of the new application/job/worker.",
+	)
+
+	createCmd.MarkPersistentFlagRequired("name")
+
+	createCmd.PersistentFlags().BoolVar(
+		&local,
+		"local",
+		true,
+		"Whether local context should be used for build",
+	)
+
+	createCmd.PersistentFlags().StringVarP(
+		&localPath,
+		"path",
+		"p",
+		".",
+		"If local build, the path to the build directory",
+	)
+
+	createCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"default",
+		"Namespace of the application",
+	)
+
+	createCmd.PersistentFlags().StringVarP(
+		&values,
+		"values",
+		"v",
+		"",
+		"Filepath to a values.yaml file",
+	)
+
+	createCmd.PersistentFlags().StringVar(
+		&dockerfile,
+		"dockerfile",
+		"",
+		"the path to the dockerfile",
+	)
+
+	createCmd.PersistentFlags().StringVar(
+		&method,
+		"method",
+		"",
+		"the build method to use (\"docker\" or \"pack\")",
+	)
+
+	createCmd.PersistentFlags().StringVar(
+		&source,
+		"source",
+		"local",
+		"the type of source (\"local\" or \"github\")",
+	)
+}
+
+var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
+
+func createFull(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	// check the kind
+	if _, exists := supportedKinds[args[0]]; !exists {
+		return fmt.Errorf("%s is not a supported type: specify web, job, or worker", args[0])
+	}
+
+	// read the values if necessary
+	valuesObj, err := readValuesFile()
+
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Printf("Creating %s release: %s\n", args[0], name)
+
+	fullPath, err := filepath.Abs(localPath)
+
+	if err != nil {
+		return err
+	}
+
+	var buildMethod deploy.DeployBuildType
+
+	if method != "" {
+		buildMethod = deploy.DeployBuildType(method)
+	} else if dockerfile != "" {
+		buildMethod = deploy.DeployBuildTypeDocker
+	}
+
+	createAgent := &deploy.CreateAgent{
+		Client: client,
+		CreateOpts: &deploy.CreateOpts{
+			SharedOpts: &deploy.SharedOpts{
+				ProjectID:       config.Project,
+				ClusterID:       config.Cluster,
+				Namespace:       namespace,
+				LocalPath:       fullPath,
+				LocalDockerfile: dockerfile,
+				Method:          buildMethod,
+			},
+			Kind:        args[0],
+			ReleaseName: name,
+		},
+	}
+
+	if source == "local" {
+		subdomain, err := createAgent.CreateFromDocker(valuesObj)
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Your web application is ready at: %s\n", subdomain)
+	} else {
+		return createFromGithub(createAgent, valuesObj)
+	}
+
+	return nil
+}
+
+func createFromGithub(createAgent *deploy.CreateAgent, overrideValues map[string]interface{}) error {
+	fullPath, err := filepath.Abs(localPath)
+
+	if err != nil {
+		return err
+	}
+
+	_, err = gitutils.GitDirectory(fullPath)
+
+	if err != nil {
+		return err
+	}
+
+	remote, gitBranch, err := gitutils.GetRemoteBranch(fullPath)
+
+	if err != nil {
+		return err
+	}
+
+	ok, remoteRepo := gitutils.ParseGithubRemote(remote)
+
+	if !ok {
+		return fmt.Errorf("remote is not a Github repository")
+	}
+
+	subdomain, err := createAgent.CreateFromGithub(&deploy.GithubOpts{
+		Branch: gitBranch,
+		Repo:   remoteRepo,
+	}, overrideValues)
+
+	color.New(color.FgGreen).Printf("Your web application is ready at: %s\n", subdomain)
+
+	return err
+}
+
+func readValuesFile() (map[string]interface{}, error) {
+	res := make(map[string]interface{})
+
+	if values == "" {
+		return res, nil
+	}
+
+	valuesFilePath, err := filepath.Abs(values)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if info, err := os.Stat(valuesFilePath); os.IsNotExist(err) || info.IsDir() {
+		return nil, fmt.Errorf("values file does not exist or is a directory")
+	}
+
+	reader, err := os.Open(valuesFilePath)
+
+	if err != nil {
+		return nil, err
+	}
+
+	bytes, err := ioutil.ReadAll(reader)
+
+	if err != nil {
+		return nil, err
+	}
+
+	err = yaml.Unmarshal(bytes, &res)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}

+ 0 - 53
cli/cmd/create/create.go

@@ -1,53 +0,0 @@
-package create
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-
-	"github.com/porter-dev/porter/cli/cmd/api"
-	"github.com/porter-dev/porter/cli/cmd/docker"
-)
-
-// CreateAgent handles the creation of a new application on Porter
-type CreateAgent struct {
-	client *api.Client
-	agent  *docker.Agent
-	opts   *CreateOpts
-}
-
-// CreateOpts are the options for creating a new CreateAgent
-type CreateOpts struct {
-	ProjectID uint
-	ClusterID uint
-	Namespace string
-}
-
-func (c *CreateAgent) CreateFromDocker() error {
-
-	// read values from local file
-
-	// overwrite with docker image repository and tag
-
-	// call subdomain creation if necessary
-
-	return nil
-}
-
-type CreateConfig struct {
-	DockerfilePath string
-}
-
-func (c *CreateAgent) DetectConfig(buildPath string) (*CreateConfig, error) {
-	// detect if there is a dockerfile at the path `./Dockerfile`
-	dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
-
-	if info, err := os.Stat(dockerFilePath); !os.IsNotExist(err) && !info.IsDir() {
-		// path/to/whatever does not exist
-		return &CreateConfig{
-			DockerfilePath: dockerFilePath,
-		}, nil
-	}
-
-	return nil, fmt.Errorf("no supported build configuration detected")
-}

+ 10 - 8
cli/cmd/deploy.go

@@ -369,14 +369,16 @@ func deployGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
 
 	// initialize the deploy agent
 	return deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
-		ProjectID:       config.Project,
-		ClusterID:       config.Cluster,
-		Namespace:       namespace,
-		Local:           local,
-		LocalPath:       localPath,
-		LocalDockerfile: dockerfile,
-		OverrideTag:     tag,
-		Method:          buildMethod,
+		SharedOpts: &deploy.SharedOpts{
+			ProjectID:       config.Project,
+			ClusterID:       config.Cluster,
+			Namespace:       namespace,
+			LocalPath:       localPath,
+			LocalDockerfile: dockerfile,
+			OverrideTag:     tag,
+			Method:          buildMethod,
+		},
+		Local: local,
 	})
 }
 

+ 71 - 0
cli/cmd/deploy/build.go

@@ -0,0 +1,71 @@
+package deploy
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/docker"
+	"github.com/porter-dev/porter/cli/cmd/pack"
+)
+
+type BuildAgent struct {
+	*SharedOpts
+
+	client      *api.Client
+	imageRepo   string
+	env         map[string]string
+	imageExists bool
+}
+
+func (b *BuildAgent) BuildDocker(dockerAgent *docker.Agent, dst, tag string) error {
+	opts := &docker.BuildOpts{
+		ImageRepo:    b.imageRepo,
+		Tag:          tag,
+		BuildContext: dst,
+		Env:          b.env,
+	}
+
+	return dockerAgent.BuildLocal(
+		opts,
+		b.LocalDockerfile,
+	)
+}
+
+func (b *BuildAgent) BuildPack(dockerAgent *docker.Agent, dst, tag string) error {
+	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
+	if b.imageExists {
+		err := dockerAgent.TagImage(
+			fmt.Sprintf("%s:%s", b.imageRepo, tag),
+			fmt.Sprintf("%s:%s", b.imageRepo, "pack-cache"),
+		)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	// create pack agent and build opts
+	packAgent := &pack.Agent{}
+
+	opts := &docker.BuildOpts{
+		ImageRepo: b.imageRepo,
+		// We tag the image with a stable param "pack-cache" so that pack can use the
+		// local image without attempting to re-pull from registry. We handle getting
+		// registry credentials and pushing/pulling the image.
+		Tag:          "pack-cache",
+		BuildContext: dst,
+		Env:          b.env,
+	}
+
+	// call builder
+	err := packAgent.Build(opts)
+
+	if err != nil {
+		return err
+	}
+
+	return dockerAgent.TagImage(
+		fmt.Sprintf("%s:%s", b.imageRepo, "pack-cache"),
+		fmt.Sprintf("%s:%s", b.imageRepo, tag),
+	)
+}

+ 428 - 0
cli/cmd/deploy/create.go

@@ -0,0 +1,428 @@
+package deploy
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/docker"
+	"github.com/porter-dev/porter/internal/templater/utils"
+)
+
+// CreateAgent handles the creation of a new application on Porter
+type CreateAgent struct {
+	Client     *api.Client
+	CreateOpts *CreateOpts
+}
+
+type CreateOpts struct {
+	*SharedOpts
+
+	Kind        string
+	ReleaseName string
+}
+
+type GithubOpts struct {
+	Branch string
+	Repo   string
+}
+
+func (c *CreateAgent) CreateFromGithub(
+	ghOpts *GithubOpts,
+	overrideValues map[string]interface{},
+) (string, error) {
+	opts := c.CreateOpts
+
+	// get all linked github repos and find matching repo
+	gitRepos, err := c.Client.ListGitRepos(
+		context.Background(),
+		c.CreateOpts.ProjectID,
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	var gitRepoMatch uint
+
+	for _, gitRepo := range gitRepos {
+		// for each git repo, search for a matching username/owner
+		githubRepos, err := c.Client.ListGithubRepos(
+			context.Background(),
+			c.CreateOpts.ProjectID,
+			gitRepo.ID,
+		)
+
+		if err != nil {
+			return "", err
+		}
+
+		for _, githubRepo := range githubRepos {
+			if githubRepo.FullName == ghOpts.Repo {
+				gitRepoMatch = gitRepo.ID
+				break
+			}
+		}
+
+		if gitRepoMatch != 0 {
+			break
+		}
+	}
+
+	if gitRepoMatch == 0 {
+		return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
+	}
+
+	latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
+
+	if err != nil {
+		return "", err
+	}
+
+	if opts.Kind == "web" || opts.Kind == "worker" {
+		mergedValues["image"] = map[string]interface{}{
+			"repository": "public.ecr.aws/o1j4x7p4/hello-porter",
+			"tag":        "latest",
+		}
+	} else if opts.Kind == "job" {
+		mergedValues["image"] = map[string]interface{}{
+			"repository": "public.ecr.aws/o1j4x7p4/hello-porter-job",
+			"tag":        "latest",
+		}
+	}
+
+	regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
+
+	if err != nil {
+		return "", err
+	}
+
+	env, err := GetEnvFromConfig(mergedValues)
+
+	if err != nil {
+		env = map[string]string{}
+	}
+
+	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+
+	if err != nil {
+		return "", err
+	}
+
+	err = c.Client.DeployTemplate(
+		context.Background(),
+		opts.ProjectID,
+		opts.ClusterID,
+		opts.Kind,
+		latestVersion,
+		&api.DeployTemplateRequest{
+			TemplateName: opts.Kind,
+			ImageURL:     imageURL,
+			FormValues:   mergedValues,
+			Namespace:    opts.Namespace,
+			Name:         opts.ReleaseName,
+			GitAction: &api.DeployTemplateGitAction{
+				GitRepo:        ghOpts.Repo,
+				GitBranch:      ghOpts.Branch,
+				ImageRepoURI:   imageURL,
+				DockerfilePath: opts.LocalDockerfile,
+				FolderPath:     ".",
+				GitRepoID:      gitRepoMatch,
+				BuildEnv:       env,
+				RegistryID:     regID,
+			},
+		},
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	return subdomain, nil
+}
+
+func (c *CreateAgent) CreateFromDocker(
+	overrideValues map[string]interface{},
+) (string, error) {
+	opts := c.CreateOpts
+
+	// detect the build config
+	if opts.Method != "" {
+		if opts.Method == DeployBuildTypeDocker {
+			if opts.LocalDockerfile == "" {
+				hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
+
+				if !hasDockerfile {
+					return "", fmt.Errorf("Dockerfile not found")
+				}
+
+				opts.LocalDockerfile = "Dockerfile"
+			}
+		}
+	} else {
+		// try to detect dockerfile, otherwise fall back to `pack`
+		hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
+
+		if !hasDockerfile {
+			opts.Method = DeployBuildTypePack
+		} else {
+			opts.Method = DeployBuildTypeDocker
+			opts.LocalDockerfile = "Dockerfile"
+		}
+	}
+
+	// overwrite with docker image repository and tag
+	regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
+
+	if err != nil {
+		return "", err
+	}
+
+	latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
+
+	if err != nil {
+		return "", err
+	}
+
+	mergedValues["image"] = map[string]interface{}{
+		"repository": imageURL,
+		"tag":        "latest",
+	}
+
+	// create docker agen
+	agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
+
+	if err != nil {
+		return "", err
+	}
+
+	env, err := GetEnvFromConfig(mergedValues)
+
+	if err != nil {
+		env = map[string]string{}
+	}
+
+	buildAgent := &BuildAgent{
+		SharedOpts:  opts.SharedOpts,
+		client:      c.Client,
+		imageRepo:   imageURL,
+		env:         env,
+		imageExists: false,
+	}
+
+	if opts.Method == DeployBuildTypeDocker {
+		err = buildAgent.BuildDocker(agent, opts.LocalPath, "latest")
+	} else {
+		err = buildAgent.BuildPack(agent, opts.LocalPath, "latest")
+	}
+
+	if err != nil {
+		return "", err
+	}
+
+	// create repository
+	err = c.Client.CreateRepository(
+		context.Background(),
+		opts.ProjectID,
+		regID,
+		&api.CreateRepositoryRequest{
+			ImageRepoURI: imageURL,
+		},
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, "latest"))
+
+	if err != nil {
+		return "", err
+	}
+
+	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+
+	if err != nil {
+		return "", err
+	}
+
+	err = c.Client.DeployTemplate(
+		context.Background(),
+		opts.ProjectID,
+		opts.ClusterID,
+		opts.Kind,
+		latestVersion,
+		&api.DeployTemplateRequest{
+			TemplateName: opts.Kind,
+			ImageURL:     imageURL,
+			FormValues:   mergedValues,
+			Namespace:    opts.Namespace,
+			Name:         opts.ReleaseName,
+		},
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	return subdomain, nil
+}
+
+type CreateConfig struct {
+	DockerfilePath string
+}
+
+// HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
+func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
+	dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
+
+	info, err := os.Stat(dockerFilePath)
+
+	return err == nil && !os.IsNotExist(err) && !info.IsDir()
+}
+
+func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
+	// get all image registries linked to the project
+	// get the list of namespaces
+	registries, err := c.Client.ListRegistries(
+		context.Background(),
+		c.CreateOpts.ProjectID,
+	)
+
+	if err != nil {
+		return 0, "", err
+	} else if len(registries) == 0 {
+		return 0, "", fmt.Errorf("must have created or linked an image registry")
+	}
+
+	// get the first non-empty registry
+	var imageURI string
+	var regID uint
+
+	for _, reg := range registries {
+		if reg.URL != "" {
+			regID = reg.ID
+			imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
+			break
+		}
+	}
+
+	return regID, imageURI, nil
+}
+
+func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
+	templates, err := c.Client.ListTemplates(
+		context.Background(),
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	var version string
+	// find the matching template name
+	for _, template := range templates {
+		if templateName == template.Name {
+			version = template.Versions[0]
+			break
+		}
+	}
+
+	if version == "" {
+		return "", fmt.Errorf("matching template version not found")
+	}
+
+	return version, nil
+}
+
+func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
+	chart, err := c.Client.GetTemplate(
+		context.Background(),
+		templateName,
+		templateVersion,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return chart.Values, nil
+}
+
+func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
+	// deploy the template
+	latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
+
+	if err != nil {
+		return "", nil, err
+	}
+
+	// get the values of the template
+	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
+
+	if err != nil {
+		return "", nil, err
+	}
+
+	// merge existing values with overriding values
+	mergedValues := utils.CoalesceValues(values, overrideValues)
+
+	return latestVersion, mergedValues, err
+}
+
+func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
+	subdomain := ""
+
+	// check for automatic subdomain creation if web kind
+	if c.CreateOpts.Kind == "web" {
+		// look for ingress.enabled and no custom domains set
+		ingressMap, err := getNestedMap(mergedValues, "ingress")
+
+		if err == nil {
+			enabledVal, enabledExists := ingressMap["enabled"]
+
+			customDomVal, customDomExists := ingressMap["custom_domain"]
+
+			if enabledExists && customDomExists {
+				enabled, eOK := enabledVal.(bool)
+				customDomain, cOK := customDomVal.(bool)
+
+				// in the case of ingress enabled but no custom domain, create subdomain
+				if eOK && cOK && enabled && !customDomain {
+					dnsRecord, err := c.Client.CreateDNSRecord(
+						context.Background(),
+						c.CreateOpts.ProjectID,
+						c.CreateOpts.ClusterID,
+						&api.CreateDNSRecordRequest{
+							ReleaseName: c.CreateOpts.ReleaseName,
+						},
+					)
+
+					if err != nil {
+						return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
+					}
+
+					subdomain = dnsRecord.ExternalURL
+
+					if ingressVal, ok := mergedValues["ingress"]; !ok {
+						mergedValues["ingress"] = map[string]interface{}{
+							"porter_hosts": []string{
+								subdomain,
+							},
+						}
+					} else {
+						ingressValMap := ingressVal.(map[string]interface{})
+
+						ingressValMap["porter_hosts"] = []string{
+							subdomain,
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return subdomain, nil
+}

+ 16 - 67
cli/cmd/deploy/deploy.go

@@ -12,7 +12,6 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/docker"
 	"github.com/porter-dev/porter/cli/cmd/github"
-	"github.com/porter-dev/porter/cli/cmd/pack"
 	"k8s.io/client-go/util/homedir"
 )
 
@@ -45,14 +44,9 @@ type DeployAgent struct {
 
 // DeployOpts are the options for creating a new DeployAgent
 type DeployOpts struct {
-	ProjectID       uint
-	ClusterID       uint
-	Namespace       string
-	Local           bool
-	LocalPath       string
-	LocalDockerfile string
-	OverrideTag     string
-	Method          DeployBuildType
+	*SharedOpts
+
+	Local bool
 }
 
 // NewDeployAgent creates a new DeployAgent given a Porter API client, application
@@ -144,7 +138,7 @@ func NewDeployAgent(client *api.Client, app string, opts *DeployOpts) (*DeployAg
 }
 
 func (d *DeployAgent) GetBuildEnv() (map[string]string, error) {
-	return d.getEnvFromRelease()
+	return GetEnvFromConfig(d.release.Config)
 }
 
 func (d *DeployAgent) SetBuildEnv(envVars map[string]string) error {
@@ -226,6 +220,14 @@ func (d *DeployAgent) Build() error {
 
 	err = d.pullCurrentReleaseImage()
 
+	buildAgent := &BuildAgent{
+		SharedOpts:  d.opts.SharedOpts,
+		client:      d.client,
+		imageRepo:   d.imageRepo,
+		env:         d.env,
+		imageExists: d.imageExists,
+	}
+
 	// if image is not found, don't return an error
 	if err != nil && err != docker.PullImageErrNotFound {
 		return err
@@ -235,63 +237,10 @@ func (d *DeployAgent) Build() error {
 	}
 
 	if d.opts.Method == DeployBuildTypeDocker {
-		return d.BuildDocker(dst, d.tag)
-	}
-
-	return d.BuildPack(dst, d.tag)
-}
-
-func (d *DeployAgent) BuildDocker(dst, tag string) error {
-	opts := &docker.BuildOpts{
-		ImageRepo:    d.imageRepo,
-		Tag:          tag,
-		BuildContext: dst,
-		Env:          d.env,
+		return buildAgent.BuildDocker(d.agent, dst, d.tag)
 	}
 
-	return d.agent.BuildLocal(
-		opts,
-		d.dockerfilePath,
-	)
-}
-
-func (d *DeployAgent) BuildPack(dst, tag string) error {
-	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
-	if d.imageExists {
-		err := d.agent.TagImage(
-			fmt.Sprintf("%s:%s", d.imageRepo, tag),
-			fmt.Sprintf("%s:%s", d.imageRepo, "pack-cache"),
-		)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	// create pack agent and build opts
-	packAgent := &pack.Agent{}
-
-	opts := &docker.BuildOpts{
-		ImageRepo: d.imageRepo,
-		// We tag the image with a stable param "pack-cache" so that pack can use the
-		// local image without attempting to re-pull from registry. We handle getting
-		// registry credentials and pushing/pulling the image.
-		Tag:          "pack-cache",
-		BuildContext: dst,
-		Env:          d.env,
-	}
-
-	// call builder
-	err := packAgent.Build(opts)
-
-	if err != nil {
-		return err
-	}
-
-	return d.agent.TagImage(
-		fmt.Sprintf("%s:%s", d.imageRepo, "pack-cache"),
-		fmt.Sprintf("%s:%s", d.imageRepo, tag),
-	)
+	return buildAgent.BuildPack(d.agent, dst, d.tag)
 }
 
 func (d *DeployAgent) Push() error {
@@ -319,8 +268,8 @@ func (d *DeployAgent) CallWebhook() error {
 }
 
 // HELPER METHODS
-func (d *DeployAgent) getEnvFromRelease() (map[string]string, error) {
-	envConfig, err := getNestedMap(d.release.Config, "container", "env", "normal")
+func GetEnvFromConfig(config map[string]interface{}) (map[string]string, error) {
+	envConfig, err := getNestedMap(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) {

+ 11 - 0
cli/cmd/deploy/shared.go

@@ -0,0 +1,11 @@
+package deploy
+
+type SharedOpts struct {
+	ProjectID       uint
+	ClusterID       uint
+	Namespace       string
+	LocalPath       string
+	LocalDockerfile string
+	OverrideTag     string
+	Method          DeployBuildType
+}

+ 102 - 0
cli/cmd/gitutils/git.go

@@ -0,0 +1,102 @@
+package gitutils
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/cli/cli/git"
+)
+
+func GitDirectory(fullpath string) (string, error) {
+	currDir, err := os.Getwd()
+
+	if err != nil {
+		return "", fmt.Errorf("could not read current directory: %s", err.Error())
+	}
+
+	err = os.Chdir(fullpath)
+
+	if err != nil {
+		return "", nil
+	}
+
+	res, gitErr := git.ToplevelDir()
+
+	err = os.Chdir(currDir)
+
+	if err != nil {
+		return "", err
+	}
+
+	return res, gitErr
+}
+
+func GetRemoteBranch(fullpath string) (*git.Remote, string, error) {
+	var remote *git.Remote
+
+	currDir, err := os.Getwd()
+
+	if err != nil {
+		return nil, "", fmt.Errorf("could not read current directory: %s", err.Error())
+	}
+
+	err = os.Chdir(fullpath)
+
+	if err != nil {
+		return nil, "", nil
+	}
+
+	// read the current branch
+	branch, gitErr := git.CurrentBranch()
+
+	if gitErr == nil {
+		branchConf := git.ReadBranchConfig(branch)
+		remoteName := "origin"
+
+		if branchConf.RemoteName != "" {
+			remoteName = branchConf.RemoteName
+		}
+
+		remotes, err := git.Remotes()
+
+		if err != nil {
+			return nil, "", err
+		}
+
+		for _, _remote := range remotes {
+			if _remote.Name == remoteName {
+				remote = _remote
+				break
+			}
+		}
+
+		if remote == nil {
+			return nil, "", fmt.Errorf("remote repository not found")
+		}
+	}
+
+	err = os.Chdir(currDir)
+
+	if err != nil {
+		return nil, "", err
+	}
+
+	return remote, branch, gitErr
+}
+
+func ParseGithubRemote(remote *git.Remote) (bool, string) {
+	if remote == nil || remote.FetchURL == nil {
+		return false, ""
+	}
+
+	if remote.FetchURL.Host != "github.com" {
+		return false, ""
+	}
+
+	if !strings.Contains(remote.FetchURL.Path, ".git") {
+		return false, ""
+	}
+
+	return true, strings.Trim(strings.TrimSuffix(remote.FetchURL.Path, ".git"), "/")
+}

+ 2 - 0
cli/values-test.yaml

@@ -0,0 +1,2 @@
+ingress:
+  enabled: false

+ 3 - 1
go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/aws/aws-sdk-go v1.35.4
 	github.com/buildpacks/pack v0.19.0
+	github.com/cli/cli v1.11.0
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/digitalocean/godo v1.56.0
 	github.com/docker/cli v20.10.5+incompatible
@@ -29,7 +30,8 @@ require (
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.4.2
 	github.com/hashicorp/golang-lru v0.5.3 // indirect
-	github.com/itchyny/gojq v0.11.1
+	github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e // indirect
+	github.com/itchyny/gojq v0.12.1
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
 	github.com/jinzhu/gorm v1.9.16
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd

+ 52 - 0
go.sum

@@ -73,6 +73,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -118,6 +120,11 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
+github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
+github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -156,6 +163,7 @@ github.com/aws/aws-sdk-go v1.35.4 h1:GG0sdhmzQSe4/UcF9iuQP9i+58bPRyU4OpujyzMlVjo
 github.com/aws/aws-sdk-go v1.35.4/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -169,6 +177,7 @@ github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
@@ -191,11 +200,19 @@ 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/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
 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=
 github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
 github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
+github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
+github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
+github.com/cli/cli v1.11.0 h1:+GNn9ya6nblVbHSIXqxcUj2ESBltHKWHA8D9+R7aU4M=
+github.com/cli/cli v1.11.0/go.mod h1:G9MSRYW7mCxBR29C3lkIQrI+ufQ5ttwpKvBJQ3ArjMM=
+github.com/cli/oauth v0.8.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
+github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
+github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
@@ -246,8 +263,10 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.13/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
 github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -267,6 +286,7 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/digitalocean/godo v1.56.0 h1:wXqWJyywrDO3YO2T4Kh8TwbCPOa+OI2vC8qh0/Ngmjk=
 github.com/digitalocean/godo v1.56.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
+github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v0.0.0-20200312141509-ef2f64abbd37 h1:MKHpi6ibJ9V5iuyUABEppUcvP0idDC1klY+UuiSFSPc=
@@ -336,6 +356,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
+github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -555,6 +576,7 @@ github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEo
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
 github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33 h1:893HsJqtxp9z1SF76gg6hY70hRY1wVlTSnC/h1yUDCo=
 github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -600,6 +622,7 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 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.1/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=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
@@ -613,6 +636,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/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
 github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0=
 github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU=
 github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
@@ -631,9 +655,12 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/itchyny/astgen-go v0.0.0-20200815150004-12a293722290 h1:9ZAJ5+eh9dfcPsJ1CXoiE16JzsBmJm1e124eUkXAyc0=
 github.com/itchyny/astgen-go v0.0.0-20200815150004-12a293722290/go.mod h1:296z3W7Xsrp2mlIY88ruDKscuvrkL6zXCNRtaYVshzw=
+github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3/go.mod h1:296z3W7Xsrp2mlIY88ruDKscuvrkL6zXCNRtaYVshzw=
 github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA=
 github.com/itchyny/gojq v0.11.1 h1:k54XkzWCGDfRJSZFRW4rXowTVzPlSjU2xUErkaFjfdo=
 github.com/itchyny/gojq v0.11.1/go.mod h1:8MKtgvJwkmRduSuzN25byPdNHfvv6y+/hmOVXei9e7k=
+github.com/itchyny/gojq v0.12.1 h1:pQJrG8LXgEbZe9hvpfjKg7UlBfieQQydIw3YQq+7WIA=
+github.com/itchyny/gojq v0.12.1/go.mod h1:Y5Lz0qoT54ii+ucY/K3yNDy19qzxZvWNBMBpKUDQR/4=
 github.com/itchyny/timefmt-go v0.1.0/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
 github.com/itchyny/timefmt-go v0.1.1 h1:rLpnm9xxb39PEEVzO0n4IRp0q6/RmBc7Dy/rE4HrA0U=
 github.com/itchyny/timefmt-go v0.1.1/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
@@ -777,6 +804,7 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
 github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -816,6 +844,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
@@ -830,6 +860,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=
 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=
@@ -878,6 +911,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
+github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -903,6 +938,7 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
 github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
 github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -1018,6 +1054,9 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -1058,6 +1097,8 @@ github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoM
 github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
+github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -1166,6 +1207,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
 github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
@@ -1323,6 +1366,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1400,6 +1444,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1415,15 +1460,20 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1673,6 +1723,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/driver/postgres v1.0.2 h1:mB5JjD4QglbCTdMT1aZDxQzHr87XDK1qh0MKIU3P96g=
 gorm.io/driver/postgres v1.0.2/go.mod h1:FvRSYfBI9jEp6ZSjlpS9qNcSjxwYxFc03UOTrHdvvYA=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=

+ 6 - 0
internal/forms/registry.go

@@ -77,3 +77,9 @@ func (urf *UpdateRegistryForm) ToRegistry(repo repository.RegistryRepository) (*
 
 	return registry, nil
 }
+
+// CreateRepository represents the accepted values for creating an image repository
+// within a registry
+type CreateRepository struct {
+	ImageRepoURI string `json:"image_repo_uri" form:"required"`
+}

+ 1 - 1
server/api/deploy_handler.go

@@ -169,7 +169,7 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		app.createGitActionFromForm(projID, release, name, gaForm, w, r)
+		app.createGitActionFromForm(projID, release, form.ChartTemplateForm.Name, gaForm, w, r)
 	}
 
 	w.WriteHeader(http.StatusOK)

+ 11 - 0
server/api/git_action_handler.go

@@ -139,6 +139,17 @@ func (app *App) createGitActionFromForm(
 
 	userID, _ := session.Values["user_id"].(uint)
 
+	if userID == 0 {
+		tok := app.getTokenFromRequest(r)
+
+		if tok != nil && tok.IBy != 0 {
+			userID = tok.IBy
+		} else if tok == nil || tok.IBy == 0 {
+			http.Error(w, "no user id found in request", http.StatusInternalServerError)
+			return nil
+		}
+	}
+
 	// generate porter jwt token
 	jwt, _ := token.GetTokenForAPI(userID, uint(projID))
 

+ 56 - 0
server/api/registry_handler.go

@@ -72,6 +72,62 @@ func (app *App) HandleCreateRegistry(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleCreateRepository creates a new image repository in a registry, if the registry
+// does not allow for create-on-push behavior
+func (app *App) HandleCreateRepository(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	regID, err := strconv.ParseUint(chi.URLParam(r, "registry_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateRepository{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// read the registry
+	reg, err := app.Repo.Registry.ReadRegistry(uint(regID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	_reg := registry.Registry(*reg)
+	regAPI := &_reg
+
+	// parse the name from the registry
+	nameSpl := strings.Split(form.ImageRepoURI, "/")
+	repoName := nameSpl[len(nameSpl)-1]
+
+	err = regAPI.CreateRepository(*app.Repo, repoName)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusCreated)
+}
+
 // HandleListProjectRegistries returns a list of registries for a project
 func (app *App) HandleListProjectRegistries(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)

+ 14 - 0
server/router/router.go

@@ -804,6 +804,20 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
+			r.Method(
+				"POST",
+				"/projects/{project_id}/registries/{registry_id}/repository",
+				auth.DoesUserHaveProjectAccess(
+					auth.DoesUserHaveRegistryAccess(
+						requestlog.NewHandler(a.HandleCreateRepository, l),
+						mw.URLParam,
+						mw.URLParam,
+					),
+					mw.URLParam,
+					mw.WriteAccess,
+				),
+			)
+
 			r.Method(
 				"GET",
 				"/projects/{project_id}/registries/ecr/{region}/token",