Sfoglia il codice sorgente

Porting legacy endpoints to porter apps (#3358)

Feroze Mohideen 2 anni fa
parent
commit
6bc1207006

+ 129 - 0
api/client/porter_app.go

@@ -0,0 +1,129 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+func (c *Client) NewGetPorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	appName string,
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/applications/%s",
+			projectID, clusterID, appName,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
+func (c *Client) NewCreatePorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	appName string,
+	req *types.CreatePorterAppRequest,
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/applications/%s",
+			projectID, clusterID, appName,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// NewCreateOrUpdatePorterAppEvent will create a porter app event if one does not exist, or else it will update the existing one if an ID is passed in the object
+func (c *Client) NewCreateOrUpdatePorterAppEvent(
+	ctx context.Context,
+	projectID, clusterID uint,
+	appName string,
+	req *types.CreateOrUpdatePorterAppEventRequest,
+) (types.PorterAppEvent, error) {
+	resp := &types.PorterAppEvent{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/applications/%s/events",
+			projectID, clusterID, appName,
+		),
+		req,
+		resp,
+	)
+
+	return *resp, err
+}
+
+// TODO: remove these functions once they are no longer called (check telemetry)
+func (c *Client) GetPorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	stackName string,
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/stacks/%s",
+			projectID, clusterID, stackName,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
+func (c *Client) CreatePorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	name string,
+	req *types.CreatePorterAppRequest,
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/stacks/%s",
+			projectID, clusterID, name,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// CreateOrUpdatePorterAppEvent will create a porter app event if one does not exist, or else it will update the existing one if an ID is passed in the object
+func (c *Client) CreateOrUpdatePorterAppEvent(
+	ctx context.Context,
+	projectID, clusterID uint,
+	name string,
+	req *types.CreateOrUpdatePorterAppEventRequest,
+) (types.PorterAppEvent, error) {
+	resp := &types.PorterAppEvent{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/stacks/%s/events",
+			projectID, clusterID, name,
+		),
+		req,
+		resp,
+	)
+
+	return *resp, err
+}

+ 0 - 68
api/client/stack.go

@@ -1,68 +0,0 @@
-package client
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/porter-dev/porter/api/types"
-)
-
-func (c *Client) GetPorterApp(
-	ctx context.Context,
-	projectID, clusterID uint,
-	stackName string,
-) (*types.PorterApp, error) {
-	resp := &types.PorterApp{}
-
-	err := c.getRequest(
-		fmt.Sprintf(
-			"/projects/%d/clusters/%d/stacks/%s",
-			projectID, clusterID, stackName,
-		),
-		nil,
-		resp,
-	)
-
-	return resp, err
-}
-
-func (c *Client) CreatePorterApp(
-	ctx context.Context,
-	projectID, clusterID uint,
-	name string,
-	req *types.CreatePorterAppRequest,
-) (*types.PorterApp, error) {
-	resp := &types.PorterApp{}
-
-	err := c.postRequest(
-		fmt.Sprintf(
-			"/projects/%d/clusters/%d/stacks/%s",
-			projectID, clusterID, name,
-		),
-		req,
-		resp,
-	)
-
-	return resp, err
-}
-
-// CreateOrUpdatePorterAppEvent will create a porter app event if one does not exist, or else it will update the existing one if an ID is passed in the object
-func (c *Client) CreateOrUpdatePorterAppEvent(
-	ctx context.Context,
-	projectID, clusterID uint,
-	name string,
-	req *types.CreateOrUpdatePorterAppEventRequest,
-) (types.PorterAppEvent, error) {
-	resp := &types.PorterAppEvent{}
-
-	err := c.postRequest(
-		fmt.Sprintf(
-			"/projects/%d/clusters/%d/stacks/%s/events",
-			projectID, clusterID, name,
-		),
-		req,
-		resp,
-	)
-
-	return *resp, err
-}

+ 22 - 20
api/server/handlers/porter_app/create.go

@@ -20,6 +20,7 @@ import (
 	"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"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
@@ -58,14 +59,14 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: stackName})
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
 
 	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
 	if err != nil {
@@ -81,7 +82,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	helmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
+	helmRelease, err := helmAgent.GetRelease(ctx, appName, 0, false)
 	shouldCreate := err != nil
 
 	porterYamlBase64 := request.PorterYAMLBase64
@@ -131,7 +132,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	if request.Builder == "" {
 		// attempt to get builder from db
-		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 		if err == nil {
 			request.Builder = app.Builder
 		}
@@ -167,6 +168,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	chart, values, preDeployJobValues, err := parse(
 		ctx,
 		ParseConf{
+			PorterAppName:             appName,
 			PorterYaml:                porterYaml,
 			ImageInfo:                 imageInfo,
 			ServerConfig:              c.Config(),
@@ -182,7 +184,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 				dnsRepo:        c.Repo().DNSRecord(),
 				powerDnsClient: c.Config().PowerDNSClient,
 				appRootDomain:  c.Config().ServerConf.AppRootDomain,
-				stackName:      stackName,
+				stackName:      appName,
 			},
 			InjectLauncherToStartCommand: injectLauncher,
 			ShouldValidateHelmValues:     shouldCreate,
@@ -205,7 +207,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-pre-deploy-job", Value: true})
 			conf, err := createReleaseJobChart(
 				ctx,
-				stackName,
+				appName,
 				preDeployJobValues,
 				c.Config().ServerConf.DefaultApplicationHelmRepoURL,
 				registries,
@@ -224,7 +226,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 				telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
 				telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
 				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-				_, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
+				_, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
 				if uninstallChartErr != nil {
 					uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
 					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
@@ -235,7 +237,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 		conf := &helm.InstallChartConfig{
 			Chart:      chart,
-			Name:       stackName,
+			Name:       appName,
 			Namespace:  namespace,
 			Values:     values,
 			Cluster:    cluster,
@@ -249,7 +251,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			err = telemetry.Error(ctx, span, err, "error installing app chart")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
-			_, err = helmAgent.UninstallChart(ctx, stackName)
+			_, err = helmAgent.UninstallChart(ctx, appName)
 			if err != nil {
 				err = telemetry.Error(ctx, span, err, "error uninstalling app chart after failed install")
 				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -258,7 +260,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 		}
 
-		existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 		if err != nil {
 			err = telemetry.Error(ctx, span, err, "error reading app from DB")
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
@@ -272,7 +274,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 
 		app := &models.PorterApp{
-			Name:      stackName,
+			Name:      appName,
 			ClusterID: cluster.ID,
 			ProjectID: project.ID,
 			RepoName:  request.RepoName,
@@ -312,7 +314,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// create/update the pre-deploy job chart
 		if request.OverrideRelease {
 			if preDeployJobValues == nil {
-				preDeployJobName := fmt.Sprintf("%s-r", stackName)
+				preDeployJobName := fmt.Sprintf("%s-r", appName)
 				_, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
 				if err == nil {
 					// handle exception where the user has chosen to delete the release job
@@ -326,13 +328,13 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					}
 				}
 			} else {
-				preDeployJobName := fmt.Sprintf("%s-r", stackName)
+				preDeployJobName := fmt.Sprintf("%s-r", appName)
 				helmRelease, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
 				if err != nil {
 					telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "creating-pre-deploy-job", Value: true})
 					conf, err := createReleaseJobChart(
 						ctx,
-						stackName,
+						appName,
 						preDeployJobValues,
 						c.Config().ServerConf.DefaultApplicationHelmRepoURL,
 						registries,
@@ -351,7 +353,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 						err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
 						telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
 						c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-						_, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
+						_, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
 						if uninstallChartErr != nil {
 							uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
 							c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
@@ -391,7 +393,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// update the app chart
 		conf := &helm.InstallChartConfig{
 			Chart:      chart,
-			Name:       stackName,
+			Name:       appName,
 			Namespace:  namespace,
 			Values:     values,
 			Cluster:    cluster,
@@ -409,7 +411,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 
 		// update the DB entry
-		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 		if err != nil {
 			err = telemetry.Error(ctx, span, err, "error reading app from DB")
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
@@ -533,8 +535,8 @@ func createReleaseJobChart(
 		return nil, err
 	}
 
-	releaseName := fmt.Sprintf("%s-r", stackName)
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
+	releaseName := utils.PredeployJobNameFromPorterAppName(stackName)
+	namespace := utils.NamespaceFromPorterAppName(stackName)
 
 	return &helm.InstallChartConfig{
 		Chart:      chart,

+ 5 - 5
api/server/handlers/porter_app/create_and_update_events.go

@@ -48,14 +48,14 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 		return
 	}
 
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "porter-app-name", Value: stackName},
+		telemetry.AttributeKV{Key: "porter-app-name", Value: appName},
 		telemetry.AttributeKV{Key: "porter-app-event-type", Value: string(request.Type)},
 		telemetry.AttributeKV{Key: "porter-app-event-status", Value: request.Status},
 		telemetry.AttributeKV{Key: "porter-app-event-external-source", Value: request.TypeExternalSource},
@@ -63,11 +63,11 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 	)
 
 	if request.Type == types.PorterAppEventType_Build {
-		reportBuildStatus(ctx, request, p.Config(), user, project, stackName)
+		reportBuildStatus(ctx, request, p.Config(), user, project, appName)
 	}
 
 	if request.ID == "" {
-		event, err := p.createNewAppEvent(ctx, *cluster, stackName, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
+		event, err := p.createNewAppEvent(ctx, *cluster, appName, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
 		if err != nil {
 			e := telemetry.Error(ctx, span, err, "error creating new app event")
 			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
@@ -77,7 +77,7 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 		return
 	}
 
-	event, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *request)
+	event, err := p.updateExistingAppEvent(ctx, *cluster, appName, *request)
 	if err != nil {
 		e := telemetry.Error(ctx, span, err, "error creating new app event")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))

+ 3 - 3
api/server/handlers/porter_app/create_secret_and_open_pr.go

@@ -37,7 +37,7 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
 		return
@@ -95,7 +95,7 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			Client:                 client,
 			GitRepoOwner:           request.GithubRepoOwner,
 			GitRepoName:            request.GithubRepoName,
-			StackName:              stackName,
+			StackName:              appName,
 			ProjectID:              project.ID,
 			ClusterID:              cluster.ID,
 			ServerURL:              c.Config().ServerConf.ServerURL,
@@ -131,7 +131,7 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 		if request.DeleteWorkflowFilename == "" {
 			// update DB with the PR url
-			porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+			porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("unable to get porter app db: %w", err)))
 				return

+ 2 - 2
api/server/handlers/porter_app/delete.go

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

+ 6 - 2
api/server/handlers/porter_app/get.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
 type GetPorterAppHandler struct {
@@ -29,14 +30,17 @@ func NewGetPorterAppHandler(
 
 func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(ctx, "serve-get-porter-app")
+	defer span.End()
+
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
 		return
 	}
 
-	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 75 - 0
api/server/handlers/porter_app/helm_release.go

@@ -0,0 +1,75 @@
+package porter_app
+
+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"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type PorterAppHelmReleaseGetHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewPorterAppHelmReleaseGetHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *PorterAppHelmReleaseGetHandler {
+	return &PorterAppHelmReleaseGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *PorterAppHelmReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	ctx, span := telemetry.NewSpan(ctx, "serve-get-porter-app-helm-release")
+	defer span.End()
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	version, reqErr := requestutils.GetURLParamUint(r, types.URLParamReleaseVersion)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting version from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "version", Value: version})
+
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	release, err := helmAgent.GetRelease(ctx, appName, int(version), false)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm release")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	res := &types.Release{
+		Release: release,
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 63 - 0
api/server/handlers/porter_app/helm_release_history.go

@@ -0,0 +1,63 @@
+package porter_app
+
+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"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type PorterAppHelmReleaseHistoryGetHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewPorterAppHelmReleaseHistoryGetHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *PorterAppHelmReleaseHistoryGetHandler {
+	return &PorterAppHelmReleaseHistoryGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *PorterAppHelmReleaseHistoryGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	ctx, span := telemetry.NewSpan(ctx, "serve-get-porter-app-helm-release-history")
+	defer span.End()
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	history, err := helmAgent.GetReleaseHistory(ctx, appName)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm release history")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	c.WriteResult(w, r, history)
+}

+ 3 - 3
api/server/handlers/porter_app/list_events.go

@@ -45,7 +45,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		e := telemetry.Error(ctx, span, nil, "error parsing stack name from url")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
@@ -61,7 +61,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	app, err := p.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+	app, err := p.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -78,7 +78,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 
 	for idx, appEvent := range porterAppEvents {
 		if appEvent.Status == "PROGRESSING" {
-			pae, err := p.updateExistingAppEvent(ctx, *cluster, stackName, *appEvent, user, project)
+			pae, err := p.updateExistingAppEvent(ctx, *cluster, appName, *appEvent, user, project)
 			if err != nil {
 				telemetry.Error(ctx, span, nil, "unable to update existing porter app event")
 			}

+ 4 - 1
api/server/handlers/porter_app/parse.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	porterAppUtils "github.com/porter-dev/porter/api/utils/porter_app"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/kubernetes"
@@ -77,6 +78,8 @@ type SubdomainCreateOpts struct {
 }
 
 type ParseConf struct {
+	// PorterAppName is the name of the porter app
+	PorterAppName string
 	// PorterYaml is the raw porter yaml which is used to build the values + chart for helm upgrade
 	PorterYaml []byte
 	// ImageInfo contains the repository and tag of the image to use for the helm upgrade. Kept separate from the PorterYaml because the image info
@@ -217,7 +220,7 @@ func parse(ctx context.Context, conf ParseConf) (*chart.Chart, map[string]interf
 	var preDeployJobValues map[string]interface{}
 	if application.Release != nil && application.Release.Run != nil {
 		application.Release = addLabelsToService(application.Release, conf.EnvironmentGroups, porter_app.LabelKey_PorterApplicationPreDeploy)
-		preDeployJobValues = buildPreDeployJobChartValues(application.Release, application.Env, synced_env, conf.ImageInfo, conf.InjectLauncherToStartCommand, conf.ExistingHelmValues, strings.TrimSuffix(strings.TrimPrefix(conf.Namespace, "porter-stack-"), "")+"-r", conf.UserUpdate, conf.AddCustomNodeSelector)
+		preDeployJobValues = buildPreDeployJobChartValues(application.Release, application.Env, synced_env, conf.ImageInfo, conf.InjectLauncherToStartCommand, conf.ExistingHelmValues, porterAppUtils.PredeployJobNameFromPorterAppName(conf.PorterAppName), conf.UserUpdate, conf.AddCustomNodeSelector)
 	}
 
 	return umbrellaChart, convertedValues, preDeployJobValues, nil

+ 85 - 0
api/server/handlers/porter_app/pods.go

@@ -0,0 +1,85 @@
+package porter_app
+
+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/handlers/release"
+	"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"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type PorterAppPodsGetHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewPorterAppPodsGetHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *PorterAppPodsGetHandler {
+	return &PorterAppPodsGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *PorterAppPodsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	ctx, span := telemetry.NewSpan(ctx, "serve-get-porter-app-pods")
+	defer span.End()
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	version, reqErr := requestutils.GetURLParamUint(r, types.URLParamReleaseVersion)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting version from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	helmRelease, err := helmAgent.GetRelease(ctx, appName, int(version), false)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm release")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	k8sAgent, err := c.GetAgent(r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting k8s agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	pods, err := release.GetPodsForRelease(ctx, helmRelease, k8sAgent)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting pods for release")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	c.WriteResult(w, r, pods)
+}

+ 14 - 13
api/server/handlers/porter_app/rollback.go

@@ -1,7 +1,6 @@
 package porter_app
 
 import (
-	"fmt"
 	"net/http"
 	"strings"
 
@@ -12,6 +11,7 @@ import (
 	"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"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
@@ -46,14 +46,14 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 		err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "stack-name", Value: stackName})
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "stack-name", Value: appName})
+	namespace := utils.NamespaceFromPorterAppName(appName)
 
 	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
 	if err != nil {
@@ -69,14 +69,14 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	helmReleaseFromRequestedRevision, err := helmAgent.GetRelease(ctx, stackName, request.Revision, false)
+	helmReleaseFromRequestedRevision, err := helmAgent.GetRelease(ctx, appName, request.Revision, false)
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error getting helm release for requested revision")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	latestHelmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
+	latestHelmRelease, err := helmAgent.GetRelease(ctx, appName, 0, false)
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error getting latest helm release")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -95,7 +95,7 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		imageInfo.Tag = "latest"
 	}
 
-	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error getting porter app")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -114,16 +114,17 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	chart, values, _, err := parse(
 		ctx,
 		ParseConf{
-			ImageInfo:    imageInfo,
-			ServerConfig: c.Config(),
-			ProjectID:    cluster.ProjectID,
-			Namespace:    namespace,
+			PorterAppName: appName,
+			ImageInfo:     imageInfo,
+			ServerConfig:  c.Config(),
+			ProjectID:     cluster.ProjectID,
+			Namespace:     namespace,
 			SubdomainCreateOpts: SubdomainCreateOpts{
 				k8sAgent:       k8sAgent,
 				dnsRepo:        c.Repo().DNSRecord(),
 				powerDnsClient: c.Config().PowerDNSClient,
 				appRootDomain:  c.Config().ServerConf.AppRootDomain,
-				stackName:      stackName,
+				stackName:      appName,
 			},
 			InjectLauncherToStartCommand: injectLauncher,
 			FullHelmValues:               string(valuesYaml),
@@ -137,7 +138,7 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	conf := &helm.InstallChartConfig{
 		Chart:      chart,
-		Name:       stackName,
+		Name:       appName,
 		Namespace:  namespace,
 		Values:     values,
 		Cluster:    cluster,

+ 42 - 23
api/server/handlers/release/get_all_pods.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -14,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm/grapher"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
 	"github.com/stefanmcshane/helm/pkg/release"
 	v1 "k8s.io/api/core/v1"
 )
@@ -34,8 +36,12 @@ func NewGetAllPodsHandler(
 }
 
 func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(ctx, "serve-get-all-pods-for-release")
+	defer span.End()
+
+	helmRelease, _ := ctx.Value(types.ReleaseScope).(*release.Release)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
 	agent, err := c.GetAgent(r, cluster, "")
 	if err != nil {
@@ -44,18 +50,35 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	pods, err := GetPodsForRelease(ctx, helmRelease, agent)
+	if err != nil {
+		err = fmt.Errorf("error getting pods: %w", err)
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, pods)
+}
+
+func GetPodsForRelease(ctx context.Context, helmRelease *release.Release, k8sAgent *kubernetes.Agent) ([]v1.Pod, error) {
+	ctx, span := telemetry.NewSpan(ctx, "get-all-pods-for-release")
+	defer span.End()
+
 	yamlArr := grapher.ImportMultiDocYAML([]byte(helmRelease.Manifest))
 	controllers := grapher.ParseControllers(yamlArr)
 	pods := make([]v1.Pod, 0)
 
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "num-controllers", Value: len(controllers)})
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: helmRelease.Namespace})
+
 	// get current status of each controller
 	for _, controller := range controllers {
 		controller.Namespace = helmRelease.Namespace
-		_, selector, err := getController(controller, agent)
+		_, selector, err := getController(controller, k8sAgent)
 		if err != nil {
-			err = fmt.Errorf("error getting controller %s: %w", controller.Name, err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "controller-name", Value: controller.Name})
+			err = telemetry.Error(ctx, span, err, "error getting controller")
+			return nil, err
 		}
 
 		selectors := make([]string, 0)
@@ -74,11 +97,10 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				})
 			}
 
-			jobPods, err := getPodsForJobs(agent, helmRelease.Namespace, jobLabels)
+			jobPods, err := getPodsForJobs(k8sAgent, helmRelease.Namespace, jobLabels)
 			if err != nil {
-				err = fmt.Errorf("error getting cronjob pods in namespace %s with labels %+v : %w", helmRelease.Namespace, jobLabels, err)
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-				return
+				err = telemetry.Error(ctx, span, err, "error getting cronjob pods")
+				return nil, err
 			}
 
 			pods = append(pods, jobPods...)
@@ -94,20 +116,18 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 
-		podList, err := agent.GetPodsByLabel(strings.Join(selectors, ","), helmRelease.Namespace)
+		podList, err := k8sAgent.GetPodsByLabel(strings.Join(selectors, ","), helmRelease.Namespace)
 		if err != nil {
-			err = fmt.Errorf("error getting pods in namespace %s with labels %+v : %w", helmRelease.Namespace, strings.Join(selectors, ","), err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
+			err = telemetry.Error(ctx, span, err, "error getting pods")
+			return nil, err
 		}
 
 		pods = append(pods, podList.Items...)
 
-		podList, err = agent.GetPodsByLabel(strings.Join(selectors, ","), "default")
+		podList, err = k8sAgent.GetPodsByLabel(strings.Join(selectors, ","), "default")
 		if err != nil {
-			err = fmt.Errorf("error getting pods in namespace %s with labels %+v : %w", helmRelease.Namespace, strings.Join(selectors, ","), err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-			return
+			err = telemetry.Error(ctx, span, err, "error getting pods")
+			return nil, err
 		}
 
 		pods = append(pods, podList.Items...)
@@ -121,16 +141,15 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Val: fmt.Sprintf("%d", helmRelease.Version),
 	})
 
-	jobPods, err := getPodsForJobs(agent, helmRelease.Namespace, labels)
+	jobPods, err := getPodsForJobs(k8sAgent, helmRelease.Namespace, labels)
 	if err != nil {
-		err = fmt.Errorf("error getting cronjob pods in namespace %s with labels %+v : %w", helmRelease.Namespace, labels, err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		err = telemetry.Error(ctx, span, err, "error getting cronjob pods")
+		return nil, err
 	}
 
 	pods = append(pods, jobPods...)
 
-	c.WriteResult(w, r, pods)
+	return pods, nil
 }
 
 func getPodsForJobs(agent *kubernetes.Agent, namespace string, labels []kubernetes.Label) ([]v1.Pod, error) {

+ 195 - 23
api/server/router/porter_app.go

@@ -11,21 +11,21 @@ import (
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewStackScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+func NewPorterAppScopedRegisterer(children ...*router.Registerer) *router.Registerer {
 	return &router.Registerer{
-		GetRoutes: GetStackScopedRoutes,
+		GetRoutes: GetPorterAppScopedRoutes,
 		Children:  children,
 	}
 }
 
-func GetStackScopedRoutes(
+func GetPorterAppScopedRoutes(
 	r chi.Router,
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*router.Registerer,
 ) []*router.Route {
-	routes, projPath := getStackRoutes(r, config, basePath, factory)
+	routes, projPath := getPorterAppRoutes(r, config, basePath, factory)
 
 	if len(children) > 0 {
 		r.Route(projPath.RelativePath, func(r chi.Router) {
@@ -40,13 +40,13 @@ func GetStackScopedRoutes(
 	return routes
 }
 
-func getStackRoutes(
+func getPorterAppRoutes(
 	r chi.Router,
 	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*router.Route, *types.Path) {
-	relPath := "/stacks"
+	relPath := "/applications"
 
 	newPath := &types.Path{
 		Parent:       basePath,
@@ -55,14 +55,14 @@ func getStackRoutes(
 
 	var routes []*router.Route
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/{name} -> porter_app.NewPorterAppGetHandler
 	getPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -83,7 +83,91 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppListHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/{name}/releases/{version} -> porter_app.NewPorterAppReleaseGetHandler
+	getPorterAppHelmReleaseEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/releases/{%s}", relPath, types.URLParamPorterAppName, types.URLParamReleaseVersion),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterAppHelmReleaseHandler := porter_app.NewPorterAppHelmReleaseGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterAppHelmReleaseEndpoint,
+		Handler:  getPorterAppHelmReleaseHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/{name}/release-history -> porter_app.NewPorterAppHelmReleaseHistoryGetHandler
+	getPorterAppHelmReleaseHistoryGetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/release-history", relPath, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterAppHelmReleaseHistoryGetHandler := porter_app.NewPorterAppHelmReleaseHistoryGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterAppHelmReleaseHistoryGetEndpoint,
+		Handler:  getPorterAppHelmReleaseHistoryGetHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/{name}/releases/{version}/pods/all -> porter_app.NewPorterAppPodsGetHandler
+	getPorterAppPodsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/releases/{%s}/pods/all", relPath, types.URLParamPorterAppName, types.URLParamReleaseVersion),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterAppPodsHandler := porter_app.NewPorterAppPodsGetHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterAppPodsEndpoint,
+		Handler:  getPorterAppPodsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications -> porter_app.NewPorterAppListHandler
 	listPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbList,
@@ -111,14 +195,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/stacks -> release.NewDeletePorterAppByNameHandler
+	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name} -> release.NewDeletePorterAppByNameHandler
 	deletePorterAppByNameEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbDelete,
 			Method: types.HTTPVerbDelete,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -140,14 +224,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack} -> porter_app.NewCreatePorterAppHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name} -> porter_app.NewCreatePorterAppHandler
 	createPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -169,14 +253,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/rollback -> porter_app.NewRollbackPorterAppHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name}/rollback -> porter_app.NewRollbackPorterAppHandler
 	rollbackPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}/rollback", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}/rollback", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -198,14 +282,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> porter_app.NewOpenStackPRHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name}/pr -> porter_app.NewOpenStackPRHandler
 	createSecretAndOpenGitHubPullRequestEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}/pr", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}/pr", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -227,14 +311,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> porter_app.NewPorterAppEventListHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name}/events -> porter_app.NewPorterAppEventListHandler
 	listPorterAppEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbList,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -255,14 +339,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> porter_app.NewCreatePorterAppEventHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/{name}/events -> porter_app.NewCreatePorterAppEventHandler
 	createPorterAppEventEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamStackName),
+				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamPorterAppName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -312,7 +396,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/analytics -> porter_app.NewPorterAppAnalyticsHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/analytics -> porter_app.NewPorterAppAnalyticsHandler
 	porterAppAnalyticsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,
@@ -341,7 +425,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/logs -> cluster.NewGetChartLogsWithinTimeRangeHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/applications/logs -> cluster.NewGetChartLogsWithinTimeRangeHandler
 	getChartLogsWithinTimeRangeEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
@@ -370,5 +454,93 @@ func getStackRoutes(
 		Router:   r,
 	})
 
+	// TODO: remove these three endpoints once these three 'stacks' routes are no longer used in telemetry
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
+	LEGACY_getPorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/stacks/{%s}", types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	LEGACY_getPorterAppHandler := porter_app.NewGetPorterAppHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: LEGACY_getPorterAppEndpoint,
+		Handler:  LEGACY_getPorterAppHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{porter_app_name} -> porter_app.NewCreatePorterAppHandler
+	LEGACY_createPorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/stacks/{%s}", types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	LEGACY_createPorterAppHandler := porter_app.NewCreatePorterAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: LEGACY_createPorterAppEndpoint,
+		Handler:  LEGACY_createPorterAppHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> porter_app.NewCreatePorterAppEventHandler
+	LEGACY_createPorterAppEventEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/stacks/{%s}/events", types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	LEGACY_createPorterAppEventHandler := porter_app.NewCreateUpdatePorterAppEventHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: LEGACY_createPorterAppEventEndpoint,
+		Handler:  LEGACY_createPorterAppEventHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 1 - 1
api/server/router/router.go

@@ -31,7 +31,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	releaseRegisterer := NewReleaseScopedRegisterer()
 	namespaceRegisterer := NewNamespaceScopedRegisterer(releaseRegisterer)
 	clusterIntegrationRegisterer := NewClusterIntegrationScopedRegisterer()
-	stackRegisterer := NewStackScopedRegisterer()
+	stackRegisterer := NewPorterAppScopedRegisterer()
 	clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer, clusterIntegrationRegisterer, stackRegisterer)
 	infraRegisterer := NewInfraScopedRegisterer()
 	gitInstallationRegisterer := NewGitInstallationScopedRegisterer()

+ 1 - 1
api/types/request.go

@@ -49,8 +49,8 @@ const (
 	URLParamWildcard              URLParam = "*"
 	URLParamIntegrationID         URLParam = "integration_id"
 	URLParamAPIContractRevisionID URLParam = "contract_revision_id"
-	URLParamStackName             URLParam = "stack_name"
 	URLParamStackEventID          URLParam = "stack_event_id"
+	URLParamPorterAppName         URLParam = "porter_app_name"
 	URLParamPorterAppEventID      URLParam = "porter_app_event_id"
 )
 

+ 18 - 0
api/utils/porter_app/namespace.go

@@ -0,0 +1,18 @@
+package porter_app
+
+import (
+	"fmt"
+	"strings"
+)
+
+func NamespaceFromPorterAppName(porterAppName string) string {
+	return fmt.Sprintf("porter-stack-%s", porterAppName)
+}
+
+func PorterAppNameFromNamespace(namespace string) string {
+	return strings.TrimPrefix(namespace, "porter-stack-")
+}
+
+func PredeployJobNameFromPorterAppName(porterAppName string) string {
+	return fmt.Sprintf("%s-r", porterAppName)
+}

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx

@@ -91,7 +91,7 @@ const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
         } else {
           return (
             <Text color="helper">
-              Deploying <Code>{event.metadata.image_tag}</Code> to {Object.keys(event.metadata.service_status).length} service{Object.keys(event.metadata.service_status).length === 1 ? "" : "s"}...
+              Deploying <Code>{event.metadata.image_tag}</Code>...
             </Text>
           );
         }

+ 14 - 13
dashboard/src/shared/api.tsx

@@ -172,7 +172,7 @@ const getPorterApps = baseApi<
   }
 >("GET", (pathParams) => {
   let { project_id, cluster_id } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications`;
 });
 
 const getPorterApp = baseApi<
@@ -184,7 +184,7 @@ const getPorterApp = baseApi<
   }
 >("GET", (pathParams) => {
   let { project_id, cluster_id, name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${name}`;
 });
 
 const getPorterAppEvent = baseApi<
@@ -208,7 +208,7 @@ const createPorterApp = baseApi<
   }
 >("POST", (pathParams) => {
   let { project_id, cluster_id, stack_name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}`;
 });
 
 const deletePorterApp = baseApi<
@@ -220,7 +220,7 @@ const deletePorterApp = baseApi<
   }
 >("DELETE", (pathParams) => {
   let { project_id, cluster_id, name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${name}`;
 });
 
 const rollbackPorterApp = baseApi<
@@ -234,7 +234,7 @@ const rollbackPorterApp = baseApi<
   }
 >("POST", (pathParams) => {
   let { project_id, cluster_id, stack_name } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/rollback`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/rollback`;
 });
 
 const getLogsWithinTimeRange = baseApi<
@@ -255,7 +255,7 @@ const getLogsWithinTimeRange = baseApi<
 >(
   "GET",
   ({ project_id, cluster_id }) =>
-    `/api/projects/${project_id}/clusters/${cluster_id}/stacks/logs`
+    `/api/projects/${project_id}/clusters/${cluster_id}/applications/logs`
 );
 
 const getFeedEvents = baseApi<
@@ -268,7 +268,7 @@ const getFeedEvents = baseApi<
   }
 >("GET", (pathParams) => {
   let { project_id, cluster_id, stack_name, page } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/events?page=${page || 1}`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1}`;
 });
 
 const createEnvironment = baseApi<
@@ -2513,7 +2513,7 @@ const updateStackStep = baseApi<
   }
 >("POST", (pathParams) => {
   let { project_id, cluster_id } = pathParams;
-  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/analytics`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/applications/analytics`;
 });
 
 // STACKS
@@ -2709,7 +2709,7 @@ const createSecretAndOpenGitHubPullRequest = baseApi<
 >(
   "POST",
   ({ project_id, cluster_id, stack_name }) =>
-    `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/pr`
+    `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/pr`
 );
 
 // Bundle export to allow default api import (api.<method> is more readable)
@@ -2746,14 +2746,18 @@ export default {
   createPasswordResetVerify,
   createPasswordResetFinalize,
   createProject,
-  // PORTER APP
+  // ------------ PORTER APP -----------
   getPorterApps,
   getPorterApp,
   getPorterAppEvent,
   createPorterApp,
   deletePorterApp,
   rollbackPorterApp,
+  createSecretAndOpenGitHubPullRequest,
   getLogsWithinTimeRange,
+  getFeedEvents,
+  updateStackStep,
+  // -----------------------------------
   createConfigMap,
   deleteCluster,
   deleteConfigMap,
@@ -2930,10 +2934,8 @@ export default {
   createContract,
   getContracts,
   deleteContract,
-  createSecretAndOpenGitHubPullRequest,
   // TRACKING
   updateOnboardingStep,
-  updateStackStep,
   // STACKS
   listStacks,
   getStack,
@@ -2947,7 +2949,6 @@ export default {
   removeStackAppResource,
   addStackEnvGroup,
   removeStackEnvGroup,
-  getFeedEvents,
 
   // STATUS
   getGithubStatus,