Kaynağa Gözat

Merge branch 'master' of github.com:porter-dev/porter into por-57-lack-of-information-for-error-boundary

jnfrati 4 yıl önce
ebeveyn
işleme
5232ef4d0b
53 değiştirilmiş dosya ile 386 ekleme ve 104 silme
  1. 0 22
      .github/workflows/release.yaml
  2. 23 0
      api/client/k8s.go
  3. 14 7
      api/server/authz/cluster.go
  4. 14 2
      api/server/authz/release.go
  5. 1 1
      api/server/handlers/cluster/create_namespace.go
  6. 1 1
      api/server/handlers/cluster/delete_namespace.go
  7. 1 1
      api/server/handlers/cluster/detect_prometheus_installed.go
  8. 1 1
      api/server/handlers/cluster/get.go
  9. 1 1
      api/server/handlers/cluster/get_node.go
  10. 1 1
      api/server/handlers/cluster/get_pod_metrics.go
  11. 1 1
      api/server/handlers/cluster/get_pods.go
  12. 1 1
      api/server/handlers/cluster/list_namespaces.go
  13. 1 1
      api/server/handlers/cluster/list_nginx_ingresses.go
  14. 1 1
      api/server/handlers/cluster/list_nodes.go
  15. 1 1
      api/server/handlers/cluster/stream_helm_release.go
  16. 1 1
      api/server/handlers/cluster/stream_status.go
  17. 1 1
      api/server/handlers/job/delete.go
  18. 1 1
      api/server/handlers/job/get_pods.go
  19. 1 1
      api/server/handlers/job/stop.go
  20. 1 1
      api/server/handlers/namespace/create_configmap.go
  21. 1 1
      api/server/handlers/namespace/delete_configmap.go
  22. 12 2
      api/server/handlers/namespace/delete_pod.go
  23. 1 1
      api/server/handlers/namespace/get_configmap.go
  24. 12 2
      api/server/handlers/namespace/get_ingress.go
  25. 1 1
      api/server/handlers/namespace/get_pod_events.go
  26. 1 1
      api/server/handlers/namespace/list_configmaps.go
  27. 1 1
      api/server/handlers/namespace/list_releases.go
  28. 1 1
      api/server/handlers/namespace/rename_configmap.go
  29. 12 2
      api/server/handlers/namespace/stream_pod_logs.go
  30. 1 1
      api/server/handlers/namespace/update_configmap.go
  31. 1 1
      api/server/handlers/release/create.go
  32. 1 1
      api/server/handlers/release/create_addon.go
  33. 1 1
      api/server/handlers/release/create_subdomain.go
  34. 1 1
      api/server/handlers/release/delete.go
  35. 1 1
      api/server/handlers/release/get_all_pods.go
  36. 10 2
      api/server/handlers/release/get_controllers.go
  37. 1 1
      api/server/handlers/release/get_history.go
  38. 1 1
      api/server/handlers/release/get_job_status.go
  39. 1 1
      api/server/handlers/release/get_jobs.go
  40. 1 1
      api/server/handlers/release/ugprade.go
  41. 1 1
      api/server/handlers/release/update_image_batch.go
  42. 1 1
      api/server/handlers/release/update_rollback.go
  43. 28 5
      api/server/handlers/release/upgrade_webhook.go
  44. 24 0
      api/server/router/router.go
  45. 7 1
      api/server/shared/writer.go
  46. 4 0
      api/types/jobs.go
  47. 4 0
      api/types/release.go
  48. 2 2
      api/types/user.go
  49. 137 2
      cli/cmd/job.go
  50. 6 2
      internal/helm/agent.go
  51. 40 10
      internal/kubernetes/agent.go
  52. 1 2
      internal/kubernetes/provisioner/resource_stream.go
  53. 2 7
      scripts/build/win.sh

+ 0 - 22
.github/workflows/release.yaml

@@ -314,28 +314,6 @@ jobs:
           asset_path: ./release/windows/porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_content_type: application/zip
-      - name: Upload Windows Server Release Asset
-        id: upload-windows-server-release-asset
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          GITHUB_TAG: ${{ github.ref }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./release/windows/portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
-          asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
-          asset_content_type: application/zip
-      - name: Upload Windows Docker Credential Release Asset
-        id: upload-windows-docker-cred-release-asset
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          GITHUB_TAG: ${{ github.ref }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./release/windows/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
-          asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
-          asset_content_type: application/zip
       - name: Upload Static Release Asset
         id: upload-static-release-asset
         uses: actions/upload-release-asset@v1

+ 23 - 0
api/client/k8s.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/api/types"
+	v1 "k8s.io/api/batch/v1"
 )
 
 // GetK8sNamespaces gets a namespaces list in a k8s cluster
@@ -66,6 +67,28 @@ func (c *Client) GetRelease(
 	return resp, err
 }
 
+func (c *Client) GetJobs(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace, name string,
+) ([]v1.Job, error) {
+	respArr := make([]v1.Job, 0)
+
+	resp := &respArr
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/namespaces/%s/releases/%s/0/jobs",
+			projectID, clusterID,
+			namespace, name,
+		),
+		nil,
+		resp,
+	)
+
+	return *resp, err
+}
+
 // GetK8sAllPods gets all pods for a given release
 func (c *Client) GetK8sAllPods(
 	ctx context.Context,

+ 14 - 7
api/server/authz/cluster.go

@@ -72,8 +72,8 @@ func NewClusterContext(ctx context.Context, cluster *models.Cluster) context.Con
 type KubernetesAgentGetter interface {
 	GetOutOfClusterConfig(cluster *models.Cluster) *kubernetes.OutOfClusterConfig
 	GetDynamicClient(r *http.Request, cluster *models.Cluster) (dynamic.Interface, error)
-	GetAgent(r *http.Request, cluster *models.Cluster) (*kubernetes.Agent, error)
-	GetHelmAgent(r *http.Request, cluster *models.Cluster) (*helm.Agent, error)
+	GetAgent(r *http.Request, cluster *models.Cluster, namespace string) (*kubernetes.Agent, error)
+	GetHelmAgent(r *http.Request, cluster *models.Cluster, namespace string) (*helm.Agent, error)
 }
 
 type OutOfClusterAgentGetter struct {
@@ -92,7 +92,7 @@ func (d *OutOfClusterAgentGetter) GetOutOfClusterConfig(cluster *models.Cluster)
 	}
 }
 
-func (d *OutOfClusterAgentGetter) GetAgent(r *http.Request, cluster *models.Cluster) (*kubernetes.Agent, error) {
+func (d *OutOfClusterAgentGetter) GetAgent(r *http.Request, cluster *models.Cluster, namespace string) (*kubernetes.Agent, error) {
 	// look for the agent in context
 	ctxAgentVal := r.Context().Value(KubernetesAgentCtxKey)
 
@@ -104,7 +104,12 @@ func (d *OutOfClusterAgentGetter) GetAgent(r *http.Request, cluster *models.Clus
 
 	// if agent not found in context, get the agent from out of cluster config
 	ooc := d.GetOutOfClusterConfig(cluster)
-	ooc.DefaultNamespace = getNamespaceFromRequest(r)
+
+	if namespace == "" {
+		ooc.DefaultNamespace = getNamespaceFromRequest(r)
+	} else {
+		ooc.DefaultNamespace = namespace
+	}
 
 	agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
 
@@ -119,7 +124,7 @@ func (d *OutOfClusterAgentGetter) GetAgent(r *http.Request, cluster *models.Clus
 	return agent, nil
 }
 
-func (d *OutOfClusterAgentGetter) GetHelmAgent(r *http.Request, cluster *models.Cluster) (*helm.Agent, error) {
+func (d *OutOfClusterAgentGetter) GetHelmAgent(r *http.Request, cluster *models.Cluster, namespace string) (*helm.Agent, error) {
 	// look for the agent in context
 	ctxAgentVal := r.Context().Value(HelmAgentCtxKey)
 
@@ -130,13 +135,15 @@ func (d *OutOfClusterAgentGetter) GetHelmAgent(r *http.Request, cluster *models.
 	}
 
 	// if helm agent not found in context, construct it from k8s agent
-	k8sAgent, err := d.GetAgent(r, cluster)
+	k8sAgent, err := d.GetAgent(r, cluster, namespace)
 
 	if err != nil {
 		return nil, err
 	}
 
-	namespace := getNamespaceFromRequest(r)
+	if namespace == "" {
+		namespace = getNamespaceFromRequest(r)
+	}
 
 	helmAgent, err := helm.GetAgentFromK8sAgent("secret", namespace, d.config.Logger, k8sAgent)
 

+ 14 - 2
api/server/authz/release.go

@@ -2,7 +2,9 @@ package authz
 
 import (
 	"context"
+	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -35,7 +37,7 @@ type ReleaseScopedMiddleware struct {
 func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	helmAgent, err := p.agentGetter.GetHelmAgent(r, cluster)
+	helmAgent, err := p.agentGetter.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
@@ -52,7 +54,17 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	release, err := helmAgent.GetRelease(name, int(version), false)
 
 	if err != nil {
-		apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
+		// 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"
+		if strings.Contains(err.Error(), "not found") {
+			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("release not found"),
+				http.StatusNotFound,
+			))
+		} else {
+			apierrors.HandleAPIError(p.config, w, r, apierrors.NewErrInternal(err))
+		}
+
 		return
 	}
 

+ 1 - 1
api/server/handlers/cluster/create_namespace.go

@@ -37,7 +37,7 @@ func (c *CreateNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/delete_namespace.go

@@ -36,7 +36,7 @@ func (c *DeleteNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/detect_prometheus_installed.go

@@ -29,7 +29,7 @@ func NewDetectPrometheusInstalledHandler(
 func (c *DetectPrometheusInstalledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/get.go

@@ -36,7 +36,7 @@ func (c *ClusterGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		Cluster: cluster.ToClusterType(),
 	}
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/get_node.go

@@ -33,7 +33,7 @@ func (c *GetNodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	name, _ := requestutils.GetURLParamString(r, types.URLParamNodeName)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/get_pod_metrics.go

@@ -39,7 +39,7 @@ func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/get_pods.go

@@ -38,7 +38,7 @@ func (c *GetPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/list_namespaces.go

@@ -30,7 +30,7 @@ func NewListNamespacesHandler(
 func (c *ListNamespacesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/list_nginx_ingresses.go

@@ -31,7 +31,7 @@ func NewListNGINXIngressesHandler(
 func (c *ListNGINXIngressesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/list_nodes.go

@@ -31,7 +31,7 @@ func NewListNodesHandler(
 func (c *ListNodesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/stream_helm_release.go

@@ -44,7 +44,7 @@ func (c *StreamHelmReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/cluster/stream_status.go

@@ -45,7 +45,7 @@ func (c *StreamStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/job/delete.go

@@ -28,7 +28,7 @@ func NewDeleteHandler(
 
 func (c *DeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamJobName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 

+ 1 - 1
api/server/handlers/job/get_pods.go

@@ -30,7 +30,7 @@ func NewGetPodsHandler(
 
 func (c *GetPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamJobName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 

+ 1 - 1
api/server/handlers/job/stop.go

@@ -28,7 +28,7 @@ func NewStopHandler(
 
 func (c *StopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamJobName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 

+ 1 - 1
api/server/handlers/namespace/create_configmap.go

@@ -42,7 +42,7 @@ func (c *CreateConfigMapHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/namespace/delete_configmap.go

@@ -38,7 +38,7 @@ func (c *DeleteConfigMapHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 12 - 2
api/server/handlers/namespace/delete_pod.go

@@ -1,6 +1,8 @@
 package namespace
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -9,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"
+	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -28,7 +31,7 @@ func NewDeletePodHandler(
 
 func (c *DeletePodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamPodName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 
@@ -39,7 +42,14 @@ func (c *DeletePodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	err = agent.DeletePod(namespace, name)
 
-	if err != nil {
+	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("pod %s/%s was not found", namespace, name),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/namespace/get_configmap.go

@@ -38,7 +38,7 @@ func (c *GetConfigMapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 12 - 2
api/server/handlers/namespace/get_ingress.go

@@ -1,6 +1,8 @@
 package namespace
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -10,6 +12,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"
+	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -30,7 +33,7 @@ func NewGetIngressHandler(
 
 func (c *GetIngressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamIngressName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 
@@ -41,7 +44,14 @@ func (c *GetIngressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	ingress, err := agent.GetIngress(namespace, name)
 
-	if err != nil {
+	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("ingress %s/%s was not found", namespace, name),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/namespace/get_pod_events.go

@@ -30,7 +30,7 @@ func NewGetPodEventsHandler(
 
 func (c *GetPodEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 	name, _ := requestutils.GetURLParamString(r, types.URLParamPodName)
 	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
 

+ 1 - 1
api/server/handlers/namespace/list_configmaps.go

@@ -31,7 +31,7 @@ func (c *ListConfigMapsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -38,7 +38,7 @@ func (c *ListReleasesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/namespace/rename_configmap.go

@@ -43,7 +43,7 @@ func (c *RenameConfigMapHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 12 - 2
api/server/handlers/namespace/stream_pod_logs.go

@@ -1,6 +1,8 @@
 package namespace
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -10,6 +12,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"
+	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -42,7 +45,7 @@ func (c *StreamPodLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -51,7 +54,14 @@ func (c *StreamPodLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	err = agent.GetPodLogs(namespace, name, conn)
 
-	if err != nil {
+	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("pod %s/%s was not found", namespace, name),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 1 - 1
api/server/handlers/namespace/update_configmap.go

@@ -39,7 +39,7 @@ func (c *UpdateConfigMapHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -53,7 +53,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		},
 	))
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/create_addon.go

@@ -46,7 +46,7 @@ func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		},
 	))
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/create_subdomain.go

@@ -34,7 +34,7 @@ func (c *CreateSubdomainHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -34,7 +34,7 @@ func (c *DeleteReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/get_all_pods.go

@@ -35,7 +35,7 @@ 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)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 10 - 2
api/server/handlers/release/get_controllers.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -38,7 +39,7 @@ func (c *GetControllersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -54,7 +55,14 @@ func (c *GetControllersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		controller.Namespace = helmRelease.Namespace
 		rc, _, err := getController(controller, agent)
 
-		if err != nil {
+		if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("%s/%s of kind %s was not found", controller.Namespace, controller.Name, controller.Kind),
+				http.StatusNotFound,
+			))
+
+			return
+		} else if err != nil {
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}

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

@@ -32,7 +32,7 @@ func NewGetReleaseHistoryHandler(
 func (c *GetReleaseHistoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/get_job_status.go

@@ -31,7 +31,7 @@ func NewGetJobsStatusHandler(
 func (c *GetJobsStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/get_jobs.go

@@ -33,7 +33,7 @@ func NewGetJobsHandler(
 func (c *GetJobsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	agent, err := c.GetAgent(r, cluster)
+	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/ugprade.go

@@ -45,7 +45,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/handlers/release/update_image_batch.go

@@ -35,7 +35,7 @@ func NewUpdateImageBatchHandler(
 func (c *UpdateImageBatchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -36,7 +36,7 @@ func (c *RollbackReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 28 - 5
api/server/handlers/release/upgrade_webhook.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/integrations/slack"
+	"gorm.io/gorm"
 )
 
 type WebhookHandler struct {
@@ -40,22 +41,39 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	release, err := c.Repo().Release().ReadReleaseByWebhookToken(token)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("release not found with given webhook"),
-			http.StatusBadRequest,
-		))
+		if err == gorm.ErrRecordNotFound {
+			// throw forbidden error, since we don't want a way to verify if webhooks exist
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("release not found with given webhook"),
+			))
+
+			return
+		}
 
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
 	cluster, err := c.Repo().Cluster().ReadCluster(release.ProjectID, release.ClusterID)
 
 	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			// throw forbidden error, since we don't want a way to verify if the cluster and project
+			// still exist for a cluster that's been deleted
+			c.HandleAPIError(w, r, apierrors.NewErrForbidden(
+				fmt.Errorf("cluster %d in project %d not found for upgrade webhook", release.ClusterID, release.ProjectID),
+			))
+
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	helmAgent, err := c.GetHelmAgent(r, cluster)
+	// in this case, we retrieve the agent by passing in the namespace field directly, since
+	// it cannot be detected from the URL
+	helmAgent, err := c.GetHelmAgent(r, cluster, release.Namespace)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -70,6 +88,11 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	rel, err := helmAgent.GetRelease(release.Name, 0, true)
 
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// repository is set to current repository by default
 	repository := rel.Config["image"].(map[string]interface{})["repository"]
 

+ 24 - 0
api/server/router/router.go

@@ -3,6 +3,7 @@ package router
 import (
 	"bufio"
 	"errors"
+	"fmt"
 	"net"
 	"net/http"
 	"os"
@@ -15,6 +16,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/logger"
@@ -52,8 +54,12 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	)
 
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
+	panicMW := &PanicMiddleware{config}
 
 	r.Route("/api", func(r chi.Router) {
+		// set panic middleware for all API endpoints to catch panics
+		r.Use(panicMW.Middleware)
+
 		// set the content type for all API endpoints and log all request info
 		r.Use(ContentTypeJSON)
 
@@ -285,3 +291,21 @@ func (mw *RequestLoggerMiddleware) Middleware(next http.Handler) http.Handler {
 		event.Send()
 	})
 }
+
+type PanicMiddleware struct {
+	config *config.Config
+}
+
+func (pmw *PanicMiddleware) Middleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer func() {
+			err := recover()
+
+			if err != nil {
+				apierrors.HandleAPIError(pmw.config, w, r, apierrors.NewErrInternal(fmt.Errorf("%v", err)))
+			}
+		}()
+
+		next.ServeHTTP(w, r)
+	})
+}

+ 7 - 1
api/server/shared/writer.go

@@ -2,7 +2,9 @@ package shared
 
 import (
 	"encoding/json"
+	"errors"
 	"net/http"
+	"syscall"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -25,7 +27,11 @@ func NewDefaultResultWriter(conf *config.Config) ResultWriter {
 func (j *DefaultResultWriter) WriteResult(w http.ResponseWriter, r *http.Request, v interface{}) {
 	err := json.NewEncoder(w).Encode(v)
 
-	if err != nil {
+	if errors.Is(err, syscall.EPIPE) {
+		// broken pipe error, ignore. This means the client closed the connection while
+		// the server was sending bytes.
+		return
+	} else if err != nil {
 		apierrors.HandleAPIError(j.config, w, r, apierrors.NewErrInternal(err))
 	}
 }

+ 4 - 0
api/types/jobs.go

@@ -1,5 +1,9 @@
 package types
 
+import v1 "k8s.io/api/batch/v1"
+
 const (
 	URLParamJobName URLParam = "name"
 )
+
+type GetJobsResponse []v1.Job

+ 4 - 0
api/types/release.go

@@ -73,6 +73,10 @@ const URLParamToken URLParam = "token"
 
 type WebhookRequest struct {
 	Commit string `schema:"commit"`
+
+	// NOTICE: deprecated. This field should no longer be used; it is not supported
+	// internally.
+	Repository string `schema:"repository"`
 }
 
 type GetGHATemplateRequest struct {

+ 2 - 2
api/types/user.go

@@ -39,8 +39,8 @@ type InitiateResetUserPasswordRequest struct {
 }
 
 type VerifyTokenFinalizeRequest struct {
-	TokenID uint   `schema:"token_id" form:"required"`
-	Token   string `schema:"token" form:"required"`
+	TokenID uint   `json:"token_id" schema:"token_id" form:"required"`
+	Token   string `json:"token" schema:"token" form:"required"`
 }
 
 type VerifyEmailFinalizeRequest struct {

+ 137 - 2
cli/cmd/job.go

@@ -4,15 +4,22 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
+	"time"
 
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
+	v1 "k8s.io/api/batch/v1"
 )
 
+var jobCmd = &cobra.Command{
+	Use: "job",
+}
+
 var batchImageUpdateCmd = &cobra.Command{
-	Use:   "job update-images",
+	Use:   "update-images",
 	Short: "Updates the image tag of all jobs in a namespace which use a specific image.",
 	Long: fmt.Sprintf(`
 %s 
@@ -43,10 +50,43 @@ use the --namespace flag:
 	},
 }
 
+var waitCmd = &cobra.Command{
+	Use:   "wait",
+	Short: "Waits for a job to complete.",
+	Long: fmt.Sprintf(`
+%s 
+
+Waits for a job with a given name and namespace to complete a run. If the job completes successfully,
+this command exits with exit code 0. Otherwise, this command exits with exit code 1. 
+
+Example commands:
+
+  %s
+
+This command is namespace-scoped and uses the default namespace. To specify a different namespace, 
+use the --namespace flag:
+
+  %s
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job wait\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example --namespace custom-namespace"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, waitForJob)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var imageRepoURI string
 
 func init() {
-	rootCmd.AddCommand(batchImageUpdateCmd)
+	rootCmd.AddCommand(jobCmd)
+	jobCmd.AddCommand(batchImageUpdateCmd)
+	jobCmd.AddCommand(waitCmd)
 
 	batchImageUpdateCmd.PersistentFlags().StringVar(
 		&tag,
@@ -72,6 +112,22 @@ func init() {
 
 	batchImageUpdateCmd.MarkPersistentFlagRequired("image-repo-uri")
 	batchImageUpdateCmd.MarkPersistentFlagRequired("tag")
+
+	waitCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"",
+		"The namespace of the jobs.",
+	)
+
+	waitCmd.PersistentFlags().StringVar(
+		&name,
+		"name",
+		"",
+		"The name of the jobs.",
+	)
+
+	waitCmd.MarkPersistentFlagRequired("name")
 }
 
 func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
@@ -88,3 +144,82 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 		},
 	)
 }
+
+// waits for a job with a given name/namespace
+func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	// get the job release
+	jobRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, name)
+
+	if err != nil {
+		return err
+	}
+
+	// make sure the job chart has a manual job running
+	pausedVal, ok := jobRelease.Release.Config["paused"]
+	pausedErr := fmt.Errorf("this job template is not currently running a manual job")
+
+	if !ok {
+		return pausedErr
+	}
+
+	if pausedValBool, ok := pausedVal.(bool); ok && pausedValBool {
+		return pausedErr
+	}
+
+	// if no job exists with the given revision, wait up to 5 minutes
+	timeWait := time.Now().Add(5 * time.Minute)
+
+	for timeNow := time.Now(); timeNow.Before(timeWait); {
+		// get the jobs for that job chart
+		jobs, err := client.GetJobs(context.Background(), config.Project, config.Cluster, namespace, name)
+
+		if err != nil {
+			return err
+		}
+
+		job := getJobMatchingRevision(uint(jobRelease.Release.Version), jobs)
+
+		if job == nil {
+			time.Sleep(10 * time.Second)
+			continue
+		}
+
+		// once job is running, wait for status to be completed, or failed
+		// if failed, exit with non-zero exit code
+		if job.Status.Failed > 0 {
+			return fmt.Errorf("job failed")
+		}
+
+		if job.Status.Succeeded > 0 {
+			return nil
+		}
+
+		// otherwise, return no error
+		time.Sleep(10 * time.Second)
+		continue
+	}
+
+	return fmt.Errorf("timed out waiting for job")
+}
+
+func getJobMatchingRevision(revision uint, jobs []v1.Job) *v1.Job {
+	for _, job := range jobs {
+		revisionLabel, revisionLabelExists := job.Labels["helm.sh/revision"]
+
+		if !revisionLabelExists {
+			continue
+		}
+
+		jobRevision, err := strconv.ParseUint(revisionLabel, 10, 64)
+
+		if err != nil {
+			continue
+		}
+
+		if uint(jobRevision) == revision {
+			return &job
+		}
+	}
+
+	return nil
+}

+ 6 - 2
internal/helm/agent.go

@@ -48,14 +48,18 @@ func (a *Agent) GetRelease(
 
 	release, err := cmd.Run(name)
 
-	if getDeps {
+	if err != nil {
+		return nil, err
+	}
+
+	if getDeps && release.Chart != nil && release.Chart.Metadata != nil {
 		for _, dep := range release.Chart.Metadata.Dependencies {
 			depExists := false
 
 			for _, currDep := range release.Chart.Dependencies() {
 				// we just case on name for now -- there might be edge cases we're missing
 				// but this will cover 99% of cases
-				if dep.Name == currDep.Name() {
+				if dep != nil && currDep != nil && dep.Name == currDep.Name() {
 					depExists = true
 					break
 				}

+ 40 - 10
internal/kubernetes/agent.go

@@ -350,13 +350,23 @@ func (a *Agent) GetJobPods(namespace, jobName string) ([]v1.Pod, error) {
 
 // GetIngress gets ingress given the name and namespace
 func (a *Agent) GetIngress(namespace string, name string) (*v1beta1.Ingress, error) {
-	return a.Clientset.ExtensionsV1beta1().Ingresses(namespace).Get(
+	resp, err := a.Clientset.ExtensionsV1beta1().Ingresses(namespace).Get(
 		context.TODO(),
 		name,
 		metav1.GetOptions{},
 	)
+
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
 }
 
+var IsNotFoundError = fmt.Errorf("not found")
+
 // GetDeployment gets the deployment given the name and namespace
 func (a *Agent) GetDeployment(c grapher.Object) (*appsv1.Deployment, error) {
 	res, err := a.Clientset.AppsV1().Deployments(c.Namespace).Get(
@@ -365,7 +375,9 @@ func (a *Agent) GetDeployment(c grapher.Object) (*appsv1.Deployment, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -382,7 +394,9 @@ func (a *Agent) GetStatefulSet(c grapher.Object) (*appsv1.StatefulSet, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -399,7 +413,9 @@ func (a *Agent) GetReplicaSet(c grapher.Object) (*appsv1.ReplicaSet, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -416,7 +432,9 @@ func (a *Agent) GetDaemonSet(c grapher.Object) (*appsv1.DaemonSet, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -433,7 +451,9 @@ func (a *Agent) GetJob(c grapher.Object) (*batchv1.Job, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -450,7 +470,9 @@ func (a *Agent) GetCronJob(c grapher.Object) (*batchv1beta1.CronJob, error) {
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
+	if err != nil && errors.IsNotFound(err) {
+		return nil, IsNotFoundError
+	} else if err != nil {
 		return nil, err
 	}
 
@@ -472,11 +494,17 @@ func (a *Agent) GetPodsByLabel(selector string, namespace string) (*v1.PodList,
 
 // DeletePod deletes a pod by name and namespace
 func (a *Agent) DeletePod(namespace string, name string) error {
-	return a.Clientset.CoreV1().Pods(namespace).Delete(
+	err := a.Clientset.CoreV1().Pods(namespace).Delete(
 		context.TODO(),
 		name,
 		metav1.DeleteOptions{},
 	)
+
+	if err != nil && errors.IsNotFound(err) {
+		return IsNotFoundError
+	}
+
+	return err
 }
 
 // GetPodLogs streams real-time logs from a given pod.
@@ -488,8 +516,10 @@ func (a *Agent) GetPodLogs(namespace string, name string, conn *websocket.Conn)
 		metav1.GetOptions{},
 	)
 
-	if err != nil {
-		return fmt.Errorf("Cannot get pod %s: %s", name, err.Error())
+	if err != nil && errors.IsNotFound(err) {
+		return IsNotFoundError
+	} else if err != nil {
+		return fmt.Errorf("Cannot get logs from pod %s: %s", name, err.Error())
 	}
 
 	container := pod.Spec.Containers[0].Name

+ 1 - 2
internal/kubernetes/provisioner/resource_stream.go

@@ -18,7 +18,7 @@ func ResourceStream(client *redis.Client, streamName string, conn *websocket.Con
 
 			if err != nil {
 				defer conn.Close()
-				errorchan <- err
+				errorchan <- nil
 				return
 			}
 		}
@@ -28,7 +28,6 @@ func ResourceStream(client *redis.Client, streamName string, conn *websocket.Con
 		lastID := "0-0"
 
 		for {
-
 			xstream, err := client.XRead(
 				context.Background(),
 				&redis.XReadArgs{

+ 2 - 7
scripts/build/win.sh

@@ -2,12 +2,7 @@
 #
 # Accepts the version as an argument
 
-go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=$1'" -a -tags cli -o ./porter.exe ./cli &
-go build -ldflags="-w -s -X 'main.Version=$1'" -a -o ./docker-credential-porter.exe ./cmd/docker-credential-porter/ &
-go build -ldflags="-w -s -X 'main.Version=$1'" -a -o ./portersvr.exe ./cmd/app/ &
-wait
+go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=$1'" -a -tags cli -o ./porter.exe ./cli
 
 mkdir -p /release/windows
-zip --junk-paths /release/windows/porter_$1_Windows_x86_64.zip ./porter.exe
-zip --junk-paths /release/windows/portersvr_$1_Windows_x86_64.zip ./portersvr.exe
-zip --junk-paths /release/windows/docker-credential-porter_$1_Windows_x86_64.zip ./docker-credential-porter.exe
+zip --junk-paths /release/windows/porter_$1_Windows_x86_64.zip ./porter.exe