Ver código fonte

Merge branch 'master' into healthChecksStacks

Soham Dessai 3 anos atrás
pai
commit
4085338680
36 arquivos alterados com 541 adições e 245 exclusões
  1. 1 1
      api/server/authz/release.go
  2. 4 4
      api/server/handlers/cluster/install_agent.go
  3. 4 3
      api/server/handlers/cluster/upgrade_agent.go
  4. 2 1
      api/server/handlers/namespace/create_env_group.go
  5. 2 1
      api/server/handlers/namespace/list_releases.go
  6. 2 2
      api/server/handlers/release/create.go
  7. 9 7
      api/server/handlers/release/create_addon.go
  8. 2 1
      api/server/handlers/release/delete.go
  9. 2 1
      api/server/handlers/release/get_history.go
  10. 3 2
      api/server/handlers/release/update_image_batch.go
  11. 2 1
      api/server/handlers/release/update_rollback.go
  12. 3 2
      api/server/handlers/release/upgrade.go
  13. 3 2
      api/server/handlers/release/upgrade_webhook.go
  14. 8 6
      api/server/handlers/stack/helpers.go
  15. 15 11
      api/server/handlers/stacks/create_porter_app.go
  16. 7 4
      api/server/handlers/stacks/parse.go
  17. 2 1
      api/server/handlers/template/get.go
  18. 2 1
      api/server/handlers/template/get_upgrade_notes.go
  19. 0 11
      api/server/handlers/user/create.go
  20. 2 1
      api/server/handlers/v1/env_group/create.go
  21. 3 2
      api/server/handlers/v1/release/upgrade.go
  22. 2 1
      api/server/handlers/v1/template/get.go
  23. 2 1
      api/server/handlers/v1/template/get_upgrade_notes.go
  24. 246 95
      dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx
  25. 4 3
      dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx
  26. 1 1
      dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx
  27. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx
  28. 13 26
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts
  29. 20 3
      ee/api/server/handlers/invite/create.go
  30. 138 29
      internal/helm/agent.go
  31. 5 4
      internal/helm/agent_test.go
  32. 21 9
      internal/helm/loader/loader.go
  33. 2 1
      internal/helm/repo/repo.go
  34. 2 1
      internal/kubernetes/envgroup/create.go
  35. 2 2
      internal/opa/opa.go
  36. 3 2
      internal/templater/helm/values/writer.go

+ 1 - 1
api/server/authz/release.go

@@ -50,7 +50,7 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	// get the version for the application
 	version, _ := requestutils.GetURLParamUint(r, types.URLParamReleaseVersion)
 
-	release, err := helmAgent.GetRelease(name, int(version), false)
+	release, err := helmAgent.GetRelease(context.Background(), name, int(version), false)
 	if err != nil {
 		// ugly casing since at the time of this commit Helm doesn't have an errors package.
 		// so we rely on the Helm error containing "not found"

+ 4 - 4
api/server/handlers/cluster/install_agent.go

@@ -65,7 +65,7 @@ func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	chart, err := loader.LoadChartPublic(c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
+	chart, err := loader.LoadChartPublic(context.Background(), c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -139,7 +139,7 @@ func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Values:    porterAgentValues,
 	}
 
-	_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+	_, err = helmAgent.InstallChart(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
@@ -172,13 +172,13 @@ func checkAndDeleteOlderAgent(k8sAgent *kubernetes.Agent, helmAgent *helm.Agent)
 	}
 
 	// detect if the `porter-agent` release is installed
-	helmRelease, err := helmAgent.GetRelease("porter-agent", 0, false)
+	helmRelease, err := helmAgent.GetRelease(context.Background(), "porter-agent", 0, false)
 
 	if err != nil || helmRelease == nil {
 		return nil
 	}
 
-	_, err = helmAgent.UninstallChart("porter-agent")
+	_, err = helmAgent.UninstallChart(context.Background(), "porter-agent")
 
 	if err != nil {
 		return err

+ 4 - 3
api/server/handlers/cluster/upgrade_agent.go

@@ -1,6 +1,7 @@
 package cluster
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 
@@ -39,13 +40,13 @@ func (c *UpgradeAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	currRelease, err := helmAgent.GetRelease("porter-agent", 0, false)
+	currRelease, err := helmAgent.GetRelease(context.Background(), "porter-agent", 0, false)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	chart, err := loader.LoadChartPublic(c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
+	chart, err := loader.LoadChartPublic(context.Background(), c.Config().ServerConf.DefaultAddonHelmRepoURL, "porter-agent", "")
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -56,7 +57,7 @@ func (c *UpgradeAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	// TODO: update values
 	// newValues["redis"] =
 
-	_, err = helmAgent.UpgradeReleaseByValues(&helm.UpgradeReleaseConfig{
+	_, err = helmAgent.UpgradeReleaseByValues(context.Background(), &helm.UpgradeReleaseConfig{
 		Chart:      chart,
 		Name:       "porter-agent",
 		Values:     newValues,

+ 2 - 1
api/server/handlers/namespace/create_env_group.go

@@ -1,6 +1,7 @@
 package namespace
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -185,7 +186,7 @@ func rolloutApplications(
 				Values:     newConfig,
 			}
 
-			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection, false)
+			_, err = helmAgent.UpgradeReleaseByValues(context.Background(), conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection, false)
 
 			if err != nil {
 				mu.Lock()

+ 2 - 1
api/server/handlers/namespace/list_releases.go

@@ -1,6 +1,7 @@
 package namespace
 
 import (
+	"context"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -48,7 +49,7 @@ func (c *ListReleasesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	releases, err := helmAgent.ListReleases(namespace, request.ReleaseListFilter)
+	releases, err := helmAgent.ListReleases(context.Background(), namespace, request.ReleaseListFilter)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 2 - 2
api/server/handlers/release/create.go

@@ -119,7 +119,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		request.TemplateVersion = ""
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, request.TemplateName, request.TemplateVersion)
+	chart, err := loader.LoadChartPublic(ctx, request.RepoURL, request.TemplateName, request.TemplateVersion)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error loading public chart")))
 		return
@@ -154,7 +154,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Registries: registries,
 	}
 
-	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+	helmRelease, err := helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			telemetry.Error(ctx, span, err, "error installing a new chart"),

+ 9 - 7
api/server/handlers/release/create_addon.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 
@@ -91,7 +92,7 @@ func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Registries: registries,
 	}
 
-	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+	helmRelease, err := helmAgent.InstallChart(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			fmt.Errorf("error installing a new chart: %s", err.Error()),
@@ -124,7 +125,7 @@ type LoadAddonChartOpts struct {
 func LoadChart(config *config.Config, opts *LoadAddonChartOpts) (*chart.Chart, error) {
 	// if the chart repo url is one of the specified application/addon charts, just load public
 	if opts.RepoURL == config.ServerConf.DefaultAddonHelmRepoURL || opts.RepoURL == config.ServerConf.DefaultApplicationHelmRepoURL {
-		return loader.LoadChartPublic(opts.RepoURL, opts.TemplateName, opts.TemplateVersion)
+		return loader.LoadChartPublic(context.Background(), opts.RepoURL, opts.TemplateName, opts.TemplateVersion)
 	} else {
 		// load the helm repos in the project
 		hrs, err := config.Repo.HelmRepo().ListHelmReposByProjectID(opts.ProjectID)
@@ -141,12 +142,13 @@ func LoadChart(config *config.Config, opts *LoadAddonChartOpts) (*chart.Chart, e
 						return nil, err
 					}
 
-					return loader.LoadChart(&loader.BasicAuthClient{
-						Username: string(basic.Username),
-						Password: string(basic.Password),
-					}, hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
+					return loader.LoadChart(context.Background(),
+						&loader.BasicAuthClient{
+							Username: string(basic.Username),
+							Password: string(basic.Password),
+						}, hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
 				} else {
-					return loader.LoadChartPublic(hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
+					return loader.LoadChartPublic(context.Background(), hr.RepoURL, opts.TemplateName, opts.TemplateVersion)
 				}
 			}
 		}

+ 2 - 1
api/server/handlers/release/delete.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -43,7 +44,7 @@ func (c *DeleteReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	_, err = helmAgent.UninstallChart(helmRelease.Name)
+	_, err = helmAgent.UninstallChart(context.Background(), helmRelease.Name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 2 - 1
api/server/handlers/release/get_history.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -40,7 +41,7 @@ func (c *GetReleaseHistoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	// get the name of the application
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
-	history, err := helmAgent.GetReleaseHistory(name)
+	history, err := helmAgent.GetReleaseHistory(context.Background(), name)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 3 - 2
api/server/handlers/release/update_image_batch.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -75,7 +76,7 @@ func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		go func() {
 			defer wg.Done()
 			// read release via agent
-			rel, err := helmAgent.GetRelease(releases[index].Name, 0, false)
+			rel, err := helmAgent.GetRelease(context.Background(), releases[index].Name, 0, false)
 			if err != nil {
 				// if this is a release not found error, just return - the release has likely been deleted from the underlying
 				// cluster but has not been deleted from the Porter database yet
@@ -104,7 +105,7 @@ func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 					Values:     rel.Config,
 				}
 
-				_, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
+				_, err = helmAgent.UpgradeReleaseByValues(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
 
 				if err != nil {
 					// if this is a release not found error, just return - the release has likely been deleted from the underlying

+ 2 - 1
api/server/handlers/release/update_rollback.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 
@@ -48,7 +49,7 @@ func (c *RollbackReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	err = helmAgent.RollbackRelease(helmRelease.Name, request.Revision)
+	err = helmAgent.RollbackRelease(context.Background(), helmRelease.Name, request.Revision)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 3 - 2
api/server/handlers/release/upgrade.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -116,7 +117,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 	// if LatestRevision is set, check that the revision matches the latest revision in the database
 	if request.LatestRevision != 0 {
-		currHelmRelease, err := helmAgent.GetRelease(helmRelease.Name, 0, false)
+		currHelmRelease, err := helmAgent.GetRelease(context.Background(), helmRelease.Name, 0, false)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf("could not retrieve latest revision"),
@@ -153,7 +154,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 	}
 
-	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf,
+	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(context.Background(), conf, request.Values, c.Config().DOConf,
 		c.Config().ServerConf.DisablePullSecretsInjection, request.IgnoreDependencies)
 
 	if upgradeErr == nil && newHelmRelease != nil {

+ 3 - 2
api/server/handlers/release/upgrade_webhook.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -84,7 +85,7 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	rel, err := helmAgent.GetRelease(release.Name, 0, true)
+	rel, err := helmAgent.GetRelease(context.Background(), release.Name, 0, true)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -168,7 +169,7 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		),
 	}
 
-	rel, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
+	rel, err = helmAgent.UpgradeReleaseByValues(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
 
 	if err != nil {
 		notifyOpts.Status = notifier.StatusHelmFailed

+ 8 - 6
api/server/handlers/stack/helpers.go

@@ -1,6 +1,8 @@
 package stack
 
 import (
+	"context"
+
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
@@ -28,7 +30,7 @@ func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
 		opts.request.TemplateVersion = ""
 	}
 
-	chart, err := loader.LoadChartPublic(opts.request.TemplateRepoURL, opts.request.TemplateName, opts.request.TemplateVersion)
+	chart, err := loader.LoadChartPublic(context.Background(), opts.request.TemplateRepoURL, opts.request.TemplateName, opts.request.TemplateVersion)
 	if err != nil {
 		return nil, err
 	}
@@ -53,7 +55,7 @@ func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
 		"revision": opts.stackRevision,
 	}
 
-	return opts.helmAgent.InstallChart(conf, opts.config.DOConf, opts.config.ServerConf.DisablePullSecretsInjection)
+	return opts.helmAgent.InstallChart(context.Background(), conf, opts.config.DOConf, opts.config.ServerConf.DisablePullSecretsInjection)
 }
 
 type rollbackAppResourceOpts struct {
@@ -63,7 +65,7 @@ type rollbackAppResourceOpts struct {
 }
 
 func rollbackAppResource(opts *rollbackAppResourceOpts) error {
-	return opts.helmAgent.RollbackRelease(opts.name, int(opts.helmRevisionID))
+	return opts.helmAgent.RollbackRelease(context.Background(), opts.name, int(opts.helmRevisionID))
 }
 
 type updateAppResourceTagOpts struct {
@@ -82,7 +84,7 @@ type updateAppResourceTagOpts struct {
 
 func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
 	// read the current release to get the current values
-	rel, err := opts.helmAgent.GetRelease(opts.name, 0, true)
+	rel, err := opts.helmAgent.GetRelease(context.Background(), opts.name, 0, true)
 	if err != nil {
 		return err
 	}
@@ -104,7 +106,7 @@ func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
 		StackRevision: opts.stackRevision,
 	}
 
-	_, err = opts.helmAgent.UpgradeReleaseByValues(conf, opts.config.DOConf,
+	_, err = opts.helmAgent.UpgradeReleaseByValues(context.Background(), conf, opts.config.DOConf,
 		opts.config.ServerConf.DisablePullSecretsInjection, false)
 
 	return err
@@ -116,7 +118,7 @@ type deleteAppResourceOpts struct {
 }
 
 func deleteAppResource(opts *deleteAppResourceOpts) error {
-	_, err := opts.helmAgent.UninstallChart(opts.name)
+	_, err := opts.helmAgent.UninstallChart(context.Background(), opts.name)
 
 	return err
 }

+ 15 - 11
api/server/handlers/stacks/create_porter_app.go

@@ -1,6 +1,7 @@
 package stacks
 
 import (
+	"context"
 	"encoding/base64"
 	"fmt"
 	"net/http"
@@ -74,7 +75,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	helmRelease, err := helmAgent.GetRelease(stackName, 0, false)
+	helmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
 	shouldCreate := err != nil
 
 	porterYamlBase64 := request.PorterYAMLBase64
@@ -149,6 +150,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
 		if request.OverrideRelease && releaseJobValues != nil {
 			conf, err := createReleaseJobChart(
+				ctx,
 				stackName,
 				releaseJobValues,
 				c.Config().ServerConf.DefaultApplicationHelmRepoURL,
@@ -160,10 +162,10 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
 				return
 			}
-			_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+			_, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
-				_, err = helmAgent.UninstallChart(fmt.Sprintf("%s-r", stackName))
+				_, err = helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
 				if err != nil {
 					c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
 				}
@@ -182,11 +184,11 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 
 		// create the app chart
-		_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+		_, err = helmAgent.InstallChart(ctx, 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)
+			_, err = helmAgent.UninstallChart(ctx, stackName)
 			if err != nil {
 				c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling chart: %w", err)))
 			}
@@ -233,10 +235,11 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// create/update the release job chart
 		if request.OverrideRelease && releaseJobValues != nil {
 			releaseJobName := fmt.Sprintf("%s-r", stackName)
-			helmRelease, err := helmAgent.GetRelease(releaseJobName, 0, false)
+			helmRelease, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
 			if err != nil {
 				// here the user has created a release job for an already created app, so we need to create and install  the release job chart
 				conf, err := createReleaseJobChart(
+					ctx,
 					stackName,
 					releaseJobValues,
 					c.Config().ServerConf.DefaultApplicationHelmRepoURL,
@@ -248,10 +251,10 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
 					return
 				}
-				_, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+				_, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 				if err != nil {
 					c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
-					_, err = helmAgent.UninstallChart(fmt.Sprintf("%s-r", stackName))
+					_, err = helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
 					if err != nil {
 						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
 					}
@@ -265,7 +268,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					Registries: registries,
 					Values:     releaseJobValues,
 				}
-				_, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
+				_, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
 				if err != nil {
 					c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error upgrading release job chart: %w", err)))
 					return
@@ -285,7 +288,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 
 		// update the chart
-		_, err = helmAgent.UpgradeInstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
+		_, err = helmAgent.UpgradeInstallChart(ctx, 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
@@ -346,6 +349,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 }
 
 func createReleaseJobChart(
+	ctx context.Context,
 	stackName string,
 	values map[string]interface{},
 	repoUrl string,
@@ -353,7 +357,7 @@ func createReleaseJobChart(
 	cluster *models.Cluster,
 	repo repository.Repository,
 ) (*helm.InstallChartConfig, error) {
-	chart, err := loader.LoadChartPublic(repoUrl, "job", "")
+	chart, err := loader.LoadChartPublic(ctx, repoUrl, "job", "")
 	if err != nil {
 		return nil, err
 	}

+ 7 - 4
api/server/handlers/stacks/parse.go

@@ -173,11 +173,14 @@ func buildReleaseValues(release *App, env map[string]string, imageInfo types.Ima
 	}
 
 	// prepend launcher if we need to
-	if injectLauncher && release.Run != nil && !strings.HasPrefix(*release.Run, "launcher") && !strings.HasPrefix(*release.Run, "/cnb/lifecycle/launcher") {
-		if helm_values["container"] == nil {
-			helm_values["container"] = map[string]interface{}{}
+	if helm_values["container"] != nil {
+		containerMap := helm_values["container"].(map[string]interface{})
+		if containerMap["command"] != nil {
+			command := containerMap["command"].(string)
+			if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
+				containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
+			}
 		}
-		helm_values["container"].(map[string]interface{})["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", *release.Run)
 	}
 
 	return helm_values

+ 2 - 1
api/server/handlers/template/get.go

@@ -1,6 +1,7 @@
 package template
 
 import (
+	"context"
 	"net/http"
 	"strings"
 
@@ -49,7 +50,7 @@ func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		request.RepoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+	chart, err := loader.LoadChartPublic(context.Background(), request.RepoURL, name, version)
 	if err != nil {
 		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 2 - 1
api/server/handlers/template/get_upgrade_notes.go

@@ -1,6 +1,7 @@
 package template
 
 import (
+	"context"
 	"net/http"
 	"strings"
 
@@ -51,7 +52,7 @@ func (t *TemplateGetUpgradeNotesHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		prevVersion = "v0.0.0"
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+	chart, err := loader.LoadChartPublic(context.Background(), request.RepoURL, name, version)
 	if err != nil {
 		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 0 - 11
api/server/handlers/user/create.go

@@ -1,12 +1,9 @@
 package user
 
 import (
-	"context"
 	"fmt"
 	"net/http"
 
-	"github.com/porter-dev/porter/internal/telemetry"
-
 	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -34,14 +31,6 @@ func NewUserCreateHandler(
 }
 
 func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	tracer, _ := telemetry.InitTracer(context.Background(), u.Config().TelemetryConfig)
-	defer tracer.Shutdown()
-
-	// just for demonstration purposes
-	_, span := telemetry.NewSpan(context.Background(), "create-new-user")
-	defer span.End()
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "hello-world", Value: "hello, world!"})
-
 	request := &types.CreateUserRequest{}
 
 	ok := u.DecodeAndValidate(w, r, request)

+ 2 - 1
api/server/handlers/v1/env_group/create.go

@@ -1,6 +1,7 @@
 package env_group
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -201,7 +202,7 @@ func rolloutApplications(
 				Values:     newConfig,
 			}
 
-			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection, false)
+			_, err = helmAgent.UpgradeReleaseByValues(context.Background(), conf, config.DOConf, config.ServerConf.DisablePullSecretsInjection, false)
 
 			if err != nil {
 				mu.Lock()

+ 3 - 2
api/server/handlers/v1/release/upgrade.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -118,7 +119,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 	// if LatestRevision is set, check that the revision matches the latest revision in the database
 	if request.LatestRevision != 0 {
-		currHelmRelease, err := helmAgent.GetRelease(helmRelease.Name, 0, false)
+		currHelmRelease, err := helmAgent.GetRelease(context.Background(), helmRelease.Name, 0, false)
 		if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf("could not retrieve latest revision"),
@@ -138,7 +139,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 	}
 
-	newHelmRelease, upgradeErr := helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
+	newHelmRelease, upgradeErr := helmAgent.UpgradeReleaseByValues(context.Background(), conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
 
 	if upgradeErr == nil && newHelmRelease != nil {
 		helmRelease = newHelmRelease

+ 2 - 1
api/server/handlers/v1/template/get.go

@@ -1,6 +1,7 @@
 package template
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -81,7 +82,7 @@ func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		version = ""
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+	chart, err := loader.LoadChartPublic(context.Background(), request.RepoURL, name, version)
 	if err != nil {
 		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 2 - 1
api/server/handlers/v1/template/get_upgrade_notes.go

@@ -1,6 +1,7 @@
 package template
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"strings"
@@ -83,7 +84,7 @@ func (t *TemplateGetUpgradeNotesHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		prevVersion = "v0.0.0"
 	}
 
-	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+	chart, err := loader.LoadChartPublic(context.Background(), request.RepoURL, name, version)
 	if err != nil {
 		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return

+ 246 - 95
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useEffect, useState, useContext, useMemo } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
@@ -16,12 +16,21 @@ import {
   getAvailabilityStacks,
 } from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
 import Spacer from "components/porter/Spacer";
+import { timeFormat } from "d3-time-format";
+import AnimateHeight, { Height } from "react-animate-height";
+import { ControllerTabPodType } from "./status/ControllerTab";
+import _ from "lodash";
 
 type Props = {
   chart: any;
   service: any;
 };
 
+interface ErrorMessage {
+  revision: string;
+  message: string;
+}
+
 const StatusFooter: React.FC<Props> = ({
   chart,
   service,
@@ -31,10 +40,10 @@ const StatusFooter: React.FC<Props> = ({
   const [available, setAvailable] = React.useState<number>(0);
   const [total, setTotal] = React.useState<number>(0);
   const [stale, setStale] = React.useState<number>(0);
-
-  useEffect(() => {
-    // Do something
-  }, []);
+  const [unavailable, setUnavailable] = React.useState<number>(0);
+  const [height, setHeight] = useState<Height>(0);
+  const [expanded, setExpanded] = useState<boolean>(false);
+  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
 
   const {
     newWebsocket,
@@ -43,7 +52,7 @@ const StatusFooter: React.FC<Props> = ({
     closeWebsocket,
   } = useWebsockets();
 
-  const getSelectors = () => {
+  const selectors = useMemo(() => {
     let ml =
       controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
     let i = 1;
@@ -56,14 +65,13 @@ const StatusFooter: React.FC<Props> = ({
       i += 1;
     }
     return selector;
-  };
+  }, [controller]);
 
   useEffect(() => {
-    const selectors = getSelectors();
-
+    updatePods();
     if (selectors.length > 0) {
       // updatePods();
-      [controller?.kind].forEach((kind) => {
+      [controller?.kind, "pod"].forEach((kind) => {
         setupWebsocket(kind, controller?.metadata?.uid, selectors);
       });
       return () => closeAllWebsockets();
@@ -131,48 +139,31 @@ const StatusFooter: React.FC<Props> = ({
     options.onopen = () => {
     };
 
-    options.onmessage = (evt: MessageEvent) => {
+    options.onmessage = async (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
       let object = event.Object;
       object.metadata.kind = event.Kind;
 
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
       // update pods no matter what if ws message is a pod event.
       // If controller event, check if ws message corresponds to the designated controller in props.
       if (event.Kind != "pod" && object.metadata.uid !== controllerUid) {
         return;
       }
 
-      if (event.event_type == "ADD" && total == 0) {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
+      if (event.Kind === "deployment") {
+        let [available, total, stale, unavailable] = getAvailabilityStacks(object);
 
         setAvailable(available);
         setTotal(total);
         setStale(stale);
+        setUnavailable(unavailable);
         return;
       }
-
-      // Make a new API call to update pods only when the event type is UPDATE
-      if (event.event_type !== "UPDATE") {
-        return;
-      }
-
-      // testing hot reload
-
-      if (event.Kind != "pod") {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
-
-        setAvailable(available);
-        setTotal(total);
-        setStale(stale);
-        return;
-      }
-      // updatePods();
+      await updatePods();
     };
 
     options.onclose = () => {
@@ -187,73 +178,157 @@ const StatusFooter: React.FC<Props> = ({
     openWebsocket(kind);
   };
 
+  const replicaSetArray = useMemo(() => {
+    setExpanded(false);
+    setHeight(0);
+    const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"])
+      .reverse()
+      .reduce<Array<Array<ControllerTabPodType>>>(function (
+        prev,
+        currentPod,
+        i
+      ) {
+        if (
+          !i ||
+          prev[prev.length - 1][0].replicaSetName !== currentPod.replicaSetName
+        ) {
+          return prev.concat([[currentPod]]);
+        }
+        prev[prev.length - 1].push(currentPod);
+        return prev;
+      },
+        []);
+
+    return podsDividedByReplicaSet;
+  }, [pods]);
+
   const percentage = Number(1 - available / total).toLocaleString(undefined, {
     style: "percent",
     minimumFractionDigits: 2,
   });
 
+  const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
+
+  const updatePods = async () => {
+    try {
+      const res = await api.getMatchingPods(
+        "<token>",
+        {
+          namespace: controller?.metadata?.namespace,
+          selectors: [selectors],
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+      const data = res?.data as any[];
+      let newPods = data
+        // Parse only data that we need
+        .map((pod: any) => {
+          const replicaSetName =
+            Array.isArray(pod?.metadata?.ownerReferences) &&
+            pod?.metadata?.ownerReferences[0]?.name;
+          const containerStatus =
+            Array.isArray(pod?.status?.containerStatuses) &&
+            pod?.status?.containerStatuses[0];
+
+          const restartCount = containerStatus
+            ? containerStatus.restartCount
+            : "N/A";
+
+          const podAge = formatCreationTimestamp(
+            new Date(pod?.metadata?.creationTimestamp)
+          );
+
+          // console.log(containerStatus)
+          const crashLoopReason = containerStatus?.lastState?.terminated?.message ?? "";
+
+          return {
+            namespace: pod?.metadata?.namespace,
+            name: pod?.metadata?.name,
+            phase: pod?.status?.phase,
+            status: pod?.status,
+            replicaSetName,
+            restartCount,
+            containerStatus,
+            podAge: pod?.metadata?.creationTimestamp ? podAge : "N/A",
+            revisionNumber: pod?.metadata?.annotations?.["helm.sh/revision"] || "N/A",
+            crashLoopReason,
+          };
+        });
+
+      setPods(newPods);
+    } catch (error) {
+      // TODO: handle error
+    }
+  };
+
   return (
-    <StyledStatusFooter>
-      {service.type === "job" && (
-        <Container row>
-          <Mi className="material-icons">check</Mi>
-          <Text color="helper">
-            Last run succeeded at 12:39 PM on 4/13/23
-          </Text>
-          {/*
-          <Spacer inline x={1} />
-          <Button
-            onClick={() => { }}
-            height="30px"
-            width="87px"
-            color="#ffffff11"
-            withBorder
-          >
-            <I className="material-icons">open_in_new</I>
-            History
-          </Button>
-          */}
-        </Container>
-      )}
-      {service.type !== "job" && (
-        <Container row>
-          {percentage === "0.00%" ? (
-            <StatusDot />
-          ) : total === 0 ? (
-            <StatusDot color="#ffffff33" />
-          ) : (
-            <StatusCircle percentage={percentage} />
-          )}
-          <Text color="helper">
-            {total > 0 ? (
-              <>
-                Running {available}/{total} instances{" "}
-                {stale == 1 ? `(${stale} old instance)` : ""}
-                {stale > 1 ? `(${stale} old instances)` : ""}
-              </>
-            ) : (
-              "Loading . . ."
-            )}
-          </Text>
-          {/*
-          <Spacer inline x={1} />
-          <Button
-            onClick={() => { }}
-            height="30px"
-            width="70px"
-            color="#ffffff11"
-            withBorder
-          >
-            <I className="material-icons">open_in_new</I>
-            Logs
-          </Button>
-          */}
-        </Container>
-      )}
-    </StyledStatusFooter>
+    <>
+      {replicaSetArray != null && replicaSetArray.length > 0 && replicaSetArray.map((replicaSet, i) => {
+        return (
+          <>
+            <StyledStatusFooterTop key={i} expanded={expanded}>
+              <StyledContainer row spaced>
+                {replicaSet.some(r => r.crashLoopReason != "") ?
+                  <>
+                    <Running>
+                      <StatusDot color="#ff0000" />
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} failing to run Revision ${replicaSet[0].revisionNumber}`}
+                      </Text>
+                    </Running>
+                    <Button
+                      onClick={() => {
+                        expanded ? setHeight(0) : setHeight(122);
+                        setExpanded(!expanded);
+                      }}
+                      height="20px"
+                      color="#ffffff11"
+                      withBorder
+                    >
+                      {expanded ? <I className="material-icons">arrow_drop_up</I>
+                        : <I className="material-icons">arrow_drop_down</I>}
+                      <Text color="helper">
+                        See failure reason
+                      </Text>
+                    </Button>
+                  </> :
+                  // check if there are more recent replicasets and if the previous replicaset has a crashloop reason
+                  i > 0 && !replicaSetArray[i - 1].some(p => p.crashLoopReason != "") ?
+                    <Running>
+                      <StatusDot color="#FFA500" />
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} still running at Revision ${replicaSet[0].revisionNumber}. Spinning down...`}
+                      </Text>
+                    </Running> :
+                    <Running>
+                      {replicaSet.length ? <StatusDot /> : <StatusDot color="#ffffff33" />}
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} running at Revision ${replicaSet[0].revisionNumber}`}
+                      </Text>
+                    </Running>
+                }
+              </StyledContainer>
+            </StyledStatusFooterTop>
+            {replicaSet.some(r => r.crashLoopReason != "") &&
+              <AnimateHeight height={height}>
+                <StyledStatusFooter>
+                  <Message>
+                    {replicaSet.find(r => r.crashLoopReason != "")?.crashLoopReason}
+                  </Message>
+                </StyledStatusFooter>
+              </AnimateHeight>
+            }
+          </>
+        )
+      })}
+    </>
   );
 };
 
+
 export default StatusFooter;
 
 const StatusDot = styled.div<{ color?: string }>`
@@ -263,6 +338,26 @@ const StatusDot = styled.div<{ color?: string }>`
   border-radius: 50%;
   margin-right: 10px;
   background: ${props => props.color || "#38a88a"};
+
+  box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
+	transform: scale(1);
+	animation: pulse 2s infinite;
+  @keyframes pulse {
+    0% {
+      transform: scale(0.95);
+      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
+    }
+  
+    70% {
+      transform: scale(1);
+      box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
+    }
+  
+    100% {
+      transform: scale(0.95);
+      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
+    }
+  }
 `;
 
 const Mi = styled.i`
@@ -277,7 +372,7 @@ const I = styled.i`
   margin-right: 5px;
 `;
 
-const StatusCircle = styled.div<{ 
+const StatusCircle = styled.div<{
   percentage?: any;
   dashed?: boolean;
 }>`
@@ -293,6 +388,11 @@ const StatusCircle = styled.div<{
   border: ${(props) => (props.dashed ? "1px dashed #ffffff55" : "none")};
 `;
 
+const Running = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const StyledStatusFooter = styled.div`
   width: 100%;
   padding: 10px 15px;
@@ -301,4 +401,55 @@ const StyledStatusFooter = styled.div`
   border-bottom-right-radius: 5px;
   border: 1px solid #494b4f;
   border-top: 0;
+  overflow: hidden;
+  display: flex;
+  align-items: stretch;
+  flex-direction: row;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledStatusFooterTop = styled(StyledStatusFooter) <{
+  expanded: boolean;
+}>`
+  height: 40px;
+  border-bottom: ${({ expanded }) => expanded && "0px"};
+  border-bottom-left-radius: ${({ expanded }) => expanded && "0px"};
+  border-bottom-right-radius: ${({ expanded }) => expanded && "0px"};
+`;
+
+const Message = styled.div`
+  padding: 20px;
+  background: #000000;
+  border-radius: 5px;
+  line-height: 1.5em;
+  border: 1px solid #aaaabb33;
+  font-family: monospace;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  > img {
+    width: 13px;
+    margin-right: 20px;
+  }
+  width: 100%;
+  height: 101px;
+  overflow: hidden;
+`;
+
+const StyledContainer = styled.div<{
+  row: boolean;
+  spaced: boolean;
+}>`
+  display: ${props => props.row ? "flex" : "block"};
+  align-items: center;
+  justify-content: ${props => props.spaced ? "space-between" : "flex-start"};
+  width: 100%;
 `;

+ 4 - 3
dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx

@@ -29,6 +29,7 @@ export type ControllerTabPodType = {
   podAge: string;
   revisionNumber?: number;
   containerStatus: any;
+  crashLoopReason?: string;
 };
 
 const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
@@ -145,7 +146,7 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
           setPodError(newPods[0].status?.message);
         handleSelectPod(newPods[0], data);
       }
-    } catch (error) {}
+    } catch (error) { }
   };
 
   /**
@@ -277,8 +278,8 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
       case "replicaset":
         return [
           c.status?.availableReplicas ||
-            c.status?.replicas - c.status?.unavailableReplicas ||
-            0,
+          c.status?.replicas - c.status?.unavailableReplicas ||
+          0,
           c.status?.replicas || 0,
         ];
       case "statefulset":

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

@@ -47,7 +47,7 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
     // props.setBuildConfig({});
     return (
       <>
-        <Text color="helper">Dockerfile path</Text>
+        <Text color="helper">Dockerfile path (absolute path)</Text>
         <Spacer y={0.5} />
         <Input
           placeholder="ex: ./Dockerfile"

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx

@@ -195,7 +195,7 @@ const BuildSettingsTab: React.FC<Props> = ({
         }
         setCurrentError(
           'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' +
-            tmpError.response.data
+          tmpError.response.data
         );
         return;
       }
@@ -364,7 +364,7 @@ const BuildSettingsTab: React.FC<Props> = ({
             {chart.git_action_config.dockerfile_path && (
               <InputRow
                 disabled={true}
-                label="Dockerfile path"
+                label="Dockerfile path (absolute path)"
                 type="text"
                 width="100%"
                 value={chart.git_action_config.dockerfile_path}

+ 13 - 26
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts

@@ -36,8 +36,8 @@ export const getAvailability = (kind: string, c: any) => {
     case "replicaset":
       return [
         c.status?.availableReplicas ||
-          c.status?.replicas - c.status?.unavailableReplicas ||
-          0,
+        c.status?.replicas - c.status?.unavailableReplicas ||
+        0,
         c.status?.replicas || 0,
       ];
     case "statefulset":
@@ -52,28 +52,15 @@ export const getAvailability = (kind: string, c: any) => {
   }
 };
 
-export const getAvailabilityStacks = (kind: string, c: any) => {
-  switch (kind?.toLowerCase()) {
-    case "deployment":
-    case "replicaset":
-      const available =
-        c.status?.updatedReplicas ||
-        c.status?.updatedReplicas ||
-        c.status?.replicas - c.status?.unavailableReplicas ||
-        0;
-      const total = c.spec.replicas;
-      const stale =
-        c.status?.availableReplicas - c.status?.updatedReplicas || 0;
-      return [available, total, stale];
-    case "statefulset":
-      return [c.status?.readyReplicas || 0, c.status?.replicas || 0, 0];
-    case "daemonset":
-      return [
-        c.status?.numberAvailable || 0,
-        c.status?.desiredNumberScheduled || 0,
-        0,
-      ];
-    case "job":
-      return [1, 1, 0];
-  }
+export const getAvailabilityStacks = (c: any) => {
+
+  const available =
+    c.status?.updatedReplicas ||
+    c.status?.replicas - c.status?.unavailableReplicas ||
+    0;
+  const unavailable = c.status?.unavailableReplicas
+  const total = c.status.replicas;
+  const stale = (unavailable != null ? c.status?.updatedReplicas : c.status?.availableReplicas - c.status?.updatedReplicas) || 0;
+  return [available, total, stale, unavailable];
+
 };

+ 20 - 3
ee/api/server/handlers/invite/create.go

@@ -4,10 +4,13 @@
 package invite
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"time"
 
+	"github.com/porter-dev/porter/internal/telemetry"
+
 	"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"
@@ -33,27 +36,41 @@ func NewInviteCreateHandler(
 }
 
 func (c *InviteCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	tracer, _ := telemetry.InitTracer(context.Background(), c.Config().TelemetryConfig)
+	defer tracer.Shutdown()
+
+	// just for demonstration purposes
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-invite")
+	defer span.End()
+
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 	request := &types.CreateInviteRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "message", Value: "failed to decode and validate request"})
 		return
 	}
 
 	// create invite model
 	invite, err := CreateInviteWithProject(request, project.ID)
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error creating invite with project")))
 		return
 	}
 
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: invite.ProjectID},
+		telemetry.AttributeKV{Key: "user-id", Value: invite.UserID},
+		telemetry.AttributeKV{Key: "kind", Value: invite.Kind},
+	)
+
 	// write to database
 	invite, err = c.Repo().Invite().CreateInvite(invite)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error creating invite in repo")))
 		return
 	}
 
@@ -67,7 +84,7 @@ func (c *InviteCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			ProjectOwnerEmail: user.Email,
 		},
 	); err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error sending project invite email")))
 		return
 	}
 

+ 138 - 29
internal/helm/agent.go

@@ -9,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/porter-dev/porter/internal/telemetry"
+
 	"github.com/pkg/errors"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/stefanmcshane/helm/pkg/action"
@@ -34,9 +36,17 @@ type Agent struct {
 
 // ListReleases lists releases based on a ListFilter
 func (a *Agent) ListReleases(
+	ctx context.Context,
 	namespace string,
 	filter *types.ReleaseListFilter,
 ) ([]*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-list-releases")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "namespace", Value: namespace},
+	)
+
 	lsel := fmt.Sprintf("owner=helm,status in (%s)", strings.Join(filter.StatusFilter, ","))
 
 	// list secrets
@@ -47,7 +57,7 @@ func (a *Agent) ListReleases(
 		},
 	)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error getting secret list")
 	}
 
 	// before decoding to helm release, only keep the latest releases for each chart
@@ -95,10 +105,20 @@ func (a *Agent) ListReleases(
 
 // GetRelease returns the info of a release.
 func (a *Agent) GetRelease(
+	ctx context.Context,
 	name string,
 	version int,
 	getDeps bool,
 ) (*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-get-release")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "name", Value: name},
+		telemetry.AttributeKV{Key: "version", Value: version},
+		telemetry.AttributeKV{Key: "getDeps", Value: getDeps},
+	)
+
 	// Namespace is already known by the RESTClientGetter.
 	cmd := action.NewGet(a.ActionConfig)
 
@@ -106,7 +126,7 @@ func (a *Agent) GetRelease(
 
 	release, err := cmd.Run(name)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error running get release")
 	}
 
 	if getDeps && release.Chart != nil && release.Chart.Metadata != nil {
@@ -125,9 +145,9 @@ func (a *Agent) GetRelease(
 				}
 
 				if !depExists {
-					depChart, err := loader.LoadChartPublic(dep.Repository, dep.Name, dep.Version)
+					depChart, err := loader.LoadChartPublic(ctx, dep.Repository, dep.Name, dep.Version)
 					if err != nil {
-						return nil, fmt.Errorf("Error retrieving chart dependency %s/%s-%s: %s", dep.Repository, dep.Name, dep.Version, err.Error())
+						return nil, telemetry.Error(ctx, span, err, fmt.Sprintf("Error retrieving chart dependency %s/%s-%s", dep.Repository, dep.Name, dep.Version))
 					}
 
 					release.Chart.AddDependency(depChart)
@@ -141,9 +161,18 @@ func (a *Agent) GetRelease(
 
 // DeleteReleaseRevision deletes a specific revision of a release
 func (a *Agent) DeleteReleaseRevision(
+	ctx context.Context,
 	name string,
 	version int,
 ) error {
+	ctx, span := telemetry.NewSpan(ctx, "helm-delete-release-history")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "name", Value: name},
+		telemetry.AttributeKV{Key: "version", Value: version},
+	)
+
 	_, err := a.ActionConfig.Releases.Delete(name, version)
 
 	return err
@@ -151,8 +180,16 @@ func (a *Agent) DeleteReleaseRevision(
 
 // GetReleaseHistory returns a list of charts for a specific release
 func (a *Agent) GetReleaseHistory(
+	ctx context.Context,
 	name string,
 ) ([]*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-get-release-history")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "name", Value: name},
+	)
+
 	cmd := action.NewHistory(a.ActionConfig)
 
 	return cmd.Run(name)
@@ -175,33 +212,57 @@ type UpgradeReleaseConfig struct {
 
 // UpgradeRelease upgrades a specific release with new values.yaml
 func (a *Agent) UpgradeRelease(
+	ctx context.Context,
 	conf *UpgradeReleaseConfig,
 	values string,
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
 	ignoreDependencies bool,
 ) (*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-upgrade-release")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: conf.Cluster.ProjectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: conf.Cluster.ID},
+		telemetry.AttributeKV{Key: "name", Value: conf.Name},
+		telemetry.AttributeKV{Key: "stack-name", Value: conf.StackName},
+		telemetry.AttributeKV{Key: "stack-revision", Value: conf.StackRevision},
+	)
+
 	valuesYaml, err := chartutil.ReadValues([]byte(values))
 	if err != nil {
-		return nil, fmt.Errorf("Values could not be parsed: %v", err)
+		return nil, telemetry.Error(ctx, span, err, "Values could not be parsed")
 	}
 
 	conf.Values = valuesYaml
 
-	return a.UpgradeReleaseByValues(conf, doAuth, disablePullSecretsInjection, ignoreDependencies)
+	return a.UpgradeReleaseByValues(ctx, conf, doAuth, disablePullSecretsInjection, ignoreDependencies)
 }
 
 // UpgradeReleaseByValues upgrades a release by unmarshaled yaml values
 func (a *Agent) UpgradeReleaseByValues(
+	ctx context.Context,
 	conf *UpgradeReleaseConfig,
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
 	ignoreDependencies bool,
 ) (*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-upgrade-release-by-values")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: conf.Cluster.ProjectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: conf.Cluster.ID},
+		telemetry.AttributeKV{Key: "name", Value: conf.Name},
+		telemetry.AttributeKV{Key: "stack-name", Value: conf.StackName},
+		telemetry.AttributeKV{Key: "stack-revision", Value: conf.StackRevision},
+	)
+
 	// grab the latest release
-	rel, err := a.GetRelease(conf.Name, 0, !ignoreDependencies)
+	rel, err := a.GetRelease(ctx, conf.Name, 0, !ignoreDependencies)
 	if err != nil {
-		return nil, fmt.Errorf("Could not get release to be upgraded: %v", err)
+		return nil, telemetry.Error(ctx, span, err, "Could not get release to be upgraded")
 	}
 
 	ch := rel.Chart
@@ -224,7 +285,7 @@ func (a *Agent) UpgradeReleaseByValues(
 	)
 
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error getting porter postrenderer")
 	}
 
 	if conf.StackName != "" && conf.StackRevision > 0 {
@@ -247,7 +308,7 @@ func (a *Agent) UpgradeReleaseByValues(
 				},
 			)
 			if err != nil {
-				return nil, fmt.Errorf("Upgrade failed: %w", err)
+				return nil, telemetry.Error(ctx, span, err, "error getting secret list")
 			}
 
 			if len(secretList.Items) > 0 {
@@ -270,20 +331,20 @@ func (a *Agent) UpgradeReleaseByValues(
 					err = helmSecrets.Update(mostRecentSecret.GetName(), rel)
 
 					if err != nil {
-						return nil, fmt.Errorf("Upgrade failed: %w", err)
+						return nil, telemetry.Error(ctx, span, err, "error updating helm secrets")
 					}
 
 					// retry upgrade
 					res, err = cmd.Run(conf.Name, ch, conf.Values)
 
 					if err != nil {
-						return nil, fmt.Errorf("Upgrade failed: %w", err)
+						return nil, telemetry.Error(ctx, span, err, "error running upgrade after updating helm secrets")
 					}
 
 					return res, nil
 				} else {
 					// ask the user to wait for about a minute before retrying for the above fix to kick in
-					return nil, fmt.Errorf("another operation (install/upgrade/rollback) is in progress. If this error persists, please wait for 60 seconds to force an upgrade")
+					return nil, telemetry.Error(ctx, span, err, "another operation (install/upgrade/rollback) is in progress. If this error persists, please wait for 60 seconds to force an upgrade")
 				}
 			}
 		} else if strings.Contains(err.Error(), "current release manifest contains removed kubernetes api(s)") || strings.Contains(err.Error(), "resource mapping not found for name") {
@@ -296,7 +357,7 @@ func (a *Agent) UpgradeReleaseByValues(
 				},
 			)
 			if err != nil {
-				return nil, fmt.Errorf("Upgrade failed: %w", err)
+				return nil, telemetry.Error(ctx, span, err, "error getting secret list")
 			}
 
 			if len(secretList.Items) > 0 {
@@ -324,7 +385,7 @@ func (a *Agent) UpgradeReleaseByValues(
 
 				newRelDryRun, err := installCmd.Run(ch, conf.Values)
 				if err != nil {
-					return nil, err
+					return nil, telemetry.Error(ctx, span, err, "error running install cmd")
 				}
 
 				oldManifestBuffer := bytes.NewBufferString(rel.Manifest)
@@ -334,7 +395,7 @@ func (a *Agent) UpgradeReleaseByValues(
 
 				updatedManifestBuffer, err := versionMapper.Run(oldManifestBuffer, newManifestBuffer)
 				if err != nil {
-					return nil, err
+					return nil, telemetry.Error(ctx, span, err, "error running version mapper")
 				}
 
 				rel.Manifest = updatedManifestBuffer.String()
@@ -344,19 +405,19 @@ func (a *Agent) UpgradeReleaseByValues(
 				err = helmSecrets.Update(mostRecentSecret.GetName(), rel)
 
 				if err != nil {
-					return nil, fmt.Errorf("Upgrade failed: %w", err)
+					return nil, telemetry.Error(ctx, span, err, "error updating helm secret")
 				}
 
 				res, err := cmd.Run(conf.Name, ch, conf.Values)
 				if err != nil {
-					return nil, fmt.Errorf("Upgrade failed: %w", err)
+					return nil, telemetry.Error(ctx, span, err, "error running upgrade after updating helm secrets")
 				}
 
 				return res, nil
 			}
 		}
 
-		return nil, fmt.Errorf("Upgrade failed: %w", err)
+		return nil, telemetry.Error(ctx, span, err, "error running upgrade")
 	}
 
 	return res, nil
@@ -375,23 +436,35 @@ type InstallChartConfig struct {
 
 // InstallChartFromValuesBytes reads the raw values and calls Agent.InstallChart
 func (a *Agent) InstallChartFromValuesBytes(
+	ctx context.Context,
 	conf *InstallChartConfig,
 	values []byte,
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-install-chart-from-values-bytes")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: conf.Cluster.ProjectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: conf.Cluster.ID},
+		telemetry.AttributeKV{Key: "chart-name", Value: conf.Name},
+		telemetry.AttributeKV{Key: "chart-namespace", Value: conf.Namespace},
+	)
+
 	valuesYaml, err := chartutil.ReadValues(values)
 	if err != nil {
-		return nil, fmt.Errorf("Values could not be parsed: %v", err)
+		return nil, telemetry.Error(ctx, span, err, "Values could not be parsed")
 	}
 
 	conf.Values = valuesYaml
 
-	return a.InstallChart(conf, doAuth, disablePullSecretsInjection)
+	return a.InstallChart(ctx, conf, doAuth, disablePullSecretsInjection)
 }
 
 // InstallChart installs a new chart
 func (a *Agent) InstallChart(
+	ctx context.Context,
 	conf *InstallChartConfig,
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
@@ -402,6 +475,16 @@ func (a *Agent) InstallChart(
 		}
 	}()
 
+	ctx, span := telemetry.NewSpan(ctx, "helm-install-chart")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: conf.Cluster.ProjectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: conf.Cluster.ID},
+		telemetry.AttributeKV{Key: "chart-name", Value: conf.Name},
+		telemetry.AttributeKV{Key: "chart-namespace", Value: conf.Namespace},
+	)
+
 	cmd := action.NewInstall(a.ActionConfig)
 
 	if cmd.Version == "" && cmd.Devel {
@@ -413,7 +496,7 @@ func (a *Agent) InstallChart(
 	cmd.Timeout = 300 * time.Second
 
 	if err := checkIfInstallable(conf.Chart); err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error checking if installable")
 	}
 
 	var err error
@@ -429,14 +512,14 @@ func (a *Agent) InstallChart(
 	)
 
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error getting post renderer")
 	}
 
 	if req := conf.Chart.Metadata.Dependencies; req != nil {
 		for _, dep := range req {
-			depChart, err := loader.LoadChartPublic(dep.Repository, dep.Name, dep.Version)
+			depChart, err := loader.LoadChartPublic(ctx, dep.Repository, dep.Name, dep.Version)
 			if err != nil {
-				return nil, fmt.Errorf("error retrieving chart dependency %s/%s-%s: %s", dep.Repository, dep.Name, dep.Version, err.Error())
+				return nil, telemetry.Error(ctx, span, err, fmt.Sprintf("error retrieving chart dependency %s/%s-%s", dep.Repository, dep.Name, dep.Version))
 			}
 
 			conf.Chart.AddDependency(depChart)
@@ -448,6 +531,7 @@ func (a *Agent) InstallChart(
 
 // UpgradeInstallChart installs a new chart if it doesn't exist, otherwise it upgrades it
 func (a *Agent) UpgradeInstallChart(
+	ctx context.Context,
 	conf *InstallChartConfig,
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
@@ -458,6 +542,16 @@ func (a *Agent) UpgradeInstallChart(
 		}
 	}()
 
+	ctx, span := telemetry.NewSpan(ctx, "helm-upgrade-install-chart")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: conf.Cluster.ProjectID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: conf.Cluster.ID},
+		telemetry.AttributeKV{Key: "chart-name", Value: conf.Name},
+		telemetry.AttributeKV{Key: "chart-namespace", Value: conf.Namespace},
+	)
+
 	cmd := action.NewUpgrade(a.ActionConfig)
 	cmd.Install = true
 
@@ -469,7 +563,7 @@ func (a *Agent) UpgradeInstallChart(
 	cmd.Timeout = 300 * time.Second
 
 	if err := checkIfInstallable(conf.Chart); err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error checking if installable")
 	}
 
 	var err error
@@ -485,14 +579,14 @@ func (a *Agent) UpgradeInstallChart(
 	)
 
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error getting post renderer")
 	}
 
 	if req := conf.Chart.Metadata.Dependencies; req != nil {
 		for _, dep := range req {
-			depChart, err := loader.LoadChartPublic(dep.Repository, dep.Name, dep.Version)
+			depChart, err := loader.LoadChartPublic(ctx, dep.Repository, dep.Name, dep.Version)
 			if err != nil {
-				return nil, fmt.Errorf("error retrieving chart dependency %s/%s-%s: %s", dep.Repository, dep.Name, dep.Version, err.Error())
+				return nil, telemetry.Error(ctx, span, err, fmt.Sprintf("error retrieving chart dependency %s/%s-%s", dep.Repository, dep.Name, dep.Version))
 			}
 
 			conf.Chart.AddDependency(depChart)
@@ -504,17 +598,32 @@ func (a *Agent) UpgradeInstallChart(
 
 // UninstallChart uninstalls a chart
 func (a *Agent) UninstallChart(
+	ctx context.Context,
 	name string,
 ) (*release.UninstallReleaseResponse, error) {
+	ctx, span := telemetry.NewSpan(ctx, "helm-uninstall-chart")
+	defer span.End()
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "chart-name", Value: name})
+
 	cmd := action.NewUninstall(a.ActionConfig)
 	return cmd.Run(name)
 }
 
 // RollbackRelease rolls a release back to a specified revision/version
 func (a *Agent) RollbackRelease(
+	ctx context.Context,
 	name string,
 	version int,
 ) error {
+	ctx, span := telemetry.NewSpan(ctx, "helm-rollback-release")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "name", Value: name},
+		telemetry.AttributeKV{Key: "version", Value: version},
+	)
+
 	cmd := action.NewRollback(a.ActionConfig)
 	cmd.Version = version
 	return cmd.Run(name)

+ 5 - 4
internal/helm/agent_test.go

@@ -1,6 +1,7 @@
 package helm_test
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stefanmcshane/helm/pkg/storage/driver"
@@ -223,7 +224,7 @@ func TestGetReleases(t *testing.T) {
 		// namespace, so we have to reset the namespace of the storage driver
 		agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace(tc.namespace)
 
-		rel, err := agent.GetRelease(tc.getName, tc.getVersion, false)
+		rel, err := agent.GetRelease(context.Background(), tc.getName, tc.getVersion, false)
 		if err != nil {
 			t.Errorf("%v", err)
 		}
@@ -256,7 +257,7 @@ func TestListReleaseHistory(t *testing.T) {
 		// namespace, so we have to reset the namespace of the storage driver
 		agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace(tc.namespace)
 
-		releases, err := agent.GetReleaseHistory("wordpress")
+		releases, err := agent.GetReleaseHistory(context.Background(), "wordpress")
 		if err != nil {
 			t.Errorf("%v", err)
 		}
@@ -325,12 +326,12 @@ func TestRollbackRelease(t *testing.T) {
 		// namespace, so we have to reset the namespace of the storage driver
 		agent.ActionConfig.Releases.Driver.(*driver.Memory).SetNamespace(tc.namespace)
 
-		err := agent.RollbackRelease("wordpress", 1)
+		err := agent.RollbackRelease(context.Background(), "wordpress", 1)
 		if err != nil {
 			t.Errorf("%v", err)
 		}
 
-		rel, err := agent.GetRelease(tc.getName, tc.getVersion, false)
+		rel, err := agent.GetRelease(context.Background(), tc.getName, tc.getVersion, false)
 		if err != nil {
 			t.Errorf("%v", err)
 		}

+ 21 - 9
internal/helm/loader/loader.go

@@ -2,12 +2,15 @@ package loader
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/telemetry"
+
 	"k8s.io/helm/pkg/repo"
 	"sigs.k8s.io/yaml"
 
@@ -123,18 +126,27 @@ func LoadRepoIndexPublic(repoURL string) (*repo.IndexFile, error) {
 }
 
 // LoadChart uses an http request to fetch a chart from a remote Helm repo
-func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+func LoadChart(ctx context.Context, client *BasicAuthClient, repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	ctx, span := telemetry.NewSpan(ctx, "load-chart")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "repo-url", Value: repoURL},
+		telemetry.AttributeKV{Key: "chart-name", Value: chartName},
+		telemetry.AttributeKV{Key: "chart-version", Value: chartVersion},
+	)
+
 	repoIndex, err := LoadRepoIndex(client, repoURL)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error loading repo index")
 	}
 
 	cv, err := repoIndex.Get(chartName, chartVersion)
 
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error getting repo index")
 	} else if len(cv.URLs) == 0 {
-		return nil, fmt.Errorf("%s:%s no valid download urls", chartName, chartVersion)
+		return nil, telemetry.Error(ctx, span, nil, fmt.Sprintf("%s:%s no valid download urls", chartName, chartVersion))
 	}
 
 	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
@@ -147,7 +159,7 @@ func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string)
 	// download tgz
 	req, err := http.NewRequest("GET", chartURL, nil)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error creating request")
 	}
 
 	if client.Username != "" {
@@ -156,14 +168,14 @@ func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string)
 
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error executing request")
 	}
 
 	defer resp.Body.Close()
 
 	data, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return nil, err
+		return nil, telemetry.Error(ctx, span, err, "error reading response body")
 	}
 
 	return chartloader.LoadArchive(bytes.NewReader(data))
@@ -174,8 +186,8 @@ func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string)
 //
 // TODO: this is an expensive operation, so after retrieving the digest from the
 // repo index, this should check the digest in the cache
-func LoadChartPublic(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
-	return LoadChart(&BasicAuthClient{}, repoURL, chartName, chartVersion)
+func LoadChartPublic(ctx context.Context, repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	return LoadChart(ctx, &BasicAuthClient{}, repoURL, chartName, chartVersion)
 }
 
 // Helper method to test if chart repo URL is valid, or is a path. Chartmuseum saves URLs

+ 2 - 1
internal/helm/repo/repo.go

@@ -1,6 +1,7 @@
 package repo
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/porter-dev/porter/api/types"
@@ -78,7 +79,7 @@ func (hr *HelmRepo) getChartBasic(
 		Password: string(basic.Password),
 	}
 
-	return loader.LoadChart(client, hr.RepoURL, chartName, chartVersion)
+	return loader.LoadChart(context.Background(), client, hr.RepoURL, chartName, chartVersion)
 }
 
 func ValidateRepoURL(

+ 2 - 1
internal/kubernetes/envgroup/create.go

@@ -1,6 +1,7 @@
 package envgroup
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strconv"
@@ -190,7 +191,7 @@ func GetSyncedReleases(helmAgent *helm.Agent, configMap *v1.ConfigMap) ([]*relea
 	appStrArr := strings.Split(appStr, ",")
 
 	// list all latest helm releases and check them against app string
-	releases, err := helmAgent.ListReleases(configMap.Namespace, &types.ReleaseListFilter{
+	releases, err := helmAgent.ListReleases(context.Background(), configMap.Namespace, &types.ReleaseListFilter{
 		StatusFilter: []string{
 			"deployed",
 			"uninstalled",

+ 2 - 2
internal/opa/opa.go

@@ -172,7 +172,7 @@ func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection
 	var helmReleases []*release.Release
 
 	if collection.Match.Name != "" {
-		helmRelease, err := helmAgent.GetRelease(collection.Match.Name, 0, false)
+		helmRelease, err := helmAgent.GetRelease(context.Background(), collection.Match.Name, 0, false)
 
 		if err != nil {
 			if collection.MustExist && strings.Contains(err.Error(), "not found") {
@@ -204,7 +204,7 @@ func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection
 
 		helmReleases = append(helmReleases, helmRelease)
 	} else if collection.Match.ChartName != "" {
-		prefilterReleases, err := helmAgent.ListReleases(collection.Match.Namespace, &types.ReleaseListFilter{
+		prefilterReleases, err := helmAgent.ListReleases(context.Background(), collection.Match.Namespace, &types.ReleaseListFilter{
 			ByDate: true,
 			StatusFilter: []string{
 				"deployed",

+ 3 - 2
internal/templater/helm/values/writer.go

@@ -1,6 +1,7 @@
 package helm
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/porter-dev/porter/internal/helm"
@@ -42,7 +43,7 @@ func (w *TemplateWriter) Create(
 		Values:    vals,
 	}
 
-	_, err := w.Agent.InstallChart(conf, nil, false)
+	_, err := w.Agent.InstallChart(context.Background(), conf, nil, false)
 	if err != nil {
 		return nil, err
 	}
@@ -63,7 +64,7 @@ func (w *TemplateWriter) Update(
 		Values: vals,
 	}
 
-	_, err := w.Agent.UpgradeReleaseByValues(conf, nil, false, false)
+	_, err := w.Agent.UpgradeReleaseByValues(context.Background(), conf, nil, false, false)
 	if err != nil {
 		return nil, err
 	}