Przeglądaj źródła

Consolidate 3 stack endpoints (create db, create stack, update stack) to a single endpoint (#2982)

* functional using one endpoint

* pushing changes so far

* testing

* cleanups

---------

Co-authored-by: jusrhee <justin@porter.run>
Feroze Mohideen 3 lat temu
rodzic
commit
6209d79d64

+ 14 - 27
api/client/stack.go

@@ -7,52 +7,39 @@ import (
 	"github.com/porter-dev/porter/api/types"
 )
 
-// CreateStack creates the stack
-func (c *Client) CreateStack(
-	ctx context.Context,
-	projectID, clusterID uint,
-	req *types.CreateStackReleaseRequest,
-) error {
-	return c.postRequest(
-		fmt.Sprintf(
-			"/projects/%d/clusters/%d/stacks",
-			projectID, clusterID,
-		),
-		req,
-		nil,
-	)
-}
-
-// UpdateStack updates the stack
-func (c *Client) UpdateStack(
+func (c *Client) GetPorterApp(
 	ctx context.Context,
 	projectID, clusterID uint,
 	stackName string,
-	req *types.CreateStackReleaseRequest,
-) error {
-	return c.patchRequest(
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.getRequest(
 		fmt.Sprintf(
 			"/projects/%d/clusters/%d/stacks/%s",
 			projectID, clusterID, stackName,
 		),
-		req,
 		nil,
+		resp,
 	)
+
+	return resp, err
 }
 
-func (c *Client) GetStack(
+func (c *Client) CreatePorterApp(
 	ctx context.Context,
 	projectID, clusterID uint,
-	stackName string,
+	name string,
+	req *types.CreatePorterAppRequest,
 ) (*types.PorterApp, error) {
 	resp := &types.PorterApp{}
 
-	err := c.getRequest(
+	err := c.postRequest(
 		fmt.Sprintf(
 			"/projects/%d/clusters/%d/stacks/%s",
-			projectID, clusterID, stackName,
+			projectID, clusterID, name,
 		),
-		nil,
+		req,
 		resp,
 	)
 

+ 0 - 119
api/server/handlers/stacks/create.go

@@ -1,119 +0,0 @@
-package stacks
-
-import (
-	"encoding/base64"
-	"fmt"
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/helm"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type CreateStackHandler struct {
-	handlers.PorterHandlerReadWriter
-	authz.KubernetesAgentGetter
-}
-
-func NewCreateStackHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *CreateStackHandler {
-	return &CreateStackHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
-	}
-}
-
-func (c *CreateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
-	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-
-	request := &types.CreateStackReleaseRequest{}
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
-		return
-	}
-
-	stackName := request.StackName
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
-
-	helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
-		return
-	}
-
-	k8sAgent, err := c.GetAgent(r, cluster, namespace)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
-		return
-	}
-
-	porterYamlBase64 := request.PorterYAMLBase64
-	porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding porter yaml: %w", err)))
-		return
-	}
-	imageInfo := request.ImageInfo
-	chart, values, err := parse(porterYaml,
-		imageInfo,
-		c.Config(),
-		cluster.ProjectID,
-		nil,
-		nil,
-		SubdomainCreateOpts{
-			k8sAgent:       k8sAgent,
-			dnsRepo:        c.Repo().DNSRecord(),
-			powerDnsClient: c.Config().PowerDNSClient,
-			appRootDomain:  c.Config().ServerConf.AppRootDomain,
-			stackName:      stackName,
-		})
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter yaml into chart and values: %w", err)))
-		return
-	}
-
-	// create the namespace if it does not exist already
-	_, err = k8sAgent.CreateNamespace(namespace, nil)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating namespace: %w", err)))
-		return
-	}
-
-	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
-		return
-	}
-
-	conf := &helm.InstallChartConfig{
-		Chart:      chart,
-		Name:       stackName,
-		Namespace:  namespace,
-		Values:     values,
-		Cluster:    cluster,
-		Repo:       c.Repo(),
-		Registries: registries,
-	}
-
-	_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
-
-		_, err = helmAgent.UninstallChart(stackName)
-		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling chart: %w", err)))
-		}
-
-		return
-	}
-	w.WriteHeader(http.StatusCreated)
-}

+ 157 - 24
api/server/handlers/stacks/create_porter_app.go

@@ -1,6 +1,7 @@
 package stacks
 
 import (
+	"encoding/base64"
 	"fmt"
 	"net/http"
 
@@ -9,7 +10,9 @@ import (
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -25,6 +28,7 @@ func NewCreatePorterAppHandler(
 ) *CreatePorterAppHandler {
 	return &CreatePorterAppHandler{
 		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
 	}
 }
 
@@ -34,42 +38,171 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
 	request := &types.CreatePorterAppRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
+		return
+	}
 
-	ok := c.DecodeAndValidate(w, r, request)
-	if !ok {
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
 		return
 	}
+	namespace := fmt.Sprintf("porter-stack-%s", stackName)
 
-	existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, request.Name)
+	helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	} else if existing.Name != "" {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("porter app with name %s already exists in this environment", existing.Name), http.StatusForbidden))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
 		return
 	}
 
-	app := &models.PorterApp{
-		Name:      request.Name,
-		ClusterID: cluster.ID,
-		ProjectID: project.ID,
-		RepoName:  request.RepoName,
-		GitRepoID: request.GitRepoID,
-		GitBranch: request.GitBranch,
-
-		BuildContext:   request.BuildContext,
-		Builder:        request.Builder,
-		Buildpacks:     request.Buildpacks,
-		Dockerfile:     request.Dockerfile,
-		ImageRepoURI:   request.ImageRepoURI,
-		PullRequestURL: request.PullRequestURL,
+	k8sAgent, err := c.GetAgent(r, cluster, namespace)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
+		return
 	}
 
-	porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
+	helmRelease, err := helmAgent.GetRelease(stackName, 0, false)
+	shouldCreate := err != nil
+
+	porterYamlBase64 := request.PorterYAMLBase64
+	porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
 	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding porter yaml: %w", err)))
 		return
 	}
+	imageInfo := request.ImageInfo
+	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
+		return
+	}
+
+	if shouldCreate {
+		chart, values, err := parse(
+			porterYaml,
+			imageInfo,
+			c.Config(),
+			cluster.ProjectID,
+			nil,
+			nil,
+			SubdomainCreateOpts{
+				k8sAgent:       k8sAgent,
+				dnsRepo:        c.Repo().DNSRecord(),
+				powerDnsClient: c.Config().PowerDNSClient,
+				appRootDomain:  c.Config().ServerConf.AppRootDomain,
+				stackName:      stackName,
+			})
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter yaml into chart and values: %w", err)))
+			return
+		}
+
+		// create the namespace if it does not exist already
+		_, err = k8sAgent.CreateNamespace(namespace, nil)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating namespace: %w", err)))
+			return
+		}
+
+		conf := &helm.InstallChartConfig{
+			Chart:      chart,
+			Name:       stackName,
+			Namespace:  namespace,
+			Values:     values,
+			Cluster:    cluster,
+			Repo:       c.Repo(),
+			Registries: registries,
+		}
+
+		_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
 
-	c.WriteResult(w, r, porterApp.ToPorterAppType())
+			_, err = helmAgent.UninstallChart(stackName)
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling chart: %w", err)))
+			}
+
+			return
+		}
+
+		existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		} else if existing.Name != "" {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("porter app with name %s already exists in this environment", existing.Name), http.StatusForbidden))
+			return
+		}
+
+		app := &models.PorterApp{
+			Name:      stackName,
+			ClusterID: cluster.ID,
+			ProjectID: project.ID,
+			RepoName:  request.RepoName,
+			GitRepoID: request.GitRepoID,
+			GitBranch: request.GitBranch,
+
+			BuildContext:   request.BuildContext,
+			Builder:        request.Builder,
+			Buildpacks:     request.Buildpacks,
+			Dockerfile:     request.Dockerfile,
+			ImageRepoURI:   request.ImageRepoURI,
+			PullRequestURL: request.PullRequestURL,
+		}
+
+		porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing app to DB: %s", err.Error())))
+			return
+		}
+
+		c.WriteResult(w, r, porterApp.ToPorterAppType())
+	} else {
+		chart, values, err := parse(
+			porterYaml,
+			imageInfo,
+			c.Config(),
+			cluster.ProjectID,
+			helmRelease.Config,
+			helmRelease.Chart.Metadata.Dependencies,
+			SubdomainCreateOpts{
+				k8sAgent:       k8sAgent,
+				dnsRepo:        c.Repo().DNSRecord(),
+				powerDnsClient: c.Config().PowerDNSClient,
+				appRootDomain:  c.Config().ServerConf.AppRootDomain,
+				stackName:      stackName,
+			})
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter yaml into chart and values: %w", err)))
+			return
+		}
+
+		conf := &helm.InstallChartConfig{
+			Chart:      chart,
+			Name:       stackName,
+			Namespace:  namespace,
+			Values:     values,
+			Cluster:    cluster,
+			Repo:       c.Repo(),
+			Registries: registries,
+		}
+
+		_, err = helmAgent.UpgradeInstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
+
+			return
+		}
+
+		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		c.WriteResult(w, r, app.ToPorterAppType())
+	}
 }

+ 15 - 0
api/server/handlers/stacks/create_secret_and_open_pr.go

@@ -116,6 +116,21 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		resp = types.CreateSecretAndOpenGHPRResponse{
 			URL: pr.GetHTMLURL(),
 		}
+
+		// update DB with the PR url
+		porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("unable to get porter app db: %w", err)))
+			return
+		}
+
+		porterApp.PullRequestURL = pr.GetHTMLURL()
+
+		_, err = c.Repo().PorterApp().UpdatePorterApp(porterApp)
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("unable to write pr url to porter app db: %w", err)))
+			return
+		}
 	}
 
 	w.WriteHeader(http.StatusCreated)

+ 2 - 2
api/server/handlers/stacks/delete_porter_app.go

@@ -33,13 +33,13 @@ func (c *DeletePorterAppByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.
 	ctx := r.Context()
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
-	name, reqErr := requestutils.GetURLParamString(r, types.URLParamReleaseName)
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
 	if reqErr != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
 		return
 	}
 
-	porterApp, appErr := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
+	porterApp, appErr := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
 	if appErr != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(appErr))
 		return

+ 6 - 2
api/server/handlers/stacks/get_porter_app.go

@@ -30,9 +30,13 @@ func NewGetPorterAppHandler(
 func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		return
+	}
 
-	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
+	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 0 - 80
api/server/handlers/stacks/update_porter_app.go

@@ -1,80 +0,0 @@
-package stacks
-
-import (
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type UpdatePorterAppHandler struct {
-	handlers.PorterHandlerReadWriter
-	authz.KubernetesAgentGetter
-}
-
-func NewUpdatePorterAppHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *UpdatePorterAppHandler {
-	return &UpdatePorterAppHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *UpdatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
-	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-
-	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
-
-	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, name)
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	request := &types.UpdatePorterAppRequest{}
-	ok := c.DecodeAndValidate(w, r, request)
-	if !ok {
-		return
-	}
-
-	if request.RepoName != "" {
-		porterApp.RepoName = request.RepoName
-	}
-	if request.GitBranch != "" {
-		porterApp.GitBranch = request.GitBranch
-	}
-	if request.BuildContext != "" {
-		porterApp.BuildContext = request.BuildContext
-	}
-	if request.Builder != "" {
-		porterApp.Builder = request.Builder
-	}
-	if request.Buildpacks != "" {
-		porterApp.Buildpacks = request.Buildpacks
-	}
-	if request.Dockerfile != "" {
-		porterApp.Dockerfile = request.Dockerfile
-	}
-	if request.ImageRepoURI != "" {
-		porterApp.ImageRepoURI = request.ImageRepoURI
-	}
-	if request.PullRequestURL != "" {
-		porterApp.PullRequestURL = request.PullRequestURL
-	}
-
-	updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(porterApp)
-	if err != nil {
-		return
-	}
-
-	c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
-}

+ 4 - 91
api/server/router/stack.go

@@ -62,7 +62,7 @@ func getStackRoutes(
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/{name}",
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -118,7 +118,7 @@ func getStackRoutes(
 			Method: types.HTTPVerbDelete,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/{name}",
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -140,14 +140,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/update_config -> stacks.NewCreatePorterAppHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack} -> stacks.NewCreatePorterAppHandler
 	createPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/update_config",
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -169,93 +169,6 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> stacks.NewCreatePorterAppHandler
-	updatePorterAppEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/{name}",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	updatePorterAppHandler := stacks.NewUpdatePorterAppHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: updatePorterAppEndpoint,
-		Handler:  updatePorterAppHandler,
-		Router:   r,
-	})
-
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks -> stacks.NewCreateStackHandler
-	createEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPost,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath,
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	createHandler := stacks.NewCreateStackHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: createEndpoint,
-		Handler:  createHandler,
-		Router:   r,
-	})
-
-	// PATCH /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack} -> stacks.NewUpdateStackHandler
-	updateEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbPatch,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/{stack}",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	updateHandler := stacks.NewUpdateStackHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: updateEndpoint,
-		Handler:  updateHandler,
-		Router:   r,
-	})
-
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> stacks.NewOpenStackPRHandler
 	createSecretAndOpenGitHubPullRequestEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 13 - 12
api/types/porter_app.go

@@ -24,18 +24,19 @@ type PorterApp struct {
 
 // swagger:model
 type CreatePorterAppRequest struct {
-	Name           string `json:"name" form:"required"`
-	ClusterID      uint   `json:"cluster_id"`
-	ProjectID      uint   `json:"project_id"`
-	RepoName       string `json:"repo_name"`
-	GitBranch      string `json:"git_branch"`
-	GitRepoID      uint   `json:"git_repo_id"`
-	BuildContext   string `json:"build_context"`
-	Builder        string `json:"builder"`
-	Buildpacks     string `json:"buildpacks"`
-	Dockerfile     string `json:"dockerfile"`
-	ImageRepoURI   string `json:"image_repo_uri"`
-	PullRequestURL string `json:"pull_request_url"`
+	ClusterID        uint      `json:"cluster_id"`
+	ProjectID        uint      `json:"project_id"`
+	RepoName         string    `json:"repo_name"`
+	GitBranch        string    `json:"git_branch"`
+	GitRepoID        uint      `json:"git_repo_id"`
+	BuildContext     string    `json:"build_context"`
+	Builder          string    `json:"builder"`
+	Buildpacks       string    `json:"buildpacks"`
+	Dockerfile       string    `json:"dockerfile"`
+	ImageRepoURI     string    `json:"image_repo_uri"`
+	PullRequestURL   string    `json:"pull_request_url"`
+	PorterYAMLBase64 string    `json:"porter_yaml" form:"required"`
+	ImageInfo        ImageInfo `json:"image_info" form:"omitempty"`
 }
 
 type UpdatePorterAppRequest struct {

+ 3 - 5
cli/cmd/preview/build_image_driver.go

@@ -69,11 +69,9 @@ func (d *BuildDriver) Apply(resource *models.Resource) (*models.Resource, error)
 
 	if tag == "" {
 		commit, err := git.LastCommit()
-		if err != nil {
-			return nil, fmt.Errorf("could not get last commit to be used as the image tag: %s", err.Error())
-		}
-
-		tag = commit.Sha[:7]
+		if err == nil {
+			tag = commit.Sha[:7]
+		} 
 	}
 
 	// if the method is registry and a tag is defined, we use the provided tag

+ 2 - 2
cli/cmd/stack/apply.go

@@ -100,9 +100,9 @@ func createV1BuildResourcesFromPorterYaml(stackConf *StackConf) (*switchboardTyp
 }
 
 func createV1BuildResourcesFromDB(client *api.Client, stackConf *StackConf) (*switchboardTypes.Resource, *switchboardTypes.Resource, error) {
-	res, err := client.GetStack(context.Background(), stackConf.projectID, stackConf.clusterID, stackConf.stackName)
+	res, err := client.GetPorterApp(context.Background(), stackConf.projectID, stackConf.clusterID, stackConf.stackName)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, fmt.Errorf("unable to read build info from DB: %w", err)
 	}
 
 	if res == nil {

+ 4 - 2
cli/cmd/stack/build.go

@@ -87,8 +87,10 @@ func (b *Build) getV1BuildImage(env map[string]string) (*types.Resource, error)
 	} else if b.GetMethod() == "registry" {
 		config.Build.Method = "registry"
 		config.Build.Image = b.GetImage()
-	} else {
-		return nil, fmt.Errorf("invalid build method: %s", b.GetMethod())
+	} else { // default to pack
+		config.Build.Method = "pack"
+		config.Build.Builder = b.GetBuilder()
+		config.Build.Buildpacks = b.GetBuildpacks()
 	}
 
 	config.Build.Context = b.GetContext()

+ 15 - 27
cli/cmd/stack/hooks.go

@@ -76,35 +76,23 @@ func (t *DeployStackHook) applyStack(client *api.Client, shouldCreate bool, driv
 		}
 	}
 
-	if shouldCreate {
-		err := client.CreateStack(
-			context.Background(),
-			t.ProjectID,
-			t.ClusterID,
-			&types.CreateStackReleaseRequest{
-				StackName:        t.StackName,
-				PorterYAMLBase64: base64.StdEncoding.EncodeToString(t.PorterYAML),
-				ImageInfo:        imageInfo,
-			},
-		)
-		if err != nil {
+	_, err := client.CreatePorterApp(
+		context.Background(),
+		t.ProjectID,
+		t.ClusterID,
+		t.StackName,
+		&types.CreatePorterAppRequest{
+			ClusterID:        t.ClusterID,
+			ProjectID:        t.ProjectID,
+			PorterYAMLBase64: base64.StdEncoding.EncodeToString(t.PorterYAML),
+			ImageInfo:        imageInfo,
+		},
+	)
+	if err != nil {
+		if shouldCreate {
 			return fmt.Errorf("error creating stack %s: %w", t.StackName, err)
 		}
-	} else {
-		err := client.UpdateStack(
-			context.Background(),
-			t.ProjectID,
-			t.ClusterID,
-			t.StackName,
-			&types.CreateStackReleaseRequest{
-				StackName:        t.StackName,
-				PorterYAMLBase64: base64.StdEncoding.EncodeToString(t.PorterYAML),
-				ImageInfo:        imageInfo,
-			},
-		)
-		if err != nil {
-			return fmt.Errorf("error updating stack %s: %w", t.StackName, err)
-		}
+		return fmt.Errorf("error updating stack %s: %w", t.StackName, err)
 	}
 
 	return nil

+ 12 - 17
dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx

@@ -20,6 +20,7 @@ import {
   BuildConfig,
   FullActionConfigType,
   GithubActionConfigType,
+  PorterAppOptions,
 } from "shared/types";
 import { RouteComponentProps } from "react-router";
 import { Context } from "shared/Context";
@@ -43,12 +44,14 @@ type Props = {
   appData: any;
   setAppData: Dispatch<any>;
   onTabSwitch: () => void;
+  updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
 };
 
 const BuildSettingsTabStack: React.FC<Props> = ({
   appData,
   setAppData,
   onTabSwitch,
+  updatePorterApp,
 }) => {
   const { setCurrentError } = useContext(Context);
   const [updated, setUpdated] = useState(null);
@@ -163,23 +166,15 @@ const BuildSettingsTabStack: React.FC<Props> = ({
   const saveConfig = async () => {
     console.log(appData);
     try {
-      await api.updatePorterApp(
-        "<token>",
-        {
-          repo_name: appData.app.repo_name,
-          git_branch: branch,
-          build_context: appData.app.build_context,
-          builder: buildConfig.builder,
-          buildpacks: buildConfig.buildpacks?.join(","),
-          dockerfile: appData.app.dockerfile,
-          image_repo_uri: appData.chart.image_repo_uri,
-        },
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          name: appData.app.name,
-        }
-      );
+      await updatePorterApp({
+        repo_name: appData.app.repo_name,
+        git_branch: branch,
+        build_context: appData.app.build_context,
+        builder: buildConfig.builder,
+        buildpacks: buildConfig.buildpacks?.join(","),
+        dockerfile: appData.app.dockerfile,
+        image_repo_uri: appData.chart.image_repo_uri,
+      });
       onTabSwitch();
     } catch (err) {
       throw err;

+ 12 - 7
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -25,7 +25,7 @@ import Spacer from "components/porter/Spacer";
 import Link from "components/porter/Link";
 import Back from "components/porter/Back";
 import TabSelector from "components/TabSelector";
-import { ChartType, ResourceType } from "shared/types";
+import { ChartType, PorterAppOptions, ResourceType } from "shared/types";
 import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
 import BuildSettingsTabStack from "./BuildSettingsTabStack";
 import Button from "components/porter/Button";
@@ -188,7 +188,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     setDeleting(true);
     const { appName } = props.match.params as any;
     try {
-      const res = await api.deletePorterApp(
+      await api.deletePorterApp(
         "<token>",
         {},
         {
@@ -197,7 +197,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           name: appName,
         }
       );
-      const nsRes = await api.deleteNamespace(
+      await api.deleteNamespace(
         "<token>",
         {},
         {
@@ -209,11 +209,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       props.history.push("/apps");
     } catch (err) {
       setError(err);
+    } finally {
       setDeleting(false);
     }
   };
 
-  const updatePorterApp = async () => {
+  const updatePorterApp = async (options: Partial<PorterAppOptions>) => {
     try {
       setButtonStatus("loading");
       if (
@@ -232,11 +233,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         );
         const yamlString = yaml.dump(finalPorterYaml);
         const base64Encoded = btoa(yamlString);
-        await api.updatePorterStack(
+        await api.createPorterApp(
           "<token>",
           {
-            stack_name: appData.app.name,
             porter_yaml: base64Encoded,
+            ...options,
           },
           {
             cluster_id: currentCluster.id,
@@ -326,7 +327,10 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     ) {
       const svcs = Service.deserialize(helmValues, defaultValues, porterJson);
       setServices(svcs);
-      if (helmValues && Object.keys(helmValues).length > 0) {
+      if (helmValues && 'global' in helmValues) {
+        delete helmValues.global; // not necessary for displaying services or env variables
+      }
+      if (Object.keys(helmValues).length > 0) {
         const envs = Service.retrieveEnvFromHelmValues(helmValues);
         setEnvVars(envs);
         const subdomain = Service.retrieveSubdomainFromHelmValues(
@@ -521,6 +525,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             appData={appData}
             setAppData={setAppData}
             onTabSwitch={getPorterApp}
+            updatePorterApp={updatePorterApp}
           />
         );
       case "settings":

+ 1 - 12
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -77,18 +77,7 @@ const GithubActionModal: React.FC<Props> = ({
               stack_name: stackName,
             }
           );
-          if (res?.data?.url) {
-            const updateRes = await api.updatePorterApp(
-              "<token>",
-              {
-                pull_request_url: res.data.url,
-              },
-              {
-                project_id: projectId,
-                cluster_id: clusterId,
-                name: stackName,
-              }
-            )
+          if (res.data?.url) {
             window.open(res.data.url, "_blank", "noreferrer");
             if (!deployPorterApp) {
               window.location.reload();

+ 12 - 25
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -143,9 +143,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       ) {
         setDetected({
           detected: true,
-          message: `Detected ${
-            Object.keys(porterYamlToJson.apps).length
-          } apps from porter.yaml`,
+          message: `Detected ${Object.keys(porterYamlToJson.apps).length
+            } apps from porter.yaml`,
         });
       } else {
         setDetected({
@@ -242,32 +241,16 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       const base64Encoded = btoa(yamlString);
       const imageInfo = imageUrl
         ? {
-            image_info: {
-              repository: imageUrl,
-              tag: imageTag,
-            },
-          }
-        : {};
-
-      // create the dummy chart
-      await api.createPorterStack(
-        "<token>",
-        {
-          stack_name: formState.applicationName,
-          porter_yaml: base64Encoded,
-          ...imageInfo,
-        },
-        {
-          cluster_id: currentCluster.id,
-          project_id: currentProject.id,
+          image_info: {
+            repository: imageUrl,
+            tag: imageTag,
+          },
         }
-      );
+        : {};
 
-      // if success, write to the db
       await api.createPorterApp(
         "<token>",
         {
-          name: formState.applicationName,
           repo_name: actionConfig.git_repo,
           git_branch: branch,
           git_repo_id: actionConfig?.git_repo_id,
@@ -276,10 +259,13 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           buildpacks: (buildConfig as any)?.buildpacks?.join(",") ?? "",
           dockerfile: dockerfilePath,
           image_repo_uri: imageUrl,
+          porter_yaml: base64Encoded,
+          ...imageInfo,
         },
         {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
+          stack_name: formState.applicationName,
         }
       );
 
@@ -314,6 +300,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   //       setAccessLoading(false);
   //     });
   // }, []);
+
   return (
     <CenterWrapper>
       <Div>
@@ -627,7 +614,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#353a3e"};
+    props.disabled ? "" : "#353a3e"};
   }
 
   > i {

+ 24 - 25
dashboard/src/main/home/cluster-dashboard/dashboard/ProvisionerStatus.tsx

@@ -24,32 +24,31 @@ const ProvisionerStatus: React.FC<Props> = ({ provisionFailureReason }) => {
   const pollProvisioningAndClusterStatus = async () => {
     if (currentProject && currentCluster) {
       try {
-        if (progress < 4) {
-          const resState = await api.getClusterState(
-            "<token>",
-            {},
-            {
-              project_id: currentProject.id,
-              cluster_id: currentCluster.id,
-            }
-          );
-          const {
-            is_control_plane_ready,
-            is_infrastructure_ready,
-            phase,
-          } = resState.data;
-          let newProgress = 1;
-          if (is_control_plane_ready) {
-            newProgress += 1;
-          }
-          if (is_infrastructure_ready) {
-            newProgress += 1;
-          }
-          if (phase === "Provisioned") {
-            newProgress += 1;
+        const resState = await api.getClusterState(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
           }
-          setProgress(newProgress);
-        } else {
+        );
+        const {
+          is_control_plane_ready,
+          is_infrastructure_ready,
+          phase,
+        } = resState.data;
+        let newProgress = 1;
+        if (is_control_plane_ready) {
+          newProgress += 1;
+        }
+        if (is_infrastructure_ready) {
+          newProgress += 1;
+        }
+        if (phase === "Provisioned") {
+          newProgress += 1;
+        }
+        setProgress(newProgress);
+        if (newProgress >= 4) {
           const resStatus = await api.getCluster(
             "<token>",
             {},

+ 5 - 75
dashboard/src/shared/api.tsx

@@ -2,7 +2,7 @@ import { PolicyDocType } from "./auth/types";
 import { PullRequest } from "main/home/cluster-dashboard/preview-environments/types";
 import { baseApi } from "./baseApi";
 
-import { BuildConfig, FullActionConfigType } from "./types";
+import { BuildConfig, FullActionConfigType, PorterAppOptions } from "./types";
 import {
   CreateStackBody,
   SourceConfig,
@@ -188,45 +188,15 @@ const getPorterApp = baseApi<
 });
 
 const createPorterApp = baseApi<
-  {
-    name: string;
-    repo_name: string;
-    git_branch: string;
-    git_repo_id: number;
-    build_context: string;
-    builder: string;
-    buildpacks: string;
-    dockerfile: string;
-    image_repo_uri: string;
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-  }
->("POST", (pathParams) => {
-  let { project_id, cluster_id } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/update_config`;
-});
-
-const updatePorterApp = baseApi<
-  {
-    repo_name?: string;
-    git_branch?: string;
-    build_context?: string;
-    builder?: string;
-    buildpacks?: string;
-    dockerfile?: string;
-    image_repo_uri?: string;
-    pull_request_url?: string;
-  },
+  PorterAppOptions,
   {
     project_id: number;
     cluster_id: number;
-    name: string;
+    stack_name: string;
   }
 >("POST", (pathParams) => {
-  let { project_id, cluster_id, name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
+  let { project_id, cluster_id, stack_name } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}`;
 });
 
 const deletePorterApp = baseApi<
@@ -241,43 +211,6 @@ const deletePorterApp = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
 });
 
-const createPorterStack = baseApi<
-  {
-    stack_name: string;
-    porter_yaml: string;
-    image_info?: {
-      repository: string;
-      tag: string;
-    }
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-  }
->("POST", (pathParams) => {
-  let { project_id, cluster_id } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
-});
-
-const updatePorterStack = baseApi<
-  {
-    stack_name: string;
-    porter_yaml: string;
-    image_info?: {
-      repository: string;
-      tag: string;
-    }
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-    stack_name: string;
-  }
->("PATCH", (pathParams) => {
-  let { project_id, cluster_id, stack_name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}`;
-});
-
 const createEnvironment = baseApi<
   {
     name: string;
@@ -2597,10 +2530,7 @@ export default {
   getPorterApps,
   getPorterApp,
   createPorterApp,
-  updatePorterApp,
   deletePorterApp,
-  createPorterStack,
-  updatePorterStack,
   createConfigMap,
   deleteCluster,
   deleteConfigMap,

+ 16 - 0
dashboard/src/shared/types.tsx

@@ -642,3 +642,19 @@ export type BuildConfig = {
     [key: string]: string;
   };
 };
+
+export interface PorterAppOptions {
+  porter_yaml: string;
+  repo_name?: string;
+  git_branch?: string;
+  git_repo_id?: number;
+  build_context?: string;
+  builder?: string;
+  buildpacks?: string;
+  dockerfile?: string;
+  image_repo_uri?: string;
+  image_info?: {
+    repository: string;
+    tag: string;
+  };
+}