Explorar o código

Merge branch 'master' of github.com:porter-dev/porter into stacks-auto-refresh-built-image-workflow-file

Feroze Mohideen %!s(int64=2) %!d(string=hai) anos
pai
achega
8292039ae0
Modificáronse 100 ficheiros con 5356 adicións e 3347 borrados
  1. 4 6
      .github/golangci-lint.yaml
  2. 0 38
      .github/workflows/old_build-dev-cli.yaml
  3. 0 183
      .github/workflows/old_dev.yaml
  4. 0 97
      .github/workflows/old_staging.yaml
  5. 23 3
      .github/workflows/pr_push_checks.yaml
  6. 0 80
      .github/workflows/preview_env.yml
  7. 1 1
      README.md
  8. 5 1
      Taskfile.yaml
  9. 0 1
      Tiltfile
  10. 54 17
      api/client/api.go
  11. 145 0
      api/client/porter_app.go
  12. 0 56
      api/server/handlers/api_contract/update.go
  13. 15 6
      api/server/handlers/cluster/get_pod_metrics.go
  14. 91 0
      api/server/handlers/gitinstallation/get_branch_head.go
  15. 20 1
      api/server/handlers/gitinstallation/get_porter_yaml.go
  16. 166 0
      api/server/handlers/porter_app/apply.go
  17. 2 3
      api/server/handlers/porter_app/create.go
  18. 7 0
      api/server/handlers/porter_app/create_and_update_events.go
  19. 238 0
      api/server/handlers/porter_app/create_app.go
  20. 159 0
      api/server/handlers/porter_app/current_app_revision.go
  21. 82 0
      api/server/handlers/porter_app/default_deployment_target.go
  22. 36 3
      api/server/handlers/porter_app/get.go
  23. 9 7
      api/server/handlers/porter_app/get_logs_within_time_range.go
  24. 18 0
      api/server/handlers/porter_app/parse.go
  25. 111 0
      api/server/handlers/porter_app/parse_yaml.go
  26. 1 2
      api/server/handlers/porter_app/rollback.go
  27. 110 0
      api/server/handlers/porter_app/run_command.go
  28. 154 0
      api/server/handlers/porter_app/validate.go
  29. 1 0
      api/server/handlers/project/create.go
  30. 1 0
      api/server/handlers/project/create_test.go
  31. 46 2
      api/server/handlers/project_integration/create_gcp.go
  32. 2 1
      api/server/handlers/registry/create_repository.go
  33. 55 14
      api/server/handlers/registry/get_token.go
  34. 18 5
      api/server/handlers/release/update_canonical_name.go
  35. 37 0
      api/server/router/git_installation.go
  36. 203 0
      api/server/router/porter_app.go
  37. 7 7
      api/server/shared/features/features.go
  38. 24 0
      api/types/porter_app.go
  39. 6 0
      api/types/project.go
  40. 4 0
      api/types/project_integration.go
  41. 2 2
      api/types/registry.go
  42. 3 0
      api/types/template.go
  43. 0 267
      cli/cmd/auth.go
  44. 52 0
      cli/cmd/commands/all.go
  45. 142 136
      cli/cmd/commands/app.go
  46. 186 140
      cli/cmd/commands/apply.go
  47. 293 0
      cli/cmd/commands/auth.go
  48. 54 57
      cli/cmd/commands/cluster.go
  49. 317 0
      cli/cmd/commands/config.go
  50. 252 0
      cli/cmd/commands/connect.go
  51. 60 48
      cli/cmd/commands/create.go
  52. 303 0
      cli/cmd/commands/delete.go
  53. 53 36
      cli/cmd/commands/deploy_bluegreen.go
  54. 36 0
      cli/cmd/commands/docker.go
  55. 14 6
      cli/cmd/commands/errors.go
  56. 60 33
      cli/cmd/commands/get.go
  57. 59 0
      cli/cmd/commands/helm.go
  58. 106 62
      cli/cmd/commands/job.go
  59. 18 18
      cli/cmd/commands/kubectl.go
  60. 235 0
      cli/cmd/commands/list.go
  61. 22 22
      cli/cmd/commands/logs.go
  62. 43 0
      cli/cmd/commands/open.go
  63. 21 0
      cli/cmd/commands/portforward.go
  64. 151 0
      cli/cmd/commands/project.go
  65. 70 71
      cli/cmd/commands/registry.go
  66. 12 23
      cli/cmd/commands/root.go
  67. 101 99
      cli/cmd/commands/run.go
  68. 75 65
      cli/cmd/commands/server.go
  69. 74 47
      cli/cmd/commands/stack.go
  70. 248 207
      cli/cmd/commands/update.go
  71. 20 0
      cli/cmd/commands/version.go
  72. 0 303
      cli/cmd/config.go
  73. 128 98
      cli/cmd/config/config.go
  74. 18 19
      cli/cmd/config/docker.go
  75. 0 242
      cli/cmd/connect.go
  76. 4 3
      cli/cmd/connect/dockerhub.go
  77. 4 3
      cli/cmd/connect/docr.go
  78. 12 10
      cli/cmd/connect/ecr.go
  79. 4 3
      cli/cmd/connect/gar.go
  80. 4 3
      cli/cmd/connect/gcr.go
  81. 4 3
      cli/cmd/connect/helmrepo.go
  82. 8 5
      cli/cmd/connect/kubeconfig.go
  83. 4 3
      cli/cmd/connect/registry.go
  84. 0 262
      cli/cmd/delete.go
  85. 7 3
      cli/cmd/deploy/build.go
  86. 37 32
      cli/cmd/deploy/create.go
  87. 29 25
      cli/cmd/deploy/deploy.go
  88. 3 2
      cli/cmd/deploy/shared.go
  89. 4 3
      cli/cmd/deploy/wait/job.go
  90. 0 36
      cli/cmd/docker.go
  91. 41 41
      cli/cmd/docker/agent.go
  92. 33 26
      cli/cmd/docker/auth.go
  93. 2 2
      cli/cmd/docker/builder.go
  94. 4 5
      cli/cmd/docker/config.go
  95. 47 46
      cli/cmd/docker/porter.go
  96. 14 6
      cli/cmd/errors/error_handler.go
  97. 8 8
      cli/cmd/github/release.go
  98. 0 57
      cli/cmd/helm.go
  99. 0 194
      cli/cmd/list.go
  100. 0 31
      cli/cmd/open.go

+ 4 - 6
.github/golangci-lint.yaml

@@ -5,23 +5,21 @@ run:
   build-tags:
   build-tags:
     - codeanalysis
     - codeanalysis
 
 
-# enable exported entity commenting lint rule
 issues:
 issues:
-  exclude:
-    - EXC0012
+  new-from-rev: origin/master # default: HEAD, this will only show linting changes in the current change
   exclude-use-default: false
   exclude-use-default: false
 linters-settings:
 linters-settings:
   revive:
   revive:
     rules:
     rules:
       - name: exported
       - name: exported
         severity: error
         severity: error
-  # gocyclo:
-  #   min-complexity: 15
+  gocyclo:
+    min-complexity: 40 # should drop to 15 max
   gomoddirectives:
   gomoddirectives:
     replace-local: false
     replace-local: false
   gosec:
   gosec:
     excludes:
     excludes:
-    - G307
+    - G307 # exclude duplicated errcheck checks
 
 
 linters:
 linters:
   # disable all default-enabled linters so nothing is mysterious
   # disable all default-enabled linters so nothing is mysterious

+ 0 - 38
.github/workflows/old_build-dev-cli.yaml

@@ -1,38 +0,0 @@
-name: Build Dev CLI
-on:
-  push:
-    branches:
-      - dev
-jobs:
-  build-push-docker-cli:
-    name: Build a new porter-cli docker image
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
-          aws-region: us-east-2
-      - name: Login to ECR public
-        id: login-ecr
-        run: |
-          aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/o1j4x7p4
-      - name: Login to GHCR
-        id: login-ghcr
-        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . \
-            -t public.ecr.aws/o1j4x7p4/porter-cli:dev \
-            -f ./services/porter_cli_container/dev.Dockerfile \
-            --build-arg SENTRY_DSN=${{ secrets.SENTRY_DSN }}
-      - name: Push to ECR public
-        run: |
-          docker push public.ecr.aws/o1j4x7p4/porter-cli:dev
-      - name: Push to GHCR
-        run: |
-          docker tag public.ecr.aws/o1j4x7p4/porter-cli:dev ghcr.io/porter-dev/porter/porter-cli:dev
-          docker push ghcr.io/porter-dev/porter/porter-cli:dev

+ 0 - 183
.github/workflows/old_dev.yaml

@@ -1,183 +0,0 @@
-name: Deploy to dev
-on:
-  push:
-    branches:
-      - dev
-jobs:
-  deploy:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Cloud SDK
-        uses: google-github-actions/setup-gcloud@v0
-        with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.GCP_SA_KEY }}
-          export_default_credentials: true
-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ secrets.AWS_REGION }}
-      - name: Install kubectl
-        uses: azure/setup-kubectl@v2.0
-        with:
-          version: "v1.19.15"
-      - name: Log in to gcloud CLI
-        run: gcloud auth configure-docker
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Write Dashboard Environment Variables
-        run: |
-          cat >./dashboard/.env <<EOL
-          NODE_ENV=development
-          API_SERVER=dashboard.dev.getporter.dev
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-          APPLICATION_CHART_REPO_URL=https://charts.dev.getporter.dev
-          ADDON_CHART_REPO_URL=https://chart-addons.dev.getporter.dev
-          ENABLE_SENTRY=true
-          SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=frontend-development
-          EOL
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter:dev -f ./ee/docker/ee.Dockerfile
-      - name: Push
-        run: |
-          docker push gcr.io/porter-dev-273614/porter:dev
-      - name: Deploy to cluster
-        run: |
-          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name dev
-
-          kubectl rollout restart deployment/porter
-  deploy-provisioner:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Cloud SDK
-        uses: google-github-actions/setup-gcloud@v0
-        with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.GCP_SA_KEY }}
-          export_default_credentials: true
-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ secrets.AWS_REGION }}
-      - name: Install kubectl
-        uses: azure/setup-kubectl@v2.0
-        with:
-          version: "v1.19.15"
-      - name: Log in to gcloud CLI
-        run: gcloud auth configure-docker
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/provisioner-service:dev -f ./ee/docker/provisioner.Dockerfile
-      - name: Push
-        run: |
-          docker push gcr.io/porter-dev-273614/provisioner-service:dev
-      - name: Deploy to cluster
-        run: |
-          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name dev
-
-          kubectl rollout restart deployment/provisioner
-  build-push-ecr-server:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v3
-      - name: Set Github tag
-        id: vars
-        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-      - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.ECR_DEV_AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.ECR_DEV_AWS_ACCESS_SECRET_KEY }}
-          aws-region: us-east-2
-      - name: Login to ECR
-        id: login-ecr
-        run: |
-          aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 801172602658.dkr.ecr.us-east-2.amazonaws.com
-      - name: Write Dashboard Environment Variables
-        run: |
-          cat >./dashboard/.env <<EOL
-          NODE_ENV=development
-          API_SERVER=dashboard.dev.getporter.dev
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-          APPLICATION_CHART_REPO_URL=https://charts.dev.getporter.dev
-          ADDON_CHART_REPO_URL=https://chart-addons.dev.getporter.dev
-          ENABLE_SENTRY=true
-          SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=frontend-development
-          EOL
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t 801172602658.dkr.ecr.us-east-2.amazonaws.com/porter:${{ steps.vars.outputs.sha_short }} -f ./ee/docker/ee.Dockerfile
-      - name: Push to ECR
-        run: |
-          docker push 801172602658.dkr.ecr.us-east-2.amazonaws.com/porter:${{ steps.vars.outputs.sha_short }}
-  build-push-ecr-provisioner:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v3
-      - name: Set Github tag
-        id: vars
-        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-      - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.ECR_DEV_AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.ECR_DEV_AWS_ACCESS_SECRET_KEY }}
-          aws-region: us-east-2
-      - name: Login to ECR
-        id: login-ecr
-        run: |
-          aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 801172602658.dkr.ecr.us-east-2.amazonaws.com
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t 801172602658.dkr.ecr.us-east-2.amazonaws.com/provisioner-service:${{ steps.vars.outputs.sha_short }} -f ./ee/docker/provisioner.Dockerfile
-      - name: Push to ECR
-        run: |
-          docker push 801172602658.dkr.ecr.us-east-2.amazonaws.com/provisioner-service:${{ steps.vars.outputs.sha_short }}
-  build-push-worker-pool:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v3
-      - name: Set Github tag
-        id: vars
-        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-      - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.ECR_DEV_AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.ECR_DEV_AWS_ACCESS_SECRET_KEY }}
-          aws-region: us-east-2
-      - name: Set up Cloud SDK
-        uses: google-github-actions/setup-gcloud@v0
-        with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.GCP_SA_KEY }}
-          export_default_credentials: true
-      - name: Log in to gcloud CLI
-        run: gcloud auth configure-docker
-      - name: Login to ECR
-        id: login-ecr
-        run: |
-          aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 801172602658.dkr.ecr.us-east-2.amazonaws.com
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t 801172602658.dkr.ecr.us-east-2.amazonaws.com/worker-pool:${{ steps.vars.outputs.sha_short }} -t gcr.io/porter-dev-273614/worker-pool:dev -f ./workers/Dockerfile
-      - name: Push to ECR
-        run: |
-          docker push 801172602658.dkr.ecr.us-east-2.amazonaws.com/worker-pool:${{ steps.vars.outputs.sha_short }}
-          docker push gcr.io/porter-dev-273614/worker-pool:dev

+ 0 - 97
.github/workflows/old_staging.yaml

@@ -1,97 +0,0 @@
-name: Deploy to staging
-on:
-  push:
-    branches:
-      - staging
-jobs:
-  login-build-push:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Cloud SDK
-        uses: google-github-actions/setup-gcloud@v0
-        with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.GCP_SA_KEY }}
-          export_default_credentials: true
-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ secrets.AWS_REGION }}
-      - name: Install kubectl
-        uses: azure/setup-kubectl@v2.0
-        with:
-          version: "v1.19.15"
-      - name: Log in to gcloud CLI
-        run: gcloud auth configure-docker
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Write Dashboard Environment Variables
-        run: |
-          cat >./dashboard/.env <<EOL
-          NODE_ENV=production
-          API_SERVER=dashboard.staging.getporter.dev
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-          IS_HOSTED=true
-          ENABLE_COHERE=true
-          COHERE_API_KEY=${{secrets.COHERE_KEY}}
-          INTERCOM_APP_ID=${{secrets.INTERCOM_APP_ID}}
-          INTERCOM_SRC=${{secrets.INTERCOM_SRC}}
-          SEGMENT_WRITE_KEY=${{secrets.SEGMENT_WRITE_KEY}}
-          SEGMENT_PUBLIC_KEY=${{secrets.SEGMENT_PUBLIC_KEY}}
-          APPLICATION_CHART_REPO_URL=https://charts.staging.getporter.dev
-          ADDON_CHART_REPO_URL=https://chart-addons.staging.getporter.dev
-          ENABLE_SENTRY=true
-          SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=frontend-staging
-          ZAPIER_WEBHOOK_URL=${{secrets.ZAPIER_WEBHOOK_URL}}
-          DISCORD_WEBHOOK_URL=${{secrets.DISCORD_WEBHOOK_URL}}
-          EOL
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter:staging -f ./ee/docker/ee.Dockerfile
-      - name: Push
-        run: |
-          docker push gcr.io/porter-dev-273614/porter:staging
-      - name: Deploy to cluster
-        run: |
-          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name staging
-            
-          kubectl rollout restart deployment/porter
-  deploy-provisioner:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Set up Cloud SDK
-        uses: google-github-actions/setup-gcloud@v0
-        with:
-          project_id: ${{ secrets.GCP_PROJECT_ID }}
-          service_account_key: ${{ secrets.GCP_SA_KEY }}
-          export_default_credentials: true
-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1-node16
-        with:
-          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          aws-region: ${{ secrets.AWS_REGION }}
-      - name: Install kubectl
-        uses: azure/setup-kubectl@v2.0
-        with:
-          version: "v1.19.15"
-      - name: Log in to gcloud CLI
-        run: gcloud auth configure-docker
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Build
-        run: |
-          DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/provisioner-service:staging -f ./ee/docker/provisioner.Dockerfile
-      - name: Push
-        run: |
-          docker push gcr.io/porter-dev-273614/provisioner-service:staging
-      - name: Deploy to cluster
-        run: |
-          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name staging
-            
-          kubectl rollout restart deployment/provisioner

+ 23 - 3
.github/workflows/pr_push_checks.yaml

@@ -26,11 +26,31 @@ jobs:
         with:
         with:
           go-version-file: go.mod
           go-version-file: go.mod
           cache: false
           cache: false
-          go-version: '1.20.5'
-      - name: Run Go vet
-        run: go vet ./${{ matrix.folder }}/...
       - name: Run Go tests
       - name: Run Go tests
         run: go test ./${{ matrix.folder }}/...
         run: go test ./${{ matrix.folder }}/...
+  linting:
+    name: Go Linter
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/setup-go@v4
+        with:
+          cache: false
+      - uses: actions/checkout@v3
+      - name: Setup Go Cache
+        uses: actions/cache@v3
+        with:
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: porter-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: porter-go-`
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: latest
+          args: -c .github/golangci-lint.yaml --verbose
+          skip-pkg-cache: true
+          only-new-issues: true # this is needed until the following is merged: https://github.com/golangci/golangci-lint-action/issues/820
   build-npm:
   build-npm:
     name: Running smoke test npm build
     name: Running smoke test npm build
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 0 - 80
.github/workflows/preview_env.yml

@@ -1,80 +0,0 @@
-"on":
-  workflow_dispatch:
-    inputs:
-      pr_branch_from:
-        description: Pull request head branch
-        required: true
-        type: string
-      pr_branch_into:
-        description: Pull request base branch
-        required: true
-        type: string
-      pr_number:
-        description: Pull request number
-        required: true
-        type: string
-      pr_title:
-        description: Pull request title
-        required: true
-        type: string
-name: Porter Preview Environment
-jobs:
-  porter-preview:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout monorepo code
-        id: checkout-monorepo-code
-        uses: actions/checkout@v3
-      - name: Checkout CCP code
-        id: checkout-ccp-code
-        uses: actions/checkout@v3
-        with:
-          repository: porter-dev/cluster-control-plane
-          token: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}
-          path: external/ccp
-      - name: Create Porter preview env
-        id: preview
-        timeout-minutes: 30
-        uses: porter-dev/porter-preview-action@dev
-        with:
-          action_id: ${{ github.run_id }}
-          cluster: "2489"
-          host: https://dashboard.getporter.dev
-          installation_id: "18533943"
-          namespace: pr-${{ github.event.inputs.pr_number }}-porter
-          pr_branch_from: ${{ github.event.inputs.pr_branch_from }}
-          pr_branch_into: ${{ github.event.inputs.pr_branch_into }}
-          pr_id: ${{ github.event.inputs.pr_number }}
-          pr_name: ${{ github.event.inputs.pr_title }}
-          project: "6680"
-          repo_name: porter
-          repo_owner: porter-dev
-          token: ${{ secrets.PORTER_PREVIEW_6680_2489 }}
-        env:
-          PORTER_APPLY_HONEYCOMB_PASSWORD: ${{ secrets.HONEYCOMB_PASSWORD_PREVIEW_ENVIRONMENTS }}
-      - name: Attach vcluster
-        run: |
-          sudo apt-get update
-          sudo apt-get install bash curl jq unzip
-
-          /bin/bash -c "$(curl -fsSL https://install.porter.run)"
-
-          echo "$VCLUSTER_KUBECONFIG" > /tmp/vcluster_kubeconfig
-
-          dashboard_domain=$(echo "$DOMAINS" | jq '.subdomains[] | select(test("porter-dashboard*"))')
-          dashboard_domain=$(sed -e 's/^"//' -e 's/"$//' <<<"$dashboard_domain")
-
-          if [ -z "$dashboard_domain" ]; then
-            exit
-          fi
-
-          export PORTER_HOST="https://${dashboard_domain}"
-
-          porter connect kubeconfig --kubeconfig /tmp/vcluster_kubeconfig
-        env:
-          PORTER_TOKEN: ${{ secrets.PREVIEW_DEPLOYMENT_PORTER_KEY }}
-          PORTER_PROJECT: 1
-          VCLUSTER_KUBECONFIG: ${{ secrets.VCLUSTER_KUBECONFIG }}
-          DOMAINS: ${{ steps.preview.outputs.domains  }}
-    concurrency:
-      group: ${{ github.workflow }}-${{ github.event.inputs.pr_number }}

+ 1 - 1
README.md

@@ -1,7 +1,7 @@
 # Porter
 # Porter
 
 
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/mmGAw5nNjr)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/mmGAw5nNjr)
-[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/getporterdev)
+[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/porterdotrun)
 
 
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to your own AWS/GCP account, while upgrading your infrastructure to Kubernetes. Get started on Porter without the overhead of DevOps and customize your infrastructure later when you need to.
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to your own AWS/GCP account, while upgrading your infrastructure to Kubernetes. Get started on Porter without the overhead of DevOps and customize your infrastructure later when you need to.
 
 

+ 5 - 1
Taskfile.yaml

@@ -17,4 +17,8 @@ tasks:
       ignore_error: false
       ignore_error: false
       silent: true
       silent: true
  
  
-
+  lint:
+    desc: Run all available linters. This mimics any checks performed in Pull Request pre-merge checks
+    cmd: golangci-lint run -c .github/golangci-lint.yaml
+    env:
+      GOWORK: off

+ 0 - 1
Tiltfile

@@ -60,7 +60,6 @@ local_resource(
   deps=[
   deps=[
     "api",
     "api",
     "build",
     "build",
-    "cli",
     "ee",
     "ee",
     "internal",
     "internal",
     "pkg",
     "pkg",

+ 54 - 17
api/client/api.go

@@ -1,8 +1,10 @@
 package client
 package client
 
 
 import (
 import (
+	"context"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
@@ -25,9 +27,61 @@ type Client struct {
 	CookieFilePath string
 	CookieFilePath string
 	Token          string
 	Token          string
 
 
+	// cfToken is a cloudflare token for accessing the API
 	cfToken string
 	cfToken string
 }
 }
 
 
+// NewClientInput contains all information required to create a new API Client
+type NewClientInput struct {
+	// BaseURL is the url for the API. This usually ends with /api, and should not end with a /
+	BaseURL string
+
+	// CookieFileName allows you to authenticate with a cookie file, if one is present in the porter directory.
+	// If both CookieFileName and BearerToken are specified, BearerToken will be preferred
+	CookieFileName string
+
+	// BearerToken uses a JWT to authenticate with the Porter API. If both BearerToken and CookieFileName are specified, BearerToken will be used
+	BearerToken string
+
+	// CloudflareToken allows for authenticating with a Porter API behind Cloudflare Zero Trust. If not specified, we will check PORTER_CF_ACCESS_TOKEN for a token.
+	// If one is found, it will be added to all API calls.
+	CloudflareToken string
+}
+
+// NewClientWithConfig creates a new API client with the provided config
+func NewClientWithConfig(ctx context.Context, input NewClientInput) (Client, error) {
+	client := Client{
+		BaseURL: input.BaseURL,
+		HTTPClient: &http.Client{
+			Timeout: time.Minute,
+		},
+	}
+	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {
+		client.cfToken = cfToken
+	}
+
+	if input.BearerToken != "" {
+		client.Token = input.BearerToken
+		return client, nil
+	}
+
+	if input.CookieFileName != "" {
+		client.CookieFilePath = input.CookieFileName
+		cookie, err := client.getCookie()
+		if err != nil {
+			return client, fmt.Errorf("error getting cooking from path: %w", err)
+		}
+		if cookie == nil {
+			return client, errors.New("no cookie found at location")
+		}
+		return client, nil
+	}
+	return client, ErrNoAuthCredential
+}
+
+// ErrNoAuthCredential returns an error when no auth credentials have been provided such as cookies or tokens
+var ErrNoAuthCredential = errors.New("unable to create an API session with cookie nor token")
+
 // NewClient constructs a new client based on a set of options
 // NewClient constructs a new client based on a set of options
 func NewClient(baseURL string, cookieFileName string) *Client {
 func NewClient(baseURL string, cookieFileName string) *Client {
 	home := homedir.HomeDir()
 	home := homedir.HomeDir()
@@ -55,23 +109,6 @@ func NewClient(baseURL string, cookieFileName string) *Client {
 	return client
 	return client
 }
 }
 
 
-func NewClientWithToken(baseURL, token string) *Client {
-	client := &Client{
-		BaseURL: baseURL,
-		Token:   token,
-		HTTPClient: &http.Client{
-			Timeout: time.Minute,
-		},
-	}
-
-	// look for a cloudflare access token specifically for Porter
-	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {
-		client.cfToken = cfToken
-	}
-
-	return client
-}
-
 func (c *Client) getRequest(relPath string, data interface{}, response interface{}) error {
 func (c *Client) getRequest(relPath string, data interface{}, response interface{}) error {
 	vals := make(map[string][]string)
 	vals := make(map[string][]string)
 	err := schema.NewEncoder().Encode(data, vals)
 	err := schema.NewEncoder().Encode(data, vals)

+ 145 - 0
api/client/porter_app.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 
 
+	"github.com/porter-dev/porter/api/server/handlers/porter_app"
+
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 )
 )
 
 
@@ -127,3 +129,146 @@ func (c *Client) CreateOrUpdatePorterAppEvent(
 
 
 	return *resp, err
 	return *resp, err
 }
 }
+
+// ListEnvGroups (List all Env Groups for a given cluster)
+func (c *Client) ListEnvGroups(
+	ctx context.Context,
+	projectID, clusterID uint,
+) (types.ListEnvironmentGroupsResponse, error) {
+	resp := &types.ListEnvironmentGroupsResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/environment-groups",
+			projectID, clusterID,
+		),
+		nil,
+		resp,
+	)
+
+	return *resp, err
+}
+
+// ParseYAML takes in a base64 encoded porter yaml and returns an app proto
+func (c *Client) ParseYAML(
+	ctx context.Context,
+	projectID, clusterID uint,
+	b64Yaml string,
+) (*porter_app.ParsePorterYAMLToProtoResponse, error) {
+	resp := &porter_app.ParsePorterYAMLToProtoResponse{}
+
+	req := &porter_app.ParsePorterYAMLToProtoRequest{
+		B64Yaml: b64Yaml,
+	}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/parse",
+			projectID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// ValidatePorterApp takes in a base64 encoded app definition that is potentially partial and returns a complete definition
+// using any previous app revisions and defaults
+func (c *Client) ValidatePorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	base64AppProto string,
+	deploymentTarget string,
+) (*porter_app.ValidatePorterAppResponse, error) {
+	resp := &porter_app.ValidatePorterAppResponse{}
+
+	req := &porter_app.ValidatePorterAppRequest{
+		Base64AppProto:     base64AppProto,
+		DeploymentTargetId: deploymentTarget,
+	}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/validate",
+			projectID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// ApplyPorterApp takes in a base64 encoded app definition and applies it to the cluster
+func (c *Client) ApplyPorterApp(
+	ctx context.Context,
+	projectID, clusterID uint,
+	base64AppProto string,
+	deploymentTarget string,
+	appRevisionID string,
+) (*porter_app.ApplyPorterAppResponse, error) {
+	resp := &porter_app.ApplyPorterAppResponse{}
+
+	req := &porter_app.ApplyPorterAppRequest{
+		Base64AppProto:     base64AppProto,
+		DeploymentTargetId: deploymentTarget,
+		AppRevisionID:      appRevisionID,
+	}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/apply",
+			projectID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// DefaultDeploymentTarget returns the default deployment target for a given project and cluster
+func (c *Client) DefaultDeploymentTarget(
+	ctx context.Context,
+	projectID, clusterID uint,
+) (*porter_app.DefaultDeploymentTargetResponse, error) {
+	resp := &porter_app.DefaultDeploymentTargetResponse{}
+
+	req := &porter_app.DefaultDeploymentTargetRequest{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/default-deployment-target",
+			projectID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
+// CurrentAppRevision returns the currently deployed app revision for a given project, app name and deployment target
+func (c *Client) CurrentAppRevision(
+	ctx context.Context,
+	projectID uint, clusterID uint,
+	appName string, deploymentTarget string,
+) (*porter_app.LatestAppRevisionResponse, error) {
+	resp := &porter_app.LatestAppRevisionResponse{}
+
+	req := &porter_app.LatestAppRevisionRequest{
+		DeploymentTargetID: deploymentTarget,
+	}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/latest",
+			projectID, clusterID, appName,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}

+ 0 - 56
api/server/handlers/api_contract/update.go

@@ -1,12 +1,10 @@
 package api_contract
 package api_contract
 
 
 import (
 import (
-	"encoding/base64"
 	"net/http"
 	"net/http"
 
 
 	"connectrpc.com/connect"
 	"connectrpc.com/connect"
 
 
-	"github.com/google/uuid"
 	helpers "github.com/porter-dev/api-contracts/generated/go/helpers"
 	helpers "github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -38,7 +36,6 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-api-contract")
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-api-contract")
 	defer span.End()
 	defer span.End()
 
 
-	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 
 	var apiContract porterv1.Contract
 	var apiContract porterv1.Contract
@@ -50,59 +47,6 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
-	if !project.CapiProvisionerEnabled && !c.Config().EnableCAPIProvisioner {
-		// return dummy data if capi provisioner disabled in project settings, and as env var
-		// TODO: remove this stub when we can spin up all services locally, easily
-		clusterID := apiContract.Cluster.ClusterId
-		if apiContract.Cluster.ClusterId == 0 {
-			dbcli := models.Cluster{
-				ProjectID:                         uint(apiContract.Cluster.ProjectId),
-				Status:                            "UPDATING_UNAVAILABLE",
-				ProvisionedBy:                     "CAPI",
-				CloudProvider:                     "AWS",
-				CloudProviderCredentialIdentifier: apiContract.Cluster.CloudProviderCredentialsId,
-				Name:                              apiContract.Cluster.GetEksKind().ClusterName,
-				VanityName:                        apiContract.Cluster.GetEksKind().ClusterName,
-			}
-			dbcl, err := c.Config().Repo.Cluster().CreateCluster(&dbcli)
-			if err != nil {
-				e := telemetry.Error(ctx, span, err, "error updating mocking contract")
-				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
-				return
-			}
-			clusterID = int32(dbcl.ID)
-		}
-
-		by, err := helpers.MarshalContractObject(ctx, &apiContract)
-		if err != nil {
-			e := telemetry.Error(ctx, span, err, "error marshalling mock api contract")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
-			return
-		}
-		b64Contract := base64.StdEncoding.EncodeToString([]byte(by))
-
-		revisionInput := models.APIContractRevision{
-			ID:             uuid.New(),
-			ClusterID:      int(clusterID),
-			ProjectID:      int(apiContract.Cluster.ProjectId),
-			Base64Contract: b64Contract,
-		}
-		revision, err := c.Config().Repo.APIContractRevisioner().Insert(ctx, revisionInput)
-		if err != nil {
-			e := telemetry.Error(ctx, span, err, "error updating mock api contract")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
-			return
-		}
-		resp := &porterv1.ContractRevision{
-			ClusterId:  int32(clusterID),
-			ProjectId:  apiContract.Cluster.ProjectId,
-			RevisionId: revision.ID.String(),
-		}
-		w.WriteHeader(http.StatusCreated)
-		c.WriteResult(w, r, resp)
-		return
-	}
-
 	apiContract.User = &porterv1.User{
 	apiContract.User = &porterv1.User{
 		Id: int32(user.ID),
 		Id: int32(user.ID),
 	}
 	}

+ 15 - 6
api/server/handlers/cluster/get_pod_metrics.go

@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"net/http"
 
 
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
+	"github.com/porter-dev/porter/internal/telemetry"
 
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -31,31 +32,39 @@ func NewGetPodMetricsHandler(
 }
 }
 
 
 func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(ctx, "service-get-pod-metrics")
+	defer span.End()
+
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
 	request := &types.GetPodMetricsRequest{}
 	request := &types.GetPodMetricsRequest{}
 
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
 
 
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-
 	agent, err := c.GetAgent(r, cluster, "")
 	agent, err := c.GetAgent(r, cluster, "")
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error getting k8s agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		return
 	}
 	}
 
 
 	// get prometheus service
 	// get prometheus service
 	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
 	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
-
 	if err != nil || !found {
 	if err != nil || !found {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error getting prometheus service")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		return
 	}
 	}
 
 
 	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, &request.QueryOpts)
 	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, &request.QueryOpts)
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error querying prometheus")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		return
 	}
 	}
 
 

+ 91 - 0
api/server/handlers/gitinstallation/get_branch_head.go

@@ -0,0 +1,91 @@
+package gitinstallation
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/commonutils"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// GetBranchHeadHandler is the handler for the /{branch}/head endpoint
+type GetBranchHeadHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewGetBranchHeadHandler handles GET requests to /{branch}/head
+func NewGetBranchHeadHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetBranchHeadHandler {
+	return &GetBranchHeadHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// GetBranchHeadResponse is the response object for the /{branch}/head endpoint
+type GetBranchHeadResponse struct {
+	CommitSHA string `json:"commit_sha"`
+}
+
+// ServeHTTP retrieves the head commit sha for a branch
+func (c *GetBranchHeadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-branch-head")
+	defer span.End()
+
+	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		err := telemetry.Error(ctx, span, nil, "could not get owner and name from request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "owner", Value: owner},
+		telemetry.AttributeKV{Key: "name", Value: name},
+	)
+
+	branchName, ok := commonutils.GetBranchParam(c, w, r)
+	if !ok {
+		err := telemetry.Error(ctx, span, nil, "unable to get branch name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	client, err := GetGithubAppClientFromRequest(c.Config(), r)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "could not get github app client")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	branch, _, err := client.Repositories.GetBranch(ctx, owner, name, branchName, true)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "could not get branch")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if branch == nil {
+		err = telemetry.Error(ctx, span, nil, "branch does not exist")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+		return
+	}
+
+	if branch.Commit == nil || branch.Commit.SHA == nil {
+		err = telemetry.Error(ctx, span, nil, "branch head does not exist")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+		return
+	}
+
+	response := &GetBranchHeadResponse{
+		CommitSHA: *branch.Commit.SHA,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 20 - 1
api/server/handlers/gitinstallation/get_porter_yaml.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 	"github.com/porter-dev/porter/internal/telemetry"
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 )
 )
@@ -37,6 +38,9 @@ func NewGithubGetPorterYamlHandler(
 func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-porter-yaml")
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-porter-yaml")
 	defer span.End()
 	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
 	request := &types.GetPorterYamlRequest{}
 	request := &types.GetPorterYamlRequest{}
 	ok := c.DecodeAndValidate(w, r, request)
 	ok := c.DecodeAndValidate(w, r, request)
 	if !ok {
 	if !ok {
@@ -97,8 +101,23 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
+
+	if project.ValidateApplyV2 {
+		if parsed.Version == nil {
+			err = telemetry.Error(ctx, span, nil, "v2 porter yaml is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if *parsed.Version != "v2" {
+			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+	}
+
 	// backwards compatibility so that old porter yamls are no longer valid
 	// backwards compatibility so that old porter yamls are no longer valid
-	if parsed.Version != nil {
+	if !project.ValidateApplyV2 && parsed.Version != nil {
 		version := *parsed.Version
 		version := *parsed.Version
 		if version != "v1stack" {
 		if version != "v1stack" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")

+ 166 - 0
api/server/handlers/porter_app/apply.go

@@ -0,0 +1,166 @@
+package porter_app
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"connectrpc.com/connect"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+
+	"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"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ApplyPorterAppHandler is the handler for the /apps/parse endpoint
+type ApplyPorterAppHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewApplyPorterAppHandler handles POST requests to the endpoint /apps/apply
+func NewApplyPorterAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ApplyPorterAppHandler {
+	return &ApplyPorterAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// ApplyPorterAppRequest is the request object for the /apps/apply endpoint
+type ApplyPorterAppRequest struct {
+	Base64AppProto     string `json:"b64_app_proto"`
+	DeploymentTargetId string `json:"deployment_target_id"`
+	AppRevisionID      string `json:"app_revision_id"`
+}
+
+// ApplyPorterAppResponse is the response object for the /apps/apply endpoint
+type ApplyPorterAppResponse struct {
+	AppRevisionId string                 `json:"app_revision_id"`
+	CLIAction     porterv1.EnumCLIAction `json:"cli_action"`
+}
+
+// ServeHTTP translates the request into a ApplyPorterApp request, forwards to the cluster control plane, and returns the response
+func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-apply-porter-app")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+	)
+
+	if !project.ValidateApplyV2 {
+		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	request := &ApplyPorterAppRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	var appRevisionID string
+	var appProto *porterv1.PorterApp
+	var deploymentTargetID string
+
+	if request.AppRevisionID != "" {
+		appRevisionID = request.AppRevisionID
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: request.AppRevisionID})
+	} else {
+		if request.Base64AppProto == "" {
+			err := telemetry.Error(ctx, span, nil, "b64 yaml is empty")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error decoding base yaml")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		appProto = &porterv1.PorterApp{}
+		err = helpers.UnmarshalContractObject(decoded, appProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if request.DeploymentTargetId == "" {
+			err := telemetry.Error(ctx, span, err, "deployment target id is empty")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+		deploymentTargetID = request.DeploymentTargetId
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "app-name", Value: appProto.Name},
+			telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId},
+		)
+	}
+
+	applyReq := connect.NewRequest(&porterv1.ApplyPorterAppRequest{
+		ProjectId:           int64(project.ID),
+		DeploymentTargetId:  deploymentTargetID,
+		App:                 appProto,
+		PorterAppRevisionId: appRevisionID,
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.ApplyPorterApp(ctx, applyReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error calling ccp apply porter app")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if ccpResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp msg is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp.Msg.PorterAppRevisionId == "" {
+		err := telemetry.Error(ctx, span, err, "ccp resp app revision id is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "resp-app-revision-id", Value: ccpResp.Msg.PorterAppRevisionId})
+
+	if ccpResp.Msg.CliAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_UNSPECIFIED {
+		err := telemetry.Error(ctx, span, err, "ccp resp cli action is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cli-action", Value: ccpResp.Msg.CliAction.String()})
+
+	response := &ApplyPorterAppResponse{
+		AppRevisionId: ccpResp.Msg.PorterAppRevisionId,
+		CLIAction:     ccpResp.Msg.CliAction,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 2 - 3
api/server/handlers/porter_app/create.go

@@ -50,7 +50,6 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	ctx := r.Context()
 	ctx := r.Context()
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
 	defer span.End()
 	defer span.End()
@@ -302,7 +301,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 			return
 		}
 		}
 
 
-		if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+		if features.AreAgentDeployEventsEnabled(k8sAgent) {
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
 		} else {
 		} else {
@@ -491,7 +490,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 			return
 		}
 		}
 
 
-		if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+		if features.AreAgentDeployEventsEnabled(k8sAgent) {
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 		} else {
 		} else {

+ 7 - 0
api/server/handlers/porter_app/create_and_update_events.go

@@ -415,5 +415,12 @@ func getServiceNameFromPodName(podName, porterAppName string) string {
 		return podName[:index]
 		return podName[:index]
 	}
 	}
 
 
+	// if the suffix wasn't found, it's possible that the service name was too long to keep the entire suffix. example: postgres-snowflake-connector-postgres-snowflake-service-wk8gnst
+	// if this is the case, find the service name by removing everything after the last dash
+	index = strings.LastIndex(podName, "-")
+	if index != -1 {
+		return podName[:index]
+	}
+
 	return ""
 	return ""
 }
 }

+ 238 - 0
api/server/handlers/porter_app/create_app.go

@@ -0,0 +1,238 @@
+package porter_app
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// CreateAppHandler is the handler for the /apps/create endpoint
+type CreateAppHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewCreateAppHandler handles POST requests to the endpoint /apps/create
+func NewCreateAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateAppHandler {
+	return &CreateAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// SourceType is a string type specifying the source type of an app. This is specified in the incoming request
+type SourceType string
+
+const (
+	// SourceType_Github is the source kind for a github repo
+	SourceType_Github SourceType = "github"
+	// SourceType_DockerRegistry is the source kind for an app using an image from a docker registry
+	SourceType_DockerRegistry SourceType = "docker-registry"
+)
+
+// Image is the image used by an app with a docker registry source
+type Image struct {
+	Repository string `json:"repository"`
+	Tag        string `json:"tag"`
+}
+
+// CreateAppRequest is the request object for the /apps/create endpoint
+type CreateAppRequest struct {
+	Name           string     `json:"name"`
+	SourceType     SourceType `json:"type"`
+	GitBranch      string     `json:"git_branch"`
+	GitRepoName    string     `json:"git_repo_name"`
+	GitRepoID      uint       `json:"git_repo_id"`
+	PorterYamlPath string     `json:"porter_yaml_path"`
+	Image          *Image     `json:"image,omitempty"`
+}
+
+// CreateGithubAppInput is the input for creating an app with a github source
+type CreateGithubAppInput struct {
+	ProjectID           uint
+	ClusterID           uint
+	Name                string
+	GitBranch           string
+	GitRepoName         string
+	PorterYamlPath      string
+	GitRepoID           uint
+	PorterAppRepository repository.PorterAppRepository
+}
+
+// CreateDockerRegistryAppInput is the input for creating an app with a docker registry source
+type CreateDockerRegistryAppInput struct {
+	ProjectID           uint
+	ClusterID           uint
+	Name                string
+	Repository          string
+	Tag                 string
+	PorterAppRepository repository.PorterAppRepository
+}
+
+func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-app")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	if !project.ValidateApplyV2 {
+		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	request := &CreateAppRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.Name == "" {
+		err := telemetry.Error(ctx, span, nil, "name is required")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: request.Name})
+
+	if request.SourceType == "" {
+		err := telemetry.Error(ctx, span, nil, "source type is required")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "source-type", Value: request.SourceType})
+
+	var porterApp *types.PorterApp
+	switch request.SourceType {
+	case SourceType_Github:
+		if request.GitRepoID == 0 {
+			err := telemetry.Error(ctx, span, nil, "git repo id is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if request.GitBranch == "" {
+			err := telemetry.Error(ctx, span, nil, "git branch is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if request.GitRepoName == "" {
+			err := telemetry.Error(ctx, span, nil, "git repo name is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "git-branch", Value: request.GitBranch},
+			telemetry.AttributeKV{Key: "git-repo-name", Value: request.GitRepoName},
+		)
+
+		input := CreateGithubAppInput{
+			ProjectID:           project.ID,
+			ClusterID:           cluster.ID,
+			Name:                request.Name,
+			GitRepoID:           request.GitRepoID,
+			GitBranch:           request.GitBranch,
+			GitRepoName:         request.GitRepoName,
+			PorterYamlPath:      request.PorterYamlPath,
+			PorterAppRepository: c.Repo().PorterApp(),
+		}
+
+		app, err := createGithubApp(ctx, input)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error creating github app")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		porterApp = app.ToPorterAppType()
+	case SourceType_DockerRegistry:
+		if request.Image == nil {
+			err := telemetry.Error(ctx, span, nil, "image is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "image-repo-uri", Value: fmt.Sprintf("%s:%s", request.Image.Repository, request.Image.Tag)},
+		)
+
+		input := CreateDockerRegistryAppInput{
+			ProjectID:           project.ID,
+			ClusterID:           cluster.ID,
+			Name:                request.Name,
+			Repository:          request.Image.Repository,
+			Tag:                 request.Image.Tag,
+			PorterAppRepository: c.Repo().PorterApp(),
+		}
+
+		app, err := createDockerRegistryApp(ctx, input)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error creating docker registry app")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		porterApp = app.ToPorterAppType()
+	default:
+		err := telemetry.Error(ctx, span, nil, "source type not supported")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-id", Value: porterApp.ID})
+
+	c.WriteResult(w, r, porterApp)
+}
+
+func createGithubApp(ctx context.Context, input CreateGithubAppInput) (*models.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-github-app")
+	defer span.End()
+
+	porterApp := &models.PorterApp{
+		Name:           input.Name,
+		ProjectID:      input.ProjectID,
+		ClusterID:      input.ClusterID,
+		GitRepoID:      input.GitRepoID,
+		GitBranch:      input.GitBranch,
+		RepoName:       input.GitRepoName,
+		PorterYamlPath: input.PorterYamlPath,
+	}
+
+	porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
+	if err != nil {
+		return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
+	}
+
+	return porterApp, nil
+}
+
+func createDockerRegistryApp(ctx context.Context, input CreateDockerRegistryAppInput) (*models.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-docker-registry-app")
+	defer span.End()
+
+	porterApp := &models.PorterApp{
+		Name:         input.Name,
+		ProjectID:    input.ProjectID,
+		ClusterID:    input.ClusterID,
+		ImageRepoURI: fmt.Sprintf("%s:%s", input.Repository, input.Tag),
+	}
+
+	porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
+	if err != nil {
+		return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
+	}
+
+	return porterApp, nil
+}

+ 159 - 0
api/server/handlers/porter_app/current_app_revision.go

@@ -0,0 +1,159 @@
+package porter_app
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+
+	"connectrpc.com/connect"
+
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
+	"github.com/google/uuid"
+
+	"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"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// LatestAppRevisionHandler handles requests to the /apps/{porter_app_name}/latest endpoint
+type LatestAppRevisionHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewLatestAppRevisionHandler returns a new LatestAppRevisionHandler
+func NewLatestAppRevisionHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *LatestAppRevisionHandler {
+	return &LatestAppRevisionHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// LatestAppRevisionRequest is the request object for the /apps/{porter_app_name}/latest endpoint
+type LatestAppRevisionRequest struct {
+	DeploymentTargetID string `schema:"deployment_target_id"`
+}
+
+// LatestAppRevisionResponse is the response object for the /apps/{porter_app_name}/latest endpoint
+type LatestAppRevisionResponse struct {
+	// B64AppProto is the base64 encoded app proto definition
+	B64AppProto string `json:"b64_app_proto"`
+	// Status is the status of the revision
+	Status string `json:"status"`
+	// RevisionNumber is the revision number with respect to the app and deployment target
+	RevisionNumber uint64 `json:"revision_number"`
+}
+
+// ServeHTTP translates the request into a CurrentAppRevision grpc request, forwards to the cluster control plane, and returns the response.
+// Multi-cluster projects are not supported, as they may have multiple porter-apps with the same name in the same project.
+func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-latest-app-revision")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+	)
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	request := &LatestAppRevisionRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	_, err := uuid.Parse(request.DeploymentTargetID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing deployment target id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
+
+	porterApps, err := c.Repo().PorterApp().ReadPorterAppsByProjectIDAndName(project.ID, appName)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting porter app from repo")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if len(porterApps) == 0 {
+		err := telemetry.Error(ctx, span, err, "no porter apps returned")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if len(porterApps) > 1 {
+		err := telemetry.Error(ctx, span, err, "multiple porter apps returned; unable to determine which one to use")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if porterApps[0].ID == 0 {
+		err := telemetry.Error(ctx, span, err, "porter app id is missiong")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	currentAppRevisionReq := connect.NewRequest(&porterv1.CurrentAppRevisionRequest{
+		ProjectId:          int64(project.ID),
+		AppId:              int64(porterApps[0].ID),
+		DeploymentTargetId: request.DeploymentTargetID,
+	})
+
+	currentAppRevisionResp, err := c.Config().ClusterControlPlaneClient.CurrentAppRevision(ctx, currentAppRevisionReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting current app revision from cluster control plane client")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if currentAppRevisionResp == nil || currentAppRevisionResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "current app revision resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if currentAppRevisionResp.Msg.App == nil {
+		err := telemetry.Error(ctx, span, err, "current app revision definition is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	encoded, err := helpers.MarshalContractObject(ctx, currentAppRevisionResp.Msg.App)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error marshalling app proto back to json")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	b64 := base64.StdEncoding.EncodeToString(encoded)
+
+	response := &LatestAppRevisionResponse{
+		B64AppProto:    b64,
+		Status:         currentAppRevisionResp.Msg.Status,
+		RevisionNumber: currentAppRevisionResp.Msg.RevisionNumber,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 82 - 0
api/server/handlers/porter_app/default_deployment_target.go

@@ -0,0 +1,82 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"github.com/google/uuid"
+
+	"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"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// DefaultDeploymentTargetHandler handles requests to the /default-deployment-target endpoint
+type DefaultDeploymentTargetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewDefaultDeploymentTargetHandler returns a new DefaultDeploymentTargetHandler
+func NewDefaultDeploymentTargetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *DefaultDeploymentTargetHandler {
+	return &DefaultDeploymentTargetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// DefaultDeploymentTargetRequest is the request object for the /default-deployment-target endpoint
+type DefaultDeploymentTargetRequest struct{}
+
+// DefaultDeploymentTargetResponse is the response object for the /default-deployment-target endpoint
+type DefaultDeploymentTargetResponse struct {
+	DeploymentTargetID string `json:"deployment_target_id"`
+}
+
+const (
+	// DeploymentTargetSelector_Default is the selector for the default deployment target in a cluster
+	DeploymentTargetSelector_Default = "default"
+	// DeploymentTargetSelectorType_Default is the selector type for the default deployment target in a cluster
+	DeploymentTargetSelectorType_Default = "NAMESPACE"
+)
+
+// ServeHTTP receives a project id and cluster id and returns the default deployment target in the cluster
+func (c *DefaultDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-default-deployment-target")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+	)
+
+	defaultDeploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType(project.ID, cluster.ID, DeploymentTargetSelector_Default, DeploymentTargetSelectorType_Default)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting default deployment target from repo")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if defaultDeploymentTarget.ID == uuid.Nil {
+		err := telemetry.Error(ctx, span, err, "default deployment target not found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: defaultDeploymentTarget.ID.String()})
+
+	response := &DefaultDeploymentTargetResponse{
+		DeploymentTargetID: defaultDeploymentTarget.ID.String(),
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 36 - 3
api/server/handlers/porter_app/get.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
@@ -25,6 +26,7 @@ func NewGetPorterAppHandler(
 ) *GetPorterAppHandler {
 ) *GetPorterAppHandler {
 	return &GetPorterAppHandler{
 	return &GetPorterAppHandler{
 		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
 		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
 	}
 	}
 }
 }
 
 
@@ -34,17 +36,48 @@ func (c *GetPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	defer span.End()
 	defer span.End()
 
 
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
 	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	if reqErr != nil {
 	if reqErr != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		err := telemetry.Error(ctx, span, nil, "error parsing porter app name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
 
 
 	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error reading porter app by name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if app == nil || app.ID == 0 {
+		err = telemetry.Error(ctx, span, nil, "app with name does not exist in project")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	// this is a temporary fix until we figure out how to reconcile the new revisions table
+	// with dependencies on helm releases throuhg the api
+	if project.ValidateApplyV2 {
+		c.WriteResult(w, r, app.ToPorterAppType())
+		return
+	}
+
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	helmRelease, err := helmAgent.GetRelease(ctx, appName, 0, false)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting helm release for app")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		return
 	}
 	}
 
 
-	c.WriteResult(w, r, app.ToPorterAppType())
+	c.WriteResult(w, r, app.ToPorterAppTypeWithRevision(helmRelease.Version))
 }
 }

+ 9 - 7
api/server/handlers/porter_app/get_logs_within_time_range.go

@@ -50,6 +50,12 @@ func (c *GetLogsWithinTimeRangeHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 		return
 	}
 	}
 
 
+	if (request.PodSelector != "" && request.ChartName != "") || (request.PodSelector == "" && request.ChartName == "") {
+		err := telemetry.Error(ctx, span, nil, "must provide either pod selector or chart name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
 	agent, err := c.GetAgent(r, cluster, "")
 	agent, err := c.GetAgent(r, cluster, "")
 	if err != nil {
 	if err != nil {
 		_ = telemetry.Error(ctx, span, err, "unable to get agent")
 		_ = telemetry.Error(ctx, span, err, "unable to get agent")
@@ -74,11 +80,6 @@ func (c *GetLogsWithinTimeRangeHandler) ServeHTTP(w http.ResponseWriter, r *http
 
 
 	var podSelector string
 	var podSelector string
 	if request.ChartName == "" {
 	if request.ChartName == "" {
-		if request.PodSelector == "" {
-			err = telemetry.Error(ctx, span, nil, "must provide either chart name or pod selector")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-			return
-		}
 		podSelector = request.PodSelector
 		podSelector = request.PodSelector
 	} else {
 	} else {
 		// get the pod values which will be used to get the correct pod selector
 		// get the pod values which will be used to get the correct pod selector
@@ -111,13 +112,14 @@ func (c *GetLogsWithinTimeRangeHandler) ServeHTTP(w http.ResponseWriter, r *http
 				for _, pod := range pods {
 				for _, pod := range pods {
 					if pod.GetCreationTimestamp().Time.After(request.StartRange) && pod.GetCreationTimestamp().Time.Before(request.EndRange) {
 					if pod.GetCreationTimestamp().Time.After(request.StartRange) && pod.GetCreationTimestamp().Time.Before(request.EndRange) {
 						if latestPod == nil || pod.GetCreationTimestamp().Time.After(latestPod.GetCreationTimestamp().Time) {
 						if latestPod == nil || pod.GetCreationTimestamp().Time.After(latestPod.GetCreationTimestamp().Time) {
-							latestPod = &pod
+							copyPod := pod
+							latestPod = &copyPod
 						}
 						}
 					}
 					}
 				}
 				}
 			}
 			}
 			if latestPod == nil {
 			if latestPod == nil {
-				err = telemetry.Error(ctx, span, nil, "no pods found within timerange")
+				err = telemetry.Error(ctx, span, nil, "unable to retrieve logs for latest job")
 				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
 				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
 				return
 				return
 			}
 			}

+ 18 - 0
api/server/handlers/porter_app/parse.go

@@ -942,6 +942,24 @@ func addLabelsToService(service *Service, envGroups []string, defaultLabelKey st
 		}
 		}
 	}
 	}
 
 
+	if _, ok := service.Config["podLabels"]; !ok {
+		service.Config["podLabels"] = make(map[string]string)
+	}
+	switch service.Config["podLabels"].(type) {
+	case map[string]string:
+		service.Config["podLabels"].(map[string]string)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
+	case map[string]any:
+		service.Config["podLabels"].(map[string]any)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
+	case any:
+		if val, ok := service.Config["podLabels"].(string); ok {
+			if val == "" {
+				service.Config["podLabels"] = map[string]string{
+					defaultLabelKey: porter_app.LabelValue_PorterApplication,
+				}
+			}
+		}
+	}
+
 	return service
 	return service
 }
 }
 
 

+ 111 - 0
api/server/handlers/porter_app/parse_yaml.go

@@ -0,0 +1,111 @@
+package porter_app
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+
+	"github.com/porter-dev/porter/internal/porter_app"
+
+	"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"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ParsePorterYAMLToProtoHandler is the handler for the /apps/parse endpoint
+type ParsePorterYAMLToProtoHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewParsePorterYAMLToProtoHandler returns a new ParsePorterYAMLToProtoHandler
+func NewParsePorterYAMLToProtoHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ParsePorterYAMLToProtoHandler {
+	return &ParsePorterYAMLToProtoHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// ParsePorterYAMLToProtoRequest is the request object for the /apps/parse endpoint
+type ParsePorterYAMLToProtoRequest struct {
+	B64Yaml string `json:"b64_yaml"`
+}
+
+// ParsePorterYAMLToProtoResponse is the response object for the /apps/parse endpoint
+type ParsePorterYAMLToProtoResponse struct {
+	B64AppProto string `json:"b64_app_proto"`
+}
+
+// ServeHTTP receives a base64-encoded porter.yaml, parses the version, and then translates it into a base64-encoded app proto object
+func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-parse-porter-yaml")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	if !project.ValidateApplyV2 {
+		err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	request := &ParsePorterYAMLToProtoRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.B64Yaml == "" {
+		err := telemetry.Error(ctx, span, nil, "b64 yaml is empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	yaml, err := base64.StdEncoding.DecodeString(request.B64Yaml)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error decoding b64 yaml")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if yaml == nil {
+		err := telemetry.Error(ctx, span, nil, "decoded yaml is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appProto, err := porter_app.ParseYAML(ctx, yaml)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing yaml")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if appProto == nil {
+		err := telemetry.Error(ctx, span, nil, "app proto is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	by, err := helpers.MarshalContractObject(ctx, appProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, nil, "error marshalling app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	b64 := base64.StdEncoding.EncodeToString(by)
+
+	response := &ParsePorterYAMLToProtoResponse{
+		B64AppProto: b64,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 1 - 2
api/server/handlers/porter_app/rollback.go

@@ -39,7 +39,6 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-rollback-porter-app")
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-rollback-porter-app")
 	defer span.End()
 	defer span.End()
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 
 	request := &types.RollbackPorterAppRequest{}
 	request := &types.RollbackPorterAppRequest{}
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -154,7 +153,7 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
-	if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+	if features.AreAgentDeployEventsEnabled(k8sAgent) {
 		serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 		serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 		_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, latestHelmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 		_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, latestHelmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 	} else {
 	} else {

+ 110 - 0
api/server/handlers/porter_app/run_command.go

@@ -0,0 +1,110 @@
+package porter_app
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
+	"github.com/porter-dev/porter/internal/kubernetes/porter_app"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// RunPorterAppCommandHandler runs a command on a porter app
+type RunPorterAppCommandHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewRunPorterAppCommandHandler returns a new RunPorterAppCommandHandler
+func NewRunPorterAppCommandHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *RunPorterAppCommandHandler {
+	return &RunPorterAppCommandHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *RunPorterAppCommandHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-run-porter-app-command")
+	defer span.End()
+
+	request := &types.RunPorterAppCommandRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error getting app name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	namespace := utils.NamespaceFromPorterAppName(appName)
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
+
+	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error reading app from DB")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if app == nil {
+		err = telemetry.Error(ctx, span, nil, "app with name does not exist in project")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	k8sAgent, err := c.GetAgent(r, cluster, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting k8s agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	podList, err := k8sAgent.GetPodsByLabel(porter_app.LabelKey_PorterApplication, namespace)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting pods by label")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if len(podList.Items) == 0 {
+		err = telemetry.Error(ctx, span, err, "no pods found to run command on")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	selectedPod := podList.Items[0]
+	execArgs := strings.Split(request.Command, " ")
+	if app.Builder != "" &&
+		(strings.Contains(app.Builder, "heroku") ||
+			strings.Contains(app.Builder, "paketo")) &&
+		execArgs[0] != "/cnb/lifecycle/launcher" &&
+		execArgs[0] != "launcher" {
+		// this is a buildpacks release using a heroku builder, so we prepend commands with launcher command
+		execArgs = append([]string{"/cnb/lifecycle/launcher"}, execArgs...)
+	}
+
+	err = k8sAgent.RunCommandOnPod(ctx, &selectedPod, execArgs)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error running command on pod")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+}

+ 154 - 0
api/server/handlers/porter_app/validate.go

@@ -0,0 +1,154 @@
+package porter_app
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"connectrpc.com/connect"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+
+	"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"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ValidatePorterAppHandler is handles requests to the /apps/validate endpoint
+type ValidatePorterAppHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewValidatePorterAppHandler returns a new ValidatePorterAppHandler
+func NewValidatePorterAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ValidatePorterAppHandler {
+	return &ValidatePorterAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// ValidatePorterAppRequest is the request object for the /apps/validate endpoint
+type ValidatePorterAppRequest struct {
+	Base64AppProto     string `json:"b64_app_proto"`
+	DeploymentTargetId string `json:"deployment_target_id"`
+	CommitSHA          string `json:"commit_sha"`
+}
+
+// ValidatePorterAppResponse is the response object for the /apps/validate endpoint
+type ValidatePorterAppResponse struct {
+	ValidatedBase64AppProto string `json:"validate_b64_app_proto"`
+}
+
+// ServeHTTP translates requests into protobuf objects and forwards them to the cluster control plane, returning the result
+func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-validate-porter-app")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+	)
+
+	if !project.ValidateApplyV2 {
+		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	request := &ValidatePorterAppRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.Base64AppProto == "" {
+		err := telemetry.Error(ctx, span, nil, "b64 yaml is empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error decoding base  yaml")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appProto := &porterv1.PorterApp{}
+	err = helpers.UnmarshalContractObject(decoded, appProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if appProto.Name == "" {
+		err := telemetry.Error(ctx, span, err, "app proto name is empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "app-name", Value: appProto.Name},
+		telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId},
+		telemetry.AttributeKV{Key: "commit-sha", Value: request.CommitSHA},
+	)
+
+	validateReq := connect.NewRequest(&porterv1.ValidatePorterAppRequest{
+		ProjectId:          int64(project.ID),
+		DeploymentTargetId: request.DeploymentTargetId,
+		CommitSha:          request.CommitSHA,
+		App:                appProto,
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.ValidatePorterApp(ctx, validateReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error calling ccp validate porter app")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if ccpResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp msg is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp.Msg.App == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp app is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	encoded, err := helpers.MarshalContractObject(ctx, ccpResp.Msg.App)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error marshalling app proto back to json")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	b64 := base64.StdEncoding.EncodeToString(encoded)
+
+	response := &ValidatePorterAppResponse{
+		ValidatedBase64AppProto: b64,
+	}
+
+	c.WriteResult(w, r, response)
+}

+ 1 - 0
api/server/handlers/project/create.go

@@ -45,6 +45,7 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		SimplifiedViewEnabled:  true,
 		SimplifiedViewEnabled:  true,
 		HelmValuesEnabled:      false,
 		HelmValuesEnabled:      false,
 		MultiCluster:           false,
 		MultiCluster:           false,
+		EnableReprovision:      false,
 	}
 	}
 
 
 	var err error
 	var err error

+ 1 - 0
api/server/handlers/project/create_test.go

@@ -46,6 +46,7 @@ func TestCreateProjectSuccessful(t *testing.T) {
 		SimplifiedViewEnabled:  true,
 		SimplifiedViewEnabled:  true,
 		HelmValuesEnabled:      false,
 		HelmValuesEnabled:      false,
 		MultiCluster:           false,
 		MultiCluster:           false,
+		EnableReprovision:      false,
 	}
 	}
 
 
 	gotProject := &types.CreateProjectResponse{}
 	gotProject := &types.CreateProjectResponse{}

+ 46 - 2
api/server/handlers/project_integration/create_gcp.go

@@ -1,8 +1,11 @@
 package project_integration
 package project_integration
 
 
 import (
 import (
+	"encoding/base64"
 	"net/http"
 	"net/http"
 
 
+	"connectrpc.com/connect"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"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/apierrors"
@@ -10,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
 
 
 type CreateGCPHandler struct {
 type CreateGCPHandler struct {
@@ -27,8 +31,11 @@ func NewCreateGCPHandler(
 }
 }
 
 
 func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-gcp-credentials")
+	defer span.End()
+
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 
 	request := &types.CreateGCPRequest{}
 	request := &types.CreateGCPRequest{}
 
 
@@ -36,6 +43,43 @@ func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	if project.CapiProvisionerEnabled {
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "capi-provisioner-enabled", Value: true})
+
+		b64Key := base64.StdEncoding.EncodeToString([]byte(request.GCPKeyData))
+
+		ccpCredentialsInput := &connect.Request[porterv1.UpdateCloudProviderCredentialsRequest]{
+			Msg: &porterv1.UpdateCloudProviderCredentialsRequest{
+				ProjectId:     int64(project.ID),
+				CloudProvider: porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_GCP,
+				CloudProviderCredentials: &porterv1.UpdateCloudProviderCredentialsRequest_GcpCredentials{
+					GcpCredentials: &porterv1.GCPCredentials{
+						ServiceAccountJsonBase64: b64Key,
+					},
+				},
+			},
+		}
+		ccpCredentialsResponse, err := p.Config().ClusterControlPlaneClient.UpdateCloudProviderCredentials(ctx, ccpCredentialsInput)
+		if err != nil {
+			e := telemetry.Error(ctx, span, err, "failed to update cloud provider credentials")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
+			return
+		}
+		if ccpCredentialsResponse.Msg == nil {
+			e := telemetry.Error(ctx, span, nil, "nil response when updating provider credentials")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
+			return
+		}
+
+		res := types.CreateGCPResponse{
+			IsCCPCluster:                      true,
+			CloudProviderCredentialIdentifier: ccpCredentialsResponse.Msg.CredentialsIdentifier,
+		}
+
+		p.WriteResult(w, r, res)
+		return
+	}
+
 	gcp := CreateGCPIntegration(request, project.ID, user.ID)
 	gcp := CreateGCPIntegration(request, project.ID, user.ID)
 
 
 	gcp, err := p.Repo().GCPIntegration().CreateGCPIntegration(gcp)
 	gcp, err := p.Repo().GCPIntegration().CreateGCPIntegration(gcp)

+ 2 - 1
api/server/handlers/registry/create_repository.go

@@ -48,7 +48,8 @@ func (p *RegistryCreateRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 
 	// parse the name from the registry
 	// parse the name from the registry
 	nameSpl := strings.Split(request.ImageRepoURI, "/")
 	nameSpl := strings.Split(request.ImageRepoURI, "/")
-	repoName := strings.ToLower(strings.ReplaceAll(nameSpl[len(nameSpl)-1], "_", "-"))
+	sanitizedName := strings.ReplaceAll(strings.ReplaceAll(nameSpl[len(nameSpl)-1], "_", "-"), ".", "-")
+	repoName := strings.ToLower(sanitizedName)
 	telemetry.WithAttributes(span,
 	telemetry.WithAttributes(span,
 		telemetry.AttributeKV{Key: "repo-name", Value: repoName},
 		telemetry.AttributeKV{Key: "repo-name", Value: repoName},
 		telemetry.AttributeKV{Key: "registry-id", Value: reg.ID},
 		telemetry.AttributeKV{Key: "registry-id", Value: reg.ID},

+ 55 - 14
api/server/handlers/registry/get_token.go

@@ -67,7 +67,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 
 
 		resp := &types.GetRegistryTokenResponse{
 		resp := &types.GetRegistryTokenResponse{
 			Token:     ecrResponse.Msg.Token,
 			Token:     ecrResponse.Msg.Token,
-			ExpiresAt: &expiry,
+			ExpiresAt: expiry,
 		}
 		}
 
 
 		c.WriteResult(w, r, resp)
 		c.WriteResult(w, r, resp)
@@ -82,7 +82,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 	}
 
 
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	for _, reg := range regs {
 	for _, reg := range regs {
 		if reg.AWSIntegrationID != 0 {
 		if reg.AWSIntegrationID != 0 {
@@ -123,8 +123,11 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 					return
 					return
 				}
 				}
 
 
+				if output == nil || output.AuthorizationData == nil || len(output.AuthorizationData) == 0 {
+					continue
+				}
 				token = *output.AuthorizationData[0].AuthorizationToken
 				token = *output.AuthorizationData[0].AuthorizationToken
-				expiresAt = output.AuthorizationData[0].ExpiresAt
+				expiresAt = *output.AuthorizationData[0].ExpiresAt
 			}
 			}
 		}
 		}
 	}
 	}
@@ -172,7 +175,7 @@ func (c *RegistryGetGCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 	}
 
 
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	for _, reg := range regs {
 	for _, reg := range regs {
 		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
 		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
@@ -191,7 +194,7 @@ func (c *RegistryGetGCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 			}
 			}
 
 
 			token = oauthTok.AccessToken
 			token = oauthTok.AccessToken
-			expiresAt = &oauthTok.Expiry
+			expiresAt = oauthTok.Expiry
 			break
 			break
 		}
 		}
 	}
 	}
@@ -238,8 +241,43 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 		return
 	}
 	}
 
 
+	if len(regs) == 0 {
+		e := telemetry.Error(ctx, span, err, "no registries found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusNotFound))
+		return
+	}
+
+	if proj.CapiProvisionerEnabled {
+		regInput := connect.NewRequest(&porterv1.TokenForRegistryRequest{
+			ProjectId:   int64(proj.ID),
+			RegistryUri: regs[0].URL,
+		})
+		regOutput, err := c.Config().ClusterControlPlaneClient.TokenForRegistry(ctx, regInput)
+		if err != nil {
+			e := telemetry.Error(ctx, span, err, "error getting gar token")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
+			return
+		}
+		if regOutput == nil || regOutput.Msg == nil {
+			e := telemetry.Error(ctx, span, err, "error reading gar token")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
+			return
+		}
+		if regOutput.Msg.Token == "" {
+			e := telemetry.Error(ctx, span, err, "no token for for registry")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
+			return
+		}
+		resp := &types.GetRegistryTokenResponse{
+			Token:     regOutput.Msg.Token,
+			ExpiresAt: regOutput.Msg.Expiry.AsTime(),
+		}
+		c.WriteResult(w, r, resp)
+		return
+	}
+
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	for _, reg := range regs {
 	for _, reg := range regs {
 		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
 		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
@@ -257,8 +295,11 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 				c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(e))
 				c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(e))
 			}
 			}
 
 
+			if oauthTok == nil {
+				continue
+			}
 			token = oauthTok.AccessToken
 			token = oauthTok.AccessToken
-			expiresAt = &oauthTok.Expiry
+			expiresAt = oauthTok.Expiry
 			break
 			break
 		}
 		}
 	}
 	}
@@ -302,7 +343,7 @@ func (c *RegistryGetDOCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 	}
 
 
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	for _, reg := range regs {
 	for _, reg := range regs {
 		if reg.DOIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
 		if reg.DOIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
@@ -323,7 +364,7 @@ func (c *RegistryGetDOCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 			}
 			}
 
 
 			token = tok
 			token = tok
-			expiresAt = expiry
+			expiresAt = *expiry
 			break
 			break
 		}
 		}
 	}
 	}
@@ -361,7 +402,7 @@ func (c *RegistryGetDockerhubTokenHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}
 	}
 
 
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	for _, reg := range regs {
 	for _, reg := range regs {
 		if reg.BasicIntegrationID != 0 && strings.Contains(reg.URL, "index.docker.io") {
 		if reg.BasicIntegrationID != 0 && strings.Contains(reg.URL, "index.docker.io") {
@@ -375,7 +416,7 @@ func (c *RegistryGetDockerhubTokenHandler) ServeHTTP(w http.ResponseWriter, r *h
 
 
 			// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 			// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 			timeExpires := time.Now().Add(30 * 24 * 3600 * time.Second)
 			timeExpires := time.Now().Add(30 * 24 * 3600 * time.Second)
-			expiresAt = &timeExpires
+			expiresAt = timeExpires
 		}
 		}
 	}
 	}
 
 
@@ -435,7 +476,7 @@ func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 	}
 
 
 	var token string
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 
 	var matchingReg *models.Registry
 	var matchingReg *models.Registry
 	for _, reg := range regs {
 	for _, reg := range regs {
@@ -482,7 +523,7 @@ func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 
 
 		// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 		// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
-		expiresAt = &timeExpires
+		expiresAt = timeExpires
 	}
 	}
 
 
 	if matchingReg.AzureIntegrationID != 0 {
 	if matchingReg.AzureIntegrationID != 0 {
@@ -499,7 +540,7 @@ func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		token = base64.StdEncoding.EncodeToString([]byte(string(username) + ":" + string(pw)))
 		token = base64.StdEncoding.EncodeToString([]byte(string(username) + ":" + string(pw)))
 		// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 		// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
-		expiresAt = &timeExpires
+		expiresAt = timeExpires
 	}
 	}
 
 
 	if token == "" {
 	if token == "" {

+ 18 - 5
api/server/handlers/release/update_canonical_name.go

@@ -13,6 +13,8 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 	"k8s.io/apimachinery/pkg/util/validation"
 	"k8s.io/apimachinery/pkg/util/validation"
 )
 )
@@ -34,9 +36,16 @@ func NewUpdateCanonicalNameHandler(
 }
 }
 
 
 func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "change-canonical-name")
+	defer span.End()
+
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
 	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
-	namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	// namespace, _ := requestutils.GetURLParamString(r, types.URLParamNamespace)
+
+	helmRelease, _ := ctx.Value(types.ReleaseScope).(*release.Release)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "release-name", Value: helmRelease.Name})
 
 
 	request := &types.UpdateCanonicalNameRequest{}
 	request := &types.UpdateCanonicalNameRequest{}
 
 
@@ -44,13 +53,15 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		return
 		return
 	}
 	}
 
 
-	release, err := c.Repo().Release().ReadRelease(cluster.ID, name, namespace)
+	release, err := c.Repo().Release().ReadRelease(cluster.ID, helmRelease.Name, helmRelease.Namespace)
 	if err != nil {
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("release %s not found", name)))
+			err = telemetry.Error(ctx, span, err, "unable to get release")
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("release %s not found: %s", name, err)))
 			return
 			return
 		}
 		}
 
 
+		err = telemetry.Error(ctx, span, err, "unable to get release resource")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -58,7 +69,8 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	if release.CanonicalName != request.CanonicalName {
 	if release.CanonicalName != request.CanonicalName {
 		if request.CanonicalName != "" {
 		if request.CanonicalName != "" {
 			if errStrs := validation.IsDNS1123Label(request.CanonicalName); len(errStrs) > 0 {
 			if errStrs := validation.IsDNS1123Label(request.CanonicalName); len(errStrs) > 0 {
-				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid canonical name"), http.StatusBadRequest))
+				err = telemetry.Error(ctx, span, err, "canonical name is incorrect")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid canonical name %s", err), http.StatusBadRequest))
 				return
 				return
 			}
 			}
 		}
 		}
@@ -68,6 +80,7 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		release, err = c.Repo().Release().UpdateRelease(release)
 		release, err = c.Repo().Release().UpdateRelease(release)
 
 
 		if err != nil {
 		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error updating chart")
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 			return
 		}
 		}

+ 37 - 0
api/server/router/git_installation.go

@@ -625,6 +625,43 @@ func getGitInstallationRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/gitrepos/{installation_id}/repos/{kind}/{owner}/{name}/{branch}/head ->
+	// gitinstallation.NewGetBranchHeadHandler
+	getBranchHeadEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/repos/{%s}/{%s}/{%s}/{%s}/head",
+					relPath,
+					types.URLParamGitKind,
+					types.URLParamGitRepoOwner,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitInstallationScope,
+			},
+		},
+	)
+
+	getBranchHeadHandler := gitinstallation.NewGetBranchHeadHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getBranchHeadEndpoint,
+		Handler:  getBranchHeadHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/gitrepos/{installation_id}/repos/{kind}/{owner}/{name}/{branch}/procfile ->
 	// GET /api/projects/{project_id}/gitrepos/{installation_id}/repos/{kind}/{owner}/{name}/{branch}/procfile ->
 	// gitinstallation.NewGithubGetProcfileHandler
 	// gitinstallation.NewGithubGetProcfileHandler
 	getProcfileEndpoint := factory.NewAPIEndpoint(
 	getProcfileEndpoint := factory.NewAPIEndpoint(

+ 203 - 0
api/server/router/porter_app.go

@@ -454,6 +454,35 @@ func getPorterAppRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/{porter_app_name}/run -> porter_app.NewRunPorterAppCommandHandler
+	runPorterAppCommandEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/run", relPath, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	runPorterAppCommandHandler := porter_app.NewRunPorterAppCommandHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: runPorterAppCommandEndpoint,
+		Handler:  runPorterAppCommandHandler,
+		Router:   r,
+	})
+
 	// TODO: remove these three endpoints once these three 'stacks' routes are no longer used in telemetry
 	// TODO: remove these three endpoints once these three 'stacks' routes are no longer used in telemetry
 
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
@@ -542,5 +571,179 @@ func getPorterAppRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/parse -> porter_app.NewParsePorterYAMLToProtoHandler
+	parsePorterYAMLToProtoEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/parse",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	parsePorterYAMLToProtoHandler := porter_app.NewParsePorterYAMLToProtoHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: parsePorterYAMLToProtoEndpoint,
+		Handler:  parsePorterYAMLToProtoHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/validate -> porter_app.NewValidatePorterAppHandler
+	validatePorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/validate",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	validatePorterAppHandler := porter_app.NewValidatePorterAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: validatePorterAppEndpoint,
+		Handler:  validatePorterAppHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/create -> porter_app.NewCreateAppHandler
+	createAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/create",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createAppHandler := porter_app.NewCreateAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createAppEndpoint,
+		Handler:  createAppHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/apply -> porter_app.NewApplyPorterAppHandler
+	applyPorterAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/apply",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	applyPorterAppHandler := porter_app.NewApplyPorterAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: applyPorterAppEndpoint,
+		Handler:  applyPorterAppHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/default-deployment-target -> porter_app.NewDefaultDeploymentTargetHandler
+	defaultDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/default-deployment-target",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	defaultDeploymentTargetHandler := porter_app.NewDefaultDeploymentTargetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: defaultDeploymentTargetEndpoint,
+		Handler:  defaultDeploymentTargetHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/latest -> porter_app.NewCurrentAppRevisionHandler
+	currentAppRevisionEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/apps/{%s}/latest", types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	currentAppRevisionHandler := porter_app.NewLatestAppRevisionHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: currentAppRevisionEndpoint,
+		Handler:  currentAppRevisionHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 	return routes, newPath
 }
 }

+ 7 - 7
api/server/shared/features/features.go

@@ -41,18 +41,18 @@ func isPorterAgentUpdated(agent *kubernetes.Agent, major, minor, patch int) bool
 	parsedPatch, _ := strconv.Atoi(parsedTag[2])
 	parsedPatch, _ := strconv.Atoi(parsedTag[2])
 	if parsedMajor < major {
 	if parsedMajor < major {
 		return false
 		return false
+	} else if parsedMajor > major {
+		return true
 	}
 	}
 	if parsedMinor < minor {
 	if parsedMinor < minor {
 		return false
 		return false
+	} else if parsedMinor > minor {
+		return true
 	}
 	}
-	if parsedPatch < patch {
-		return false
-	}
-	return true
+	return parsedPatch >= patch
 }
 }
 
 
 // Only create the PROGRESSING event if the cluster's agent is updated, because only the updated agent can update the status
 // Only create the PROGRESSING event if the cluster's agent is updated, because only the updated agent can update the status
-// TODO: remove dependence on porter email once we are ready to release this feature
-func AreAgentDeployEventsEnabled(email string, agent *kubernetes.Agent) bool {
-	return isPorterAgentUpdated(agent, 3, 1, 6) && strings.HasSuffix(email, "porter.run")
+func AreAgentDeployEventsEnabled(agent *kubernetes.Agent) bool {
+	return isPorterAgentUpdated(agent, 3, 1, 6)
 }
 }

+ 24 - 0
api/types/porter_app.go

@@ -68,6 +68,11 @@ type UpdatePorterAppRequest struct {
 	PullRequestURL string `json:"pull_request_url"`
 	PullRequestURL string `json:"pull_request_url"`
 }
 }
 
 
+// RunPorterAppCommandRequest represents a request to run a command on a pod of a porter app
+type RunPorterAppCommandRequest struct {
+	Command string `json:"command" form:"required"`
+}
+
 type RollbackPorterAppRequest struct {
 type RollbackPorterAppRequest struct {
 	Revision int `json:"revision" form:"required"`
 	Revision int `json:"revision" form:"required"`
 }
 }
@@ -143,3 +148,22 @@ type ServiceDeploymentMetadata struct {
 	// Type is the type of the service - one of web, worker, or job
 	// Type is the type of the service - one of web, worker, or job
 	Type string `json:"type"`
 	Type string `json:"type"`
 }
 }
+type ListEnvironmentGroupsResponse struct {
+	// EnvironmentGroups is a list of environment groups
+	EnvironmentGroups []EnvironmentGroupListItem `json:"environment_groups,omitempty"`
+}
+
+type EnvironmentGroupListItem struct {
+	// Name is the name of the environment group
+	Name string `json:"name"`
+	// LatestVersion is the latest version of the environment group
+	LatestVersion int `json:"latest_version"`
+	// Variables is a map of variables for the environment group
+	Variables map[string]string `json:"variables"`
+	// SecretVariables is a map of secret variables for the environment group
+	SecretVariables map[string]string `json:"secret_variables"`
+	// CreatedAtUTC is the time the environment group was created
+	CreatedAtUTC time.Time `json:"created_at"`
+	// LinkedApplications is the list of applications this env group is linked to
+	LinkedApplications []string `json:"linked_applications,omitempty"`
+}

+ 6 - 0
api/types/project.go

@@ -14,6 +14,9 @@ type Project struct {
 	AzureEnabled           bool    `json:"azure_enabled"`
 	AzureEnabled           bool    `json:"azure_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	MultiCluster           bool    `json:"multi_cluster"`
 	MultiCluster           bool    `json:"multi_cluster"`
+	FullAddOns             bool    `json:"full_add_ons"`
+	EnableReprovision      bool    `json:"enable_reprovision"`
+	ValidateApplyV2        bool    `json:"validate_apply_v2"`
 }
 }
 
 
 type FeatureFlags struct {
 type FeatureFlags struct {
@@ -26,6 +29,9 @@ type FeatureFlags struct {
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,omitempty"`
 	MultiCluster               bool   `json:"multi_cluster,omitempty"`
 	MultiCluster               bool   `json:"multi_cluster,omitempty"`
+	FullAddOns                 bool   `json:"full_add_ons,omitempty"`
+	EnableReprovision          bool   `json:"enable_reprovision,omitempty"`
+	ValidateApplyV2            bool   `json:"validate_apply_v2"`
 }
 }
 
 
 type CreateProjectRequest struct {
 type CreateProjectRequest struct {

+ 4 - 0
api/types/project_integration.go

@@ -138,6 +138,10 @@ type CreateGCPRequest struct {
 
 
 type CreateGCPResponse struct {
 type CreateGCPResponse struct {
 	*GCPIntegration
 	*GCPIntegration
+	// IsCCPCluster is true if the cluster is managed through CCP, instead of the legacy provisioner
+	IsCCPCluster bool `json:"is_ccp_cluster"`
+	// CloudProviderCredentialIdentifier is the identifier for the cloud provider credential for CCP clusters
+	CloudProviderCredentialIdentifier string `json:"cloud_provider_credentials_id"`
 }
 }
 
 
 type AzureIntegration struct {
 type AzureIntegration struct {

+ 2 - 2
api/types/registry.go

@@ -172,8 +172,8 @@ type UpdateRegistryRequest struct {
 }
 }
 
 
 type GetRegistryTokenResponse struct {
 type GetRegistryTokenResponse struct {
-	Token     string     `json:"token"`
-	ExpiresAt *time.Time `json:"expires_at"`
+	Token     string    `json:"token"`
+	ExpiresAt time.Time `json:"expires_at"`
 }
 }
 
 
 type GetRegistryACRTokenRequest struct {
 type GetRegistryACRTokenRequest struct {

+ 3 - 0
api/types/template.go

@@ -33,6 +33,9 @@ type PorterTemplateSimple struct {
 
 
 	// The repo URL for the template
 	// The repo URL for the template
 	RepoURL string `json:"repo_url,omitempty"`
 	RepoURL string `json:"repo_url,omitempty"`
+
+	//
+	Tags []string `json:"tags,omitempty"`
 }
 }
 
 
 // ListTemplatesResponse is how a chart gets displayed when listed
 // ListTemplatesResponse is how a chart gets displayed when listed

+ 0 - 267
cli/cmd/auth.go

@@ -1,267 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"os"
-
-	"github.com/fatih/color"
-
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/cli/cmd/config"
-	loginBrowser "github.com/porter-dev/porter/cli/cmd/login"
-	"github.com/porter-dev/porter/cli/cmd/utils"
-	"github.com/spf13/cobra"
-)
-
-var authCmd = &cobra.Command{
-	Use:   "auth",
-	Short: "Commands for authenticating to a Porter server",
-}
-
-var loginCmd = &cobra.Command{
-	Use:   "login",
-	Short: "Authorizes a user for a given Porter server",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := login()
-		if err != nil {
-			color.Red("Error logging in: %s\n", err.Error())
-			os.Exit(1)
-		}
-	},
-}
-
-var registerCmd = &cobra.Command{
-	Use:   "register",
-	Short: "Creates a user for a given Porter server",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := register()
-		if err != nil {
-			color.Red("Error registering: %s\n", err.Error())
-			os.Exit(1)
-		}
-	},
-}
-
-var logoutCmd = &cobra.Command{
-	Use:   "logout",
-	Short: "Logs a user out of a given Porter server",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, logout)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var manual bool = false
-
-func init() {
-	rootCmd.AddCommand(authCmd)
-
-	authCmd.AddCommand(loginCmd)
-	authCmd.AddCommand(registerCmd)
-	authCmd.AddCommand(logoutCmd)
-
-	loginCmd.PersistentFlags().BoolVar(
-		&manual,
-		"manual",
-		false,
-		"whether to prompt for manual authentication (username/pw)",
-	)
-}
-
-func login() error {
-	client := api.NewClientWithToken(cliConf.Host+"/api", cliConf.Token)
-
-	user, err := client.AuthCheck(context.Background())
-
-	if err == nil {
-		// set the token if the user calls login with the --token flag or the PORTER_TOKEN env
-		if cliConf.Token != "" {
-			cliConf.SetToken(cliConf.Token)
-			color.New(color.FgGreen).Println("Successfully logged in!")
-
-			projID, exists, err := api.GetProjectIDFromToken(cliConf.Token)
-			if err != nil {
-				return err
-			}
-
-			// if project ID does not exist for the token, this is a user-issued CLI token, so the project
-			// ID should be queried
-			if !exists {
-				err = setProjectForUser(client, user.ID)
-
-				if err != nil {
-					return err
-				}
-			} else {
-				// if the project ID does exist for the token, this is a project-issued token, and
-				// the project should be set automatically
-				err = cliConf.SetProject(projID)
-
-				if err != nil {
-					return err
-				}
-
-				err = setProjectCluster(client, projID)
-
-				if err != nil {
-					return err
-				}
-			}
-		} else {
-			color.Yellow("You are already logged in. If you'd like to log out, run \"porter auth logout\".")
-		}
-
-		return nil
-	}
-
-	// check for the --manual flag
-	if manual {
-		return loginManual()
-	}
-
-	// log the user in
-	token, err := loginBrowser.Login(cliConf.Host)
-	if err != nil {
-		return err
-	}
-
-	// set the token in config
-	err = cliConf.SetToken(token)
-
-	if err != nil {
-		return err
-	}
-
-	client = api.NewClientWithToken(cliConf.Host+"/api", token)
-
-	user, err = client.AuthCheck(context.Background())
-
-	if err != nil {
-		color.Red("Invalid token.")
-		return err
-	}
-
-	color.New(color.FgGreen).Println("Successfully logged in!")
-
-	return setProjectForUser(client, user.ID)
-}
-
-func setProjectForUser(client *api.Client, userID uint) error {
-	// get a list of projects, and set the current project
-	resp, err := client.ListUserProjects(context.Background())
-	if err != nil {
-		return err
-	}
-
-	projects := *resp
-
-	if len(projects) > 0 {
-		cliConf.SetProject(projects[0].ID)
-
-		err = setProjectCluster(client, projects[0].ID)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func loginManual() error {
-	client := api.NewClient(cliConf.Host+"/api", "cookie.json")
-
-	var username, pw string
-
-	fmt.Println("Please log in with an email and password:")
-
-	username, err := utils.PromptPlaintext("Email: ")
-	if err != nil {
-		return err
-	}
-
-	pw, err = utils.PromptPassword("Password: ")
-
-	if err != nil {
-		return err
-	}
-
-	_, err = client.Login(context.Background(), &types.LoginUserRequest{
-		Email:    username,
-		Password: pw,
-	})
-
-	if err != nil {
-		return err
-	}
-
-	// set the token to empty since this is manual (cookie-based) login
-	cliConf.SetToken("")
-
-	color.New(color.FgGreen).Println("Successfully logged in!")
-
-	// get a list of projects, and set the current project
-	resp, err := client.ListUserProjects(context.Background())
-	if err != nil {
-		return err
-	}
-
-	projects := *resp
-
-	if len(projects) > 0 {
-		cliConf.SetProject(projects[0].ID)
-
-		err = setProjectCluster(client, projects[0].ID)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func register() error {
-	fmt.Println("Please register your admin account with an email and password:")
-
-	username, err := utils.PromptPlaintext("Email: ")
-	if err != nil {
-		return err
-	}
-
-	pw, err := utils.PromptPasswordWithConfirmation()
-	if err != nil {
-		return err
-	}
-
-	client := config.GetAPIClient()
-
-	resp, err := client.CreateUser(context.Background(), &types.CreateUserRequest{
-		Email:    username,
-		Password: pw,
-	})
-	if err != nil {
-		return err
-	}
-
-	color.New(color.FgGreen).Printf("Created user with email %s and id %d\n", username, resp.ID)
-
-	return nil
-}
-
-func logout(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	err := client.Logout(context.Background())
-	if err != nil {
-		return err
-	}
-
-	cliConf.SetToken("")
-
-	color.Green("Successfully logged out")
-
-	return nil
-}

+ 52 - 0
cli/cmd/commands/all.go

@@ -0,0 +1,52 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+)
+
+// RegisterCommands initiates config and sets up all commands.
+// Error returned here is a placeholder as the register commands do not currently return errors, and handle exits themselves. This may change at a later date.
+func RegisterCommands() (*cobra.Command, error) {
+	cliConf, err := config.InitAndLoadConfig()
+	if err != nil {
+		return nil, fmt.Errorf("error loading porter config: %w", err)
+	}
+
+	rootCmd := &cobra.Command{
+		Use:   "porter",
+		Short: "Porter is a dashboard for managing Kubernetes clusters.",
+		Long:  `Porter is a tool for creating, versioning, and updating Kubernetes deployments using a visual dashboard. For more information, visit github.com/porter-dev/porter`,
+	}
+	rootCmd.PersistentFlags().AddFlagSet(utils.DefaultFlagSet)
+
+	rootCmd.AddCommand(registerCommand_App(cliConf))
+	rootCmd.AddCommand(registerCommand_Apply(cliConf))
+	rootCmd.AddCommand(registerCommand_Auth(cliConf))
+	rootCmd.AddCommand(registerCommand_Cluster(cliConf))
+	rootCmd.AddCommand(registerCommand_Config(cliConf))
+	rootCmd.AddCommand(registerCommand_Connect(cliConf))
+	rootCmd.AddCommand(registerCommand_Create(cliConf))
+	rootCmd.AddCommand(registerCommand_Delete(cliConf))
+	rootCmd.AddCommand(registerCommand_Deploy(cliConf))
+	rootCmd.AddCommand(registerCommand_Docker(cliConf))
+	rootCmd.AddCommand(registerCommand_Get(cliConf))
+	rootCmd.AddCommand(registerCommand_Helm(cliConf))
+	rootCmd.AddCommand(registerCommand_Job(cliConf))
+	rootCmd.AddCommand(registerCommand_Kubectl(cliConf))
+	rootCmd.AddCommand(registerCommand_List(cliConf))
+	rootCmd.AddCommand(registerCommand_Logs(cliConf))
+	rootCmd.AddCommand(registerCommand_Open(cliConf))
+	rootCmd.AddCommand(registerCommand_PortForward(cliConf))
+	rootCmd.AddCommand(registerCommand_Project(cliConf))
+	rootCmd.AddCommand(registerCommand_Registry(cliConf))
+	rootCmd.AddCommand(registerCommand_Run(cliConf))
+	rootCmd.AddCommand(registerCommand_Server(cliConf))
+	rootCmd.AddCommand(registerCommand_Stack(cliConf))
+	rootCmd.AddCommand(registerCommand_Update(cliConf))
+	rootCmd.AddCommand(registerCommand_Version(cliConf))
+	return rootCmd, nil
+}

+ 142 - 136
cli/cmd/app.go → cli/cmd/commands/app.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -12,6 +12,7 @@ import (
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1 "k8s.io/api/batch/v1"
@@ -42,55 +43,67 @@ var (
 	appMemoryMi      int
 	appMemoryMi      int
 )
 )
 
 
-// appCmd represents the "porter app" base command when called
-// without any subcommands
-var appCmd = &cobra.Command{
-	Use:   "app",
-	Short: "Runs a command for your application.",
-}
+func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
+	appCmd := &cobra.Command{
+		Use:   "app",
+		Short: "Runs a command for your application.",
+	}
+
+	// appRunCmd represents the "porter app run" subcommand
+	appRunCmd := &cobra.Command{
+		Use:   "run [application] -- COMMAND [args...]",
+		Args:  cobra.MinimumNArgs(2),
+		Short: "Runs a command inside a connected cluster container.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appRun)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	appRunFlags(appRunCmd)
+	appCmd.AddCommand(appRunCmd)
 
 
-// appRunCmd represents the "porter app run" subcommand
-var appRunCmd = &cobra.Command{
-	Use:   "run [application] -- COMMAND [args...]",
-	Args:  cobra.MinimumNArgs(2),
-	Short: "Runs a command inside a connected cluster container.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, appRun)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	// appRunCleanupCmd represents the "porter app run cleanup" subcommand
+	appRunCleanupCmd := &cobra.Command{
+		Use:   "cleanup",
+		Args:  cobra.NoArgs,
+		Short: "Delete any lingering ephemeral pods that were created with \"porter app run\".",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appCleanup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	appRunCmd.AddCommand(appRunCleanupCmd)
 
 
-// appRunCleanupCmd represents the "porter app run cleanup" subcommand
-var appRunCleanupCmd = &cobra.Command{
-	Use:   "cleanup",
-	Args:  cobra.NoArgs,
-	Short: "Delete any lingering ephemeral pods that were created with \"porter app run\".",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, appCleanup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	// appUpdateTagCmd represents the "porter app update-tag" subcommand
+	appUpdateTagCmd := &cobra.Command{
+		Use:   "update-tag [application]",
+		Args:  cobra.MinimumNArgs(1),
+		Short: "Updates the image tag for an application.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appUpdateTag)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-// appUpdateTagCmd represents the "porter app update-tag" subcommand
-var appUpdateTagCmd = &cobra.Command{
-	Use:   "update-tag [application]",
-	Args:  cobra.MinimumNArgs(1),
-	Short: "Updates the image tag for an application.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, appUpdateTag)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	appUpdateTagCmd.PersistentFlags().StringVarP(
+		&appTag,
+		"tag",
+		"t",
+		"",
+		"the specified tag to use, default is \"latest\"",
+	)
+	appCmd.AddCommand(appUpdateTagCmd)
 
 
-func init() {
-	rootCmd.AddCommand(appCmd)
+	return appCmd
+}
 
 
+func appRunFlags(appRunCmd *cobra.Command) {
 	appRunCmd.PersistentFlags().BoolVarP(
 	appRunCmd.PersistentFlags().BoolVarP(
 		&appExistingPod,
 		&appExistingPod,
 		"existing_pod",
 		"existing_pod",
@@ -137,20 +150,9 @@ func init() {
 		"",
 		"",
 		"name of the container inside pod to run the command in",
 		"name of the container inside pod to run the command in",
 	)
 	)
-	appRunCmd.AddCommand(appRunCleanupCmd)
-
-	appUpdateTagCmd.PersistentFlags().StringVarP(
-		&appTag,
-		"tag",
-		"t",
-		"",
-		"the specified tag to use, default is \"latest\"",
-	)
-	appCmd.AddCommand(appRunCmd)
-	appCmd.AddCommand(appUpdateTagCmd)
 }
 }
 
 
-func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
 	execArgs := args[1:]
 	execArgs := args[1:]
 
 
 	color.New(color.FgGreen).Println("Attempting to run", strings.Join(execArgs, " "), "for application", args[0])
 	color.New(color.FgGreen).Println("Attempting to run", strings.Join(execArgs, " "), "for application", args[0])
@@ -158,12 +160,12 @@ func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []st
 	appNamespace = fmt.Sprintf("porter-stack-%s", args[0])
 	appNamespace = fmt.Sprintf("porter-stack-%s", args[0])
 
 
 	if len(execArgs) > 0 {
 	if len(execArgs) > 0 {
-		res, err := client.GetPorterApp(context.Background(), cliConf.Project, cliConf.Cluster, args[0])
+		res, err := client.GetPorterApp(ctx, cliConfig.Project, cliConfig.Cluster, args[0])
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("Unable to run command: %w", err)
 			return fmt.Errorf("Unable to run command: %w", err)
 		}
 		}
 		if res.Name == "" {
 		if res.Name == "" {
-			return fmt.Errorf("An application named \"%s\" was not found in your project (ID: %d). Please check your spelling and try again.", args[0], cliConf.Project)
+			return fmt.Errorf("An application named \"%s\" was not found in your project (ID: %d). Please check your spelling and try again.", args[0], cliConfig.Project)
 		}
 		}
 
 
 		if res.Builder != "" &&
 		if res.Builder != "" &&
@@ -176,7 +178,7 @@ func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []st
 		}
 		}
 	}
 	}
 
 
-	podsSimple, err := appGetPods(client, appNamespace, args[0])
+	podsSimple, err := appGetPods(ctx, cliConfig, client, appNamespace, args[0])
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 	}
 	}
@@ -248,10 +250,11 @@ func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []st
 	}
 	}
 
 
 	config := &AppPorterRunSharedConfig{
 	config := &AppPorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
@@ -261,15 +264,16 @@ func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []st
 		return appExecuteRun(config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
 		return appExecuteRun(config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
 	}
 	}
 
 
-	return appExecuteRunEphemeral(config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
+	return appExecuteRunEphemeral(ctx, config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
 }
 }
 
 
-func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
+func appCleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) error {
 	config := &AppPorterRunSharedConfig{
 	config := &AppPorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err := config.setSharedConfig()
+	err := config.setSharedConfig(ctx)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
@@ -291,20 +295,20 @@ func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []s
 	color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
 	color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
 
 
 	if proceed == "All namespaces" {
 	if proceed == "All namespaces" {
-		namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+		namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		for _, namespace := range namespaces.Items {
 		for _, namespace := range namespaces.Items {
-			if pods, err := appGetEphemeralPods(namespace.Name, config.Clientset); err == nil {
+			if pods, err := appGetEphemeralPods(ctx, namespace.Name, config.Clientset); err == nil {
 				podNames = append(podNames, pods...)
 				podNames = append(podNames, pods...)
 			} else {
 			} else {
 				return err
 				return err
 			}
 			}
 		}
 		}
 	} else {
 	} else {
-		if pods, err := appGetEphemeralPods(appNamespace, config.Clientset); err == nil {
+		if pods, err := appGetEphemeralPods(ctx, appNamespace, config.Clientset); err == nil {
 			podNames = append(podNames, pods...)
 			podNames = append(podNames, pods...)
 		} else {
 		} else {
 			return err
 			return err
@@ -325,7 +329,7 @@ func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []s
 		color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
 		color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
 
 
 		err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
 		err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
-			context.Background(), podName, metav1.DeleteOptions{},
+			ctx, podName, metav1.DeleteOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -335,11 +339,11 @@ func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []s
 	return nil
 	return nil
 }
 }
 
 
-func appGetEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]string, error) {
+func appGetEphemeralPods(ctx context.Context, namespace string, clientset *kubernetes.Clientset) ([]string, error) {
 	var podNames []string
 	var podNames []string
 
 
 	pods, err := clientset.CoreV1().Pods(namespace).List(
 	pods, err := clientset.CoreV1().Pods(namespace).List(
-		context.Background(), metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
+		ctx, metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -353,17 +357,18 @@ func appGetEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]s
 }
 }
 
 
 type AppPorterRunSharedConfig struct {
 type AppPorterRunSharedConfig struct {
-	Client     *api.Client
+	Client     api.Client
 	RestConf   *rest.Config
 	RestConf   *rest.Config
 	Clientset  *kubernetes.Clientset
 	Clientset  *kubernetes.Clientset
 	RestClient *rest.RESTClient
 	RestClient *rest.RESTClient
+	CLIConfig  config.CLIConfig
 }
 }
 
 
-func (p *AppPorterRunSharedConfig) setSharedConfig() error {
-	pID := cliConf.Project
-	cID := cliConf.Cluster
+func (p *AppPorterRunSharedConfig) setSharedConfig(ctx context.Context) error {
+	pID := p.CLIConfig.Project
+	cID := p.CLIConfig.Cluster
 
 
-	kubeResp, err := p.Client.GetKubeconfig(context.Background(), pID, cID, cliConf.Kubeconfig)
+	kubeResp, err := p.Client.GetKubeconfig(ctx, pID, cID, p.CLIConfig.Kubeconfig)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -411,11 +416,11 @@ type appPodSimple struct {
 	ContainerNames []string
 	ContainerNames []string
 }
 }
 
 
-func appGetPods(client *api.Client, namespace, releaseName string) ([]appPodSimple, error) {
-	pID := cliConf.Project
-	cID := cliConf.Cluster
+func appGetPods(ctx context.Context, cliConfig config.CLIConfig, client api.Client, namespace, releaseName string) ([]appPodSimple, error) {
+	pID := cliConfig.Project
+	cID := cliConfig.Cluster
 
 
-	resp, err := client.GetK8sAllPods(context.TODO(), pID, cID, namespace, releaseName)
+	resp, err := client.GetK8sAllPods(ctx, pID, cID, namespace, releaseName)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -482,28 +487,28 @@ func appExecuteRun(config *AppPorterRunSharedConfig, namespace, name, container
 	})
 	})
 }
 }
 
 
-func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, container string, args []string) error {
-	existing, err := appGetExistingPod(config, name, namespace)
+func appExecuteRunEphemeral(ctx context.Context, config *AppPorterRunSharedConfig, namespace, name, container string, args []string) error {
+	existing, err := appGetExistingPod(ctx, config, name, namespace)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	newPod, err := appCreateEphemeralPodFromExisting(config, existing, container, args)
+	newPod, err := appCreateEphemeralPodFromExisting(ctx, config, existing, container, args)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	podName := newPod.ObjectMeta.Name
 	podName := newPod.ObjectMeta.Name
 
 
 	// delete the ephemeral pod no matter what
 	// delete the ephemeral pod no matter what
-	defer appDeletePod(config, podName, namespace)
+	defer appDeletePod(ctx, config, podName, namespace) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
 	color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
-	if err = appWaitForPod(config, newPod); err != nil {
+	if err = appWaitForPod(ctx, config, newPod); err != nil {
 		color.New(color.FgRed).Println("failed")
 		color.New(color.FgRed).Println("failed")
-		return appHandlePodAttachError(err, config, namespace, podName, container)
+		return appHandlePodAttachError(ctx, err, config, namespace, podName, container)
 	}
 	}
 
 
-	err = appCheckForPodDeletionCronJob(config)
+	err = appCheckForPodDeletionCronJob(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -511,7 +516,7 @@ func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, c
 	// refresh pod info for latest status
 	// refresh pod info for latest status
 	newPod, err = config.Clientset.CoreV1().
 	newPod, err = config.Clientset.CoreV1().
 		Pods(newPod.Namespace).
 		Pods(newPod.Namespace).
-		Get(context.Background(), newPod.Name, metav1.GetOptions{})
+		Get(ctx, newPod.Name, metav1.GetOptions{})
 
 
 	// pod exited while we were waiting.  maybe an error maybe not.
 	// pod exited while we were waiting.  maybe an error maybe not.
 	// we dont know if the user wanted an interactive shell or not.
 	// we dont know if the user wanted an interactive shell or not.
@@ -519,11 +524,11 @@ func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, c
 	if appIsPodExited(newPod) {
 	if appIsPodExited(newPod) {
 		color.New(color.FgGreen).Println("complete!")
 		color.New(color.FgGreen).Println("complete!")
 		var writtenBytes int64
 		var writtenBytes int64
-		writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
+		writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 
 		if appVerbose || writtenBytes == 0 {
 		if appVerbose || writtenBytes == 0 {
 			color.New(color.FgYellow).Println("Could not get logs. Pod events:")
 			color.New(color.FgYellow).Println("Could not get logs. Pod events:")
-			appPipeEventsToStdout(config, namespace, podName, container, false)
+			_ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 		}
 		}
 		return nil
 		return nil
 	}
 	}
@@ -564,44 +569,44 @@ func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, c
 		})
 		})
 	}); err != nil {
 	}); err != nil {
 		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
 		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
-		return appHandlePodAttachError(err, config, namespace, podName, container)
+		return appHandlePodAttachError(ctx, err, config, namespace, podName, container)
 	}
 	}
 
 
 	if appVerbose {
 	if appVerbose {
 		color.New(color.FgYellow).Println("Pod events:")
 		color.New(color.FgYellow).Println("Pod events:")
-		appPipeEventsToStdout(config, namespace, podName, container, false)
+		_ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 
 
 	return err
 	return err
 }
 }
 
 
-func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
+func appCheckForPodDeletionCronJob(ctx context.Context, config *AppPorterRunSharedConfig) error {
 	// try and create the cron job and all of the other required resources as necessary,
 	// try and create the cron job and all of the other required resources as necessary,
 	// starting with the service account, then role and then a role binding
 	// starting with the service account, then role and then a role binding
 
 
-	err := appCheckForServiceAccount(config)
+	err := appCheckForServiceAccount(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = appCheckForClusterRole(config)
+	err = appCheckForClusterRole(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = appCheckForRoleBinding(config)
+	err = appCheckForRoleBinding(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+	namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	for _, namespace := range namespaces.Items {
 	for _, namespace := range namespaces.Items {
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -617,7 +622,7 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 			for _, cronJob := range cronJobs.Items {
 			for _, cronJob := range cronJobs.Items {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
-						context.Background(), cronJob.Name, metav1.DeleteOptions{},
+						ctx, cronJob.Name, metav1.DeleteOptions{},
 					)
 					)
 					if err != nil {
 					if err != nil {
 						return err
 						return err
@@ -656,7 +661,7 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
-		context.Background(), cronJob, metav1.CreateOptions{},
+		ctx, cronJob, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -665,15 +670,15 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
-	namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+func appCheckForServiceAccount(ctx context.Context, config *AppPorterRunSharedConfig) error {
+	namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	for _, namespace := range namespaces.Items {
 	for _, namespace := range namespaces.Items {
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -689,7 +694,7 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 			for _, svcAccount := range serviceAccounts.Items {
 			for _, svcAccount := range serviceAccounts.Items {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
-						context.Background(), svcAccount.Name, metav1.DeleteOptions{},
+						ctx, svcAccount.Name, metav1.DeleteOptions{},
 					)
 					)
 					if err != nil {
 					if err != nil {
 						return err
 						return err
@@ -705,7 +710,7 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
-		context.Background(), serviceAccount, metav1.CreateOptions{},
+		ctx, serviceAccount, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -714,9 +719,9 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
+func appCheckForClusterRole(ctx context.Context, config *AppPorterRunSharedConfig) error {
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -746,7 +751,7 @@ func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
-		context.Background(), role, metav1.CreateOptions{},
+		ctx, role, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -755,9 +760,9 @@ func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
+func appCheckForRoleBinding(ctx context.Context, config *AppPorterRunSharedConfig) error {
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -788,7 +793,7 @@ func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
-		context.Background(), binding, metav1.CreateOptions{},
+		ctx, binding, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -797,7 +802,7 @@ func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
+func appWaitForPod(ctx context.Context, config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 	var (
 	var (
 		w   watch.Interface
 		w   watch.Interface
 		err error
 		err error
@@ -810,7 +815,7 @@ func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		w, err = config.Clientset.CoreV1().
 		w, err = config.Clientset.CoreV1().
 			Pods(pod.Namespace).
 			Pods(pod.Namespace).
-			Watch(context.Background(), metav1.ListOptions{FieldSelector: selector})
+			Watch(ctx, metav1.ListOptions{FieldSelector: selector})
 
 
 		if err == nil {
 		if err == nil {
 			break
 			break
@@ -828,7 +833,7 @@ func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 			// creating the listener.
 			// creating the listener.
 			pod, err = config.Clientset.CoreV1().
 			pod, err = config.Clientset.CoreV1().
 				Pods(pod.Namespace).
 				Pods(pod.Namespace).
-				Get(context.Background(), pod.Name, metav1.GetOptions{})
+				Get(ctx, pod.Name, metav1.GetOptions{})
 			if appIsPodReady(pod) || appIsPodExited(pod) {
 			if appIsPodReady(pod) || appIsPodExited(pod) {
 				return nil
 				return nil
 			}
 			}
@@ -861,23 +866,23 @@ func appIsPodExited(pod *v1.Pod) bool {
 	return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
 	return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
 }
 }
 
 
-func appHandlePodAttachError(err error, config *AppPorterRunSharedConfig, namespace, podName, container string) error {
+func appHandlePodAttachError(ctx context.Context, err error, config *AppPorterRunSharedConfig, namespace, podName, container string) error {
 	if appVerbose {
 	if appVerbose {
 		color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
 		color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
 	}
 	}
 	color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
 	color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
 
 
 	var writtenBytes int64
 	var writtenBytes int64
-	writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
+	writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 
 	if appVerbose || writtenBytes == 0 {
 	if appVerbose || writtenBytes == 0 {
 		color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
 		color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
-		appPipeEventsToStdout(config, namespace, podName, container, false)
+		_ = appPipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 	return err
 	return err
 }
 }
 
 
-func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
+func appPipePodLogsToStdout(ctx context.Context, config *AppPorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
 	podLogOpts := v1.PodLogOptions{
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Container: container,
 		Follow:    follow,
 		Follow:    follow,
@@ -886,7 +891,7 @@ func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, c
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 
 	podLogs, err := req.Stream(
 	podLogs, err := req.Stream(
-		context.Background(),
+		ctx,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return 0, err
 		return 0, err
@@ -897,13 +902,13 @@ func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, c
 	return io.Copy(os.Stdout, podLogs)
 	return io.Copy(os.Stdout, podLogs)
 }
 }
 
 
-func appPipeEventsToStdout(config *AppPorterRunSharedConfig, namespace, name, container string, follow bool) error {
+func appPipeEventsToStdout(ctx context.Context, config *AppPorterRunSharedConfig, namespace, name, _ string, _ bool) error {
 	// update the config in case the operation has taken longer than token expiry time
 	// update the config in case the operation has taken longer than token expiry time
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	// creates the clientset
 	// creates the clientset
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
-		context.TODO(),
+		ctx,
 		metav1.ListOptions{
 		metav1.ListOptions{
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 		},
 		},
@@ -919,20 +924,20 @@ func appPipeEventsToStdout(config *AppPorterRunSharedConfig, namespace, name, co
 	return nil
 	return nil
 }
 }
 
 
-func appGetExistingPod(config *AppPorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
+func appGetExistingPod(ctx context.Context, config *AppPorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
 	return config.Clientset.CoreV1().Pods(namespace).Get(
 	return config.Clientset.CoreV1().Pods(namespace).Get(
-		context.Background(),
+		ctx,
 		name,
 		name,
 		metav1.GetOptions{},
 		metav1.GetOptions{},
 	)
 	)
 }
 }
 
 
-func appDeletePod(config *AppPorterRunSharedConfig, name, namespace string) error {
+func appDeletePod(ctx context.Context, config *AppPorterRunSharedConfig, name, namespace string) error {
 	// update the config in case the operation has taken longer than token expiry time
 	// update the config in case the operation has taken longer than token expiry time
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	err := config.Clientset.CoreV1().Pods(namespace).Delete(
 	err := config.Clientset.CoreV1().Pods(namespace).Delete(
-		context.Background(),
+		ctx,
 		name,
 		name,
 		metav1.DeleteOptions{},
 		metav1.DeleteOptions{},
 	)
 	)
@@ -947,6 +952,7 @@ func appDeletePod(config *AppPorterRunSharedConfig, name, namespace string) erro
 }
 }
 
 
 func appCreateEphemeralPodFromExisting(
 func appCreateEphemeralPodFromExisting(
+	ctx context.Context,
 	config *AppPorterRunSharedConfig,
 	config *AppPorterRunSharedConfig,
 	existing *v1.Pod,
 	existing *v1.Pod,
 	container string,
 	container string,
@@ -1032,18 +1038,18 @@ func appCreateEphemeralPodFromExisting(
 
 
 	// create the pod and return it
 	// create the pod and return it
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
-		context.Background(),
+		ctx,
 		newPod,
 		newPod,
 		metav1.CreateOptions{},
 		metav1.CreateOptions{},
 	)
 	)
 }
 }
 
 
-func appUpdateTag(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func appUpdateTag(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
 	namespace := fmt.Sprintf("porter-stack-%s", args[0])
 	namespace := fmt.Sprintf("porter-stack-%s", args[0])
 	if appTag == "" {
 	if appTag == "" {
 		appTag = "latest"
 		appTag = "latest"
 	}
 	}
-	release, err := client.GetRelease(context.TODO(), cliConf.Project, cliConf.Cluster, namespace, args[0])
+	release, err := client.GetRelease(ctx, cliConfig.Project, cliConfig.Cluster, namespace, args[0])
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Unable to find application %s", args[0])
 		return fmt.Errorf("Unable to find application %s", args[0])
 	}
 	}
@@ -1056,8 +1062,8 @@ func appUpdateTag(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 		Tag:        appTag,
 		Tag:        appTag,
 	}
 	}
 	createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
 	createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
-		ClusterID:       cliConf.Cluster,
-		ProjectID:       cliConf.Project,
+		ClusterID:       cliConfig.Cluster,
+		ProjectID:       cliConfig.Project,
 		ImageInfo:       imageInfo,
 		ImageInfo:       imageInfo,
 		OverrideRelease: false,
 		OverrideRelease: false,
 	}
 	}
@@ -1065,9 +1071,9 @@ func appUpdateTag(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 	color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
 	color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
 
 
 	_, err = client.CreatePorterApp(
 	_, err = client.CreatePorterApp(
-		context.Background(),
-		cliConf.Project,
-		cliConf.Cluster,
+		ctx,
+		cliConfig.Project,
+		cliConfig.Cluster,
 		args[0],
 		args[0],
 		createUpdatePorterAppRequest,
 		createUpdatePorterAppRequest,
 	)
 	)

+ 186 - 140
cli/cmd/apply.go → cli/cmd/commands/apply.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -13,6 +13,8 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/cli/cli/git"
 	"github.com/cli/cli/git"
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	"github.com/mitchellh/mapstructure"
 	"github.com/mitchellh/mapstructure"
@@ -21,9 +23,10 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy/wait"
 	"github.com/porter-dev/porter/cli/cmd/deploy/wait"
+	porter_app "github.com/porter-dev/porter/cli/cmd/porter_app"
 	"github.com/porter-dev/porter/cli/cmd/preview"
 	"github.com/porter-dev/porter/cli/cmd/preview"
 	previewV2Beta1 "github.com/porter-dev/porter/cli/cmd/preview/v2beta1"
 	previewV2Beta1 "github.com/porter-dev/porter/cli/cmd/preview/v2beta1"
-	stack "github.com/porter-dev/porter/cli/cmd/stack"
+	cliUtils "github.com/porter-dev/porter/cli/cmd/utils"
 	previewInt "github.com/porter-dev/porter/internal/integrations/preview"
 	previewInt "github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/drivers"
@@ -36,12 +39,13 @@ import (
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 )
 )
 
 
-// applyCmd represents the "porter apply" base command when called
-// with a porter.yaml file as an argument
-var applyCmd = &cobra.Command{
-	Use:   "apply",
-	Short: "Applies a configuration to an application",
-	Long: fmt.Sprintf(`
+var porterYAML string
+
+func registerCommand_Apply(cliConf config.CLIConfig) *cobra.Command {
+	applyCmd := &cobra.Command{
+		Use:   "apply",
+		Short: "Applies a configuration to an application",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Applies a configuration to an application by either creating a new one or updating an existing
 Applies a configuration to an application by either creating a new one or updating an existing
@@ -65,51 +69,60 @@ applying a configuration:
   PORTER_SOURCE_VERSION       The version of the Helm chart to use
   PORTER_SOURCE_VERSION       The version of the Helm chart to use
   PORTER_TAG                  The Docker image tag to use (like the git commit hash)
   PORTER_TAG                  The Docker image tag to use (like the git commit hash)
 	`,
 	`,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, apply)
-		if err != nil {
-			if strings.Contains(err.Error(), "Forbidden") {
-				color.New(color.FgRed).Fprintf(os.Stderr, "You may have to update your GitHub secret token")
-			}
-
-			os.Exit(1)
-		}
-	},
-}
-
-// applyValidateCmd represents the "porter apply validate" command when called
-// with a porter.yaml file as an argument
-var applyValidateCmd = &cobra.Command{
-	Use:   "validate",
-	Short: "Validates a porter.yaml",
-	Run: func(*cobra.Command, []string) {
-		err := applyValidate()
-
-		if err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "Error: %s\n", err.Error())
-			os.Exit(1)
-		} else {
-			color.New(color.FgGreen).Printf("The porter.yaml file is valid!\n")
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, apply)
+			if err != nil {
+				if strings.Contains(err.Error(), "Forbidden") {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "You may have to update your GitHub secret token")
+				}
 
 
-var porterYAML string
+				os.Exit(1)
+			}
+		},
+	}
+	// applyValidateCmd represents the "porter apply validate" command when called
+	// with a porter.yaml file as an argument
+	applyValidateCmd := &cobra.Command{
+		Use:   "validate",
+		Short: "Validates a porter.yaml",
+		Run: func(*cobra.Command, []string) {
+			err := applyValidate()
 
 
-func init() {
-	rootCmd.AddCommand(applyCmd)
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "Error: %s\n", err.Error())
+				os.Exit(1)
+			} else {
+				_, _ = color.New(color.FgGreen).Printf("The porter.yaml file is valid!\n")
+			}
+		},
+	}
 
 
 	applyCmd.AddCommand(applyValidateCmd)
 	applyCmd.AddCommand(applyValidateCmd)
 
 
 	applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
 	applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
 	applyCmd.MarkFlagRequired("file")
 	applyCmd.MarkFlagRequired("file")
+
+	return applyCmd
 }
 }
 
 
-func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) (err error) {
-	fileBytes, err := ioutil.ReadFile(porterYAML)
+func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) (err error) {
+	project, err := client.GetProject(ctx, cliConfig.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.Apply(ctx, cliConfig, client, porterYAML)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	fileBytes, err := os.ReadFile(porterYAML) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	if err != nil {
 	if err != nil {
 		stackName := os.Getenv("PORTER_STACK_NAME")
 		stackName := os.Getenv("PORTER_STACK_NAME")
 		if stackName == "" {
 		if stackName == "" {
@@ -133,7 +146,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 	if previewVersion.Version == "v2beta1" {
 	if previewVersion.Version == "v2beta1" {
 		ns := os.Getenv("PORTER_NAMESPACE")
 		ns := os.Getenv("PORTER_NAMESPACE")
 
 
-		applier, err := previewV2Beta1.NewApplier(client, fileBytes, ns)
+		applier, err := previewV2Beta1.NewApplier(client, cliConfig, fileBytes, ns)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -158,7 +171,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		}
 		}
 	} else if previewVersion.Version == "v1stack" || previewVersion.Version == "" {
 	} else if previewVersion.Version == "v1stack" || previewVersion.Version == "" {
 
 
-		parsed, err := stack.ValidateAndMarshal(fileBytes)
+		parsed, err := porter_app.ValidateAndMarshal(fileBytes)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("error parsing porter.yaml: %w", err)
 			return fmt.Errorf("error parsing porter.yaml: %w", err)
 		}
 		}
@@ -175,7 +188,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 
 
 		if parsed.Applications != nil {
 		if parsed.Applications != nil {
 			for appName, app := range parsed.Applications {
 			for appName, app := range parsed.Applications {
-				resources, err := stack.CreateApplicationDeploy(client, worker, app, appName, cliConf)
+				resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, appName, cliConfig)
 				if err != nil {
 				if err != nil {
 					return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 					return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 				}
 				}
@@ -192,7 +205,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 				return fmt.Errorf("'apps' and 'services' are synonymous but both were defined")
 				return fmt.Errorf("'apps' and 'services' are synonymous but both were defined")
 			}
 			}
 
 
-			var services map[string]*stack.Service
+			var services map[string]*porter_app.Service
 			if parsed.Apps != nil {
 			if parsed.Apps != nil {
 				services = parsed.Apps
 				services = parsed.Apps
 			}
 			}
@@ -201,7 +214,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 				services = parsed.Services
 				services = parsed.Services
 			}
 			}
 
 
-			app := &stack.Application{
+			app := &porter_app.Application{
 				Env:      parsed.Env,
 				Env:      parsed.Env,
 				Services: services,
 				Services: services,
 				Build:    parsed.Build,
 				Build:    parsed.Build,
@@ -212,7 +225,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 			}
 			}
 
 
-			resources, err := stack.CreateApplicationDeploy(client, worker, app, appName, cliConf)
+			resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, appName, cliConfig)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 			}
 			}
@@ -233,12 +246,12 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		name     string
 		name     string
 		funcName func(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error)
 		funcName func(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error)
 	}{
 	}{
-		{"deploy", NewDeployDriver},
-		{"build-image", preview.NewBuildDriver},
-		{"push-image", preview.NewPushDriver},
-		{"update-config", preview.NewUpdateConfigDriver},
+		{"deploy", NewDeployDriver(ctx, client, cliConfig)},
+		{"build-image", preview.NewBuildDriver(ctx, client, cliConfig)},
+		{"push-image", preview.NewPushDriver(ctx, client, cliConfig)},
+		{"update-config", preview.NewUpdateConfigDriver(ctx, client, cliConfig)},
 		{"random-string", preview.NewRandomStringDriver},
 		{"random-string", preview.NewRandomStringDriver},
-		{"env-group", preview.NewEnvGroupDriver},
+		{"env-group", preview.NewEnvGroupDriver(ctx, client, cliConfig)},
 		{"os-env", preview.NewOSEnvDriver},
 		{"os-env", preview.NewOSEnvDriver},
 	}
 	}
 	for _, driver := range drivers {
 	for _, driver := range drivers {
@@ -259,7 +272,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 			return
 			return
 		}
 		}
 
 
-		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
+		deploymentHook, err := NewDeploymentHook(cliConfig, client, resGroup, deplNamespace)
 		if err != nil {
 		if err != nil {
 			err = fmt.Errorf("error creating deployment hook: %w", err)
 			err = fmt.Errorf("error creating deployment hook: %w", err)
 			return err
 			return err
@@ -279,7 +292,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		return err
 		return err
 	}
 	}
 
 
-	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
+	cloneEnvGroupHook := NewCloneEnvGroupHook(client, cliConfig, resGroup)
 	err = worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
 	err = worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
 	if err != nil {
 	if err != nil {
 		err = fmt.Errorf("error registering clone env group hook: %w", err)
 		err = fmt.Errorf("error registering clone env group hook: %w", err)
@@ -349,47 +362,55 @@ func hasDeploymentHookEnvVars() bool {
 	return true
 	return true
 }
 }
 
 
+// DeployDriver contains all information needed for deploying with switchboard
 type DeployDriver struct {
 type DeployDriver struct {
 	source      *previewInt.Source
 	source      *previewInt.Source
 	target      *previewInt.Target
 	target      *previewInt.Target
 	output      map[string]interface{}
 	output      map[string]interface{}
 	lookupTable *map[string]drivers.Driver
 	lookupTable *map[string]drivers.Driver
 	logger      *zerolog.Logger
 	logger      *zerolog.Logger
+	cliConfig   config.CLIConfig
+	apiClient   api.Client
 }
 }
 
 
-func NewDeployDriver(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
-	driver := &DeployDriver{
-		lookupTable: opts.DriverLookupTable,
-		logger:      opts.Logger,
-		output:      make(map[string]interface{}),
-	}
+// NewDeployDriver creates a deployment driver for use with switchboard
+func NewDeployDriver(ctx context.Context, apiClient api.Client, cliConfig config.CLIConfig) func(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	return func(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+		driver := &DeployDriver{
+			lookupTable: opts.DriverLookupTable,
+			logger:      opts.Logger,
+			output:      make(map[string]interface{}),
+			cliConfig:   cliConfig,
+			apiClient:   apiClient,
+		}
 
 
-	target, err := preview.GetTarget(resource.Name, resource.Target)
-	if err != nil {
-		return nil, err
-	}
+		target, err := preview.GetTarget(ctx, resource.Name, resource.Target, apiClient, cliConfig)
+		if err != nil {
+			return nil, err
+		}
 
 
-	driver.target = target
+		driver.target = target
 
 
-	source, err := preview.GetSource(target.Project, resource.Name, resource.Source)
-	if err != nil {
-		return nil, err
-	}
+		source, err := preview.GetSource(ctx, target.Project, resource.Name, resource.Source, apiClient)
+		if err != nil {
+			return nil, err
+		}
 
 
-	driver.source = source
+		driver.source = source
 
 
-	return driver, nil
+		return driver, nil
+	}
 }
 }
 
 
+// ShouldApply extends switchboard
 func (d *DeployDriver) ShouldApply(_ *switchboardModels.Resource) bool {
 func (d *DeployDriver) ShouldApply(_ *switchboardModels.Resource) bool {
 	return true
 	return true
 }
 }
 
 
+// Apply extends switchboard
 func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboardModels.Resource, error) {
 func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboardModels.Resource, error) {
-	ctx := context.Background()
-	client := config.GetAPIClient()
-
-	_, err := client.GetRelease(
+	ctx := context.TODO() // blocked from switchboard for now
+	_, err := d.apiClient.GetRelease(
 		ctx,
 		ctx,
 		d.target.Project,
 		d.target.Project,
 		d.target.Cluster,
 		d.target.Cluster,
@@ -404,14 +425,14 @@ func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboard
 	}
 	}
 
 
 	if d.source.IsApplication {
 	if d.source.IsApplication {
-		return d.applyApplication(ctx, resource, client, shouldCreate)
+		return d.applyApplication(ctx, resource, d.apiClient, shouldCreate)
 	}
 	}
 
 
-	return d.applyAddon(resource, client, shouldCreate)
+	return d.applyAddon(ctx, resource, d.apiClient, shouldCreate)
 }
 }
 
 
 // Simple apply for addons
 // Simple apply for addons
-func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
+func (d *DeployDriver) applyAddon(ctx context.Context, resource *switchboardModels.Resource, client api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
 	addonConfig, err := d.getAddonConfig(resource)
 	addonConfig, err := d.getAddonConfig(resource)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
 		return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
@@ -419,7 +440,7 @@ func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *
 
 
 	if shouldCreate {
 	if shouldCreate {
 		err := client.DeployAddon(
 		err := client.DeployAddon(
-			context.Background(),
+			ctx,
 			d.target.Project,
 			d.target.Project,
 			d.target.Cluster,
 			d.target.Cluster,
 			d.target.Namespace,
 			d.target.Namespace,
@@ -443,7 +464,7 @@ func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *
 		}
 		}
 
 
 		err = client.UpgradeRelease(
 		err = client.UpgradeRelease(
-			context.Background(),
+			ctx,
 			d.target.Project,
 			d.target.Project,
 			d.target.Cluster,
 			d.target.Cluster,
 			d.target.Namespace,
 			d.target.Namespace,
@@ -458,14 +479,14 @@ func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *
 		}
 		}
 	}
 	}
 
 
-	if err = d.assignOutput(resource, client); err != nil {
+	if err = d.assignOutput(ctx, resource, client); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	return resource, nil
 	return resource, nil
 }
 }
 
 
-func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboardModels.Resource, client *api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
+func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboardModels.Resource, client api.Client, shouldCreate bool) (*switchboardModels.Resource, error) {
 	if resource == nil {
 	if resource == nil {
 		return nil, fmt.Errorf("nil resource")
 		return nil, fmt.Errorf("nil resource")
 	}
 	}
@@ -526,20 +547,20 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 
 
 	if appConfig.Build.UseCache {
 	if appConfig.Build.UseCache {
 		// set the docker config so that pack caching can use the repo credentials
 		// set the docker config so that pack caching can use the repo credentials
-		err := config.SetDockerConfig(client)
+		err := config.SetDockerConfig(ctx, client, d.target.Project)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}
 
 
 	if shouldCreate {
 	if shouldCreate {
-		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
+		resource, err = d.createApplication(ctx, resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
 			return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
 			return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
 		}
 		}
 	} else if !appConfig.OnlyCreate {
 	} else if !appConfig.OnlyCreate {
-		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
+		resource, err = d.updateApplication(ctx, resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
 			return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
 			return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
@@ -548,7 +569,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resourceName)
 		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resourceName)
 	}
 	}
 
 
-	if err = d.assignOutput(resource, client); err != nil {
+	if err = d.assignOutput(ctx, resource, client); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -574,7 +595,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 			predeployEventResponseID = eventResponse.ID
 			predeployEventResponseID = eventResponse.ID
 		}
 		}
 
 
-		err = wait.WaitForJob(client, &wait.WaitOpts{
+		err = wait.WaitForJob(ctx, client, &wait.WaitOpts{
 			ProjectID: d.target.Project,
 			ProjectID: d.target.Project,
 			ClusterID: d.target.Cluster,
 			ClusterID: d.target.Cluster,
 			Namespace: d.target.Namespace,
 			Namespace: d.target.Namespace,
@@ -602,7 +623,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 
 
 			if appConfig.OnlyCreate {
 			if appConfig.OnlyCreate {
 				deleteJobErr := client.DeleteRelease(
 				deleteJobErr := client.DeleteRelease(
-					context.Background(),
+					ctx,
 					d.target.Project,
 					d.target.Project,
 					d.target.Cluster,
 					d.target.Cluster,
 					d.target.Namespace,
 					d.target.Namespace,
@@ -641,7 +662,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 	return resource, err
 	return resource, err
 }
 }
 
 
-func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
+func (d *DeployDriver) createApplication(ctx context.Context, resource *switchboardModels.Resource, client api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
 	// create new release
 	// create new release
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 
 
@@ -652,7 +673,7 @@ func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, c
 
 
 	if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
 	if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
 		if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
 		if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
-			repoSuffix = strings.ToLower(strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-"))
+			repoSuffix = cliUtils.SlugifyRepoSuffix(repoOwner, repoName)
 		}
 		}
 	}
 	}
 
 
@@ -680,17 +701,17 @@ func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, c
 	var err error
 	var err error
 
 
 	if appConf.Build.Method == "registry" {
 	if appConf.Build.Method == "registry" {
-		subdomain, err = createAgent.CreateFromRegistry(appConf.Build.Image, appConf.Values)
+		subdomain, err = createAgent.CreateFromRegistry(ctx, appConf.Build.Image, appConf.Values)
 	} else {
 	} else {
 		// if useCache is set, create the image repository first
 		// if useCache is set, create the image repository first
 		if appConf.Build.UseCache {
 		if appConf.Build.UseCache {
-			regID, imageURL, err := createAgent.GetImageRepoURL(resource.Name, sharedOpts.Namespace)
+			regID, imageURL, err := createAgent.GetImageRepoURL(ctx, resource.Name, sharedOpts.Namespace)
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
 
 
 			err = client.CreateRepository(
 			err = client.CreateRepository(
-				context.Background(),
+				ctx,
 				sharedOpts.ProjectID,
 				sharedOpts.ProjectID,
 				regID,
 				regID,
 				&types.CreateRegistryRepositoryRequest{
 				&types.CreateRegistryRepositoryRequest{
@@ -703,7 +724,7 @@ func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, c
 			}
 			}
 		}
 		}
 
 
-		subdomain, err = createAgent.CreateFromDocker(appConf.Values, sharedOpts.OverrideTag, buildConfig)
+		subdomain, err = createAgent.CreateFromDocker(ctx, appConf.Values, sharedOpts.OverrideTag, buildConfig)
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
@@ -713,14 +734,14 @@ func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, c
 	return resource, handleSubdomainCreate(subdomain, err)
 	return resource, handleSubdomainCreate(subdomain, err)
 }
 }
 
 
-func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
+func (d *DeployDriver) updateApplication(ctx context.Context, resource *switchboardModels.Resource, client api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*switchboardModels.Resource, error) {
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 
 
 	if len(appConf.Build.Env) > 0 {
 	if len(appConf.Build.Env) > 0 {
 		sharedOpts.AdditionalEnv = appConf.Build.Env
 		sharedOpts.AdditionalEnv = appConf.Build.Env
 	}
 	}
 
 
-	updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
+	updateAgent, err := deploy.NewDeployAgent(ctx, client, resource.Name, &deploy.DeployOpts{
 		SharedOpts: sharedOpts,
 		SharedOpts: sharedOpts,
 		Local:      appConf.Build.Method != "registry",
 		Local:      appConf.Build.Method != "registry",
 	})
 	})
@@ -730,7 +751,7 @@ func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, c
 
 
 	// if the build method is registry, we do not trigger a build
 	// if the build method is registry, we do not trigger a build
 	if appConf.Build.Method != "registry" {
 	if appConf.Build.Method != "registry" {
-		buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+		buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 			UseNewConfig: true,
 			UseNewConfig: true,
 			NewConfig:    appConf.Values,
 			NewConfig:    appConf.Values,
 		})
 		})
@@ -753,14 +774,14 @@ func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, c
 			}
 			}
 		}
 		}
 
 
-		err = updateAgent.Build(buildConfig)
+		err = updateAgent.Build(ctx, buildConfig)
 
 
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
 		if !appConf.Build.UseCache {
 		if !appConf.Build.UseCache {
-			err = updateAgent.Push()
+			err = updateAgent.Push(ctx)
 
 
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
@@ -784,7 +805,7 @@ func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, c
 		}
 		}
 	}
 	}
 
 
-	err = updateAgent.UpdateImageAndValues(appConf.Values)
+	err = updateAgent.UpdateImageAndValues(ctx, appConf.Values)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -792,9 +813,9 @@ func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, c
 	return resource, nil
 	return resource, nil
 }
 }
 
 
-func (d *DeployDriver) assignOutput(resource *switchboardModels.Resource, client *api.Client) error {
+func (d *DeployDriver) assignOutput(ctx context.Context, resource *switchboardModels.Resource, client api.Client) error {
 	release, err := client.GetRelease(
 	release, err := client.GetRelease(
-		context.Background(),
+		ctx,
 		d.target.Project,
 		d.target.Project,
 		d.target.Cluster,
 		d.target.Cluster,
 		d.target.Namespace,
 		d.target.Namespace,
@@ -809,6 +830,7 @@ func (d *DeployDriver) assignOutput(resource *switchboardModels.Resource, client
 	return nil
 	return nil
 }
 }
 
 
+// Output extends switchboard
 func (d *DeployDriver) Output() (map[string]interface{}, error) {
 func (d *DeployDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 	return d.output, nil
 }
 }
@@ -847,18 +869,22 @@ func (d *DeployDriver) getAddonConfig(resource *switchboardModels.Resource) (map
 	})
 	})
 }
 }
 
 
+// DeploymentHook contains all information needed for deploying with switchboard
 type DeploymentHook struct {
 type DeploymentHook struct {
-	client                                                                    *api.Client
+	client                                                                    api.Client
 	resourceGroup                                                             *switchboardTypes.ResourceGroup
 	resourceGroup                                                             *switchboardTypes.ResourceGroup
 	gitInstallationID, projectID, clusterID, prID, actionID, envID            uint
 	gitInstallationID, projectID, clusterID, prID, actionID, envID            uint
 	branchFrom, branchInto, namespace, repoName, repoOwner, prName, commitSHA string
 	branchFrom, branchInto, namespace, repoName, repoOwner, prName, commitSHA string
+	cliConfig                                                                 config.CLIConfig
 }
 }
 
 
-func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
+// NewDeploymentHook creates a new deployment using switchboard
+func NewDeploymentHook(cliConfig config.CLIConfig, client api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
 	res := &DeploymentHook{
 	res := &DeploymentHook{
 		client:        client,
 		client:        client,
 		resourceGroup: resourceGroup,
 		resourceGroup: resourceGroup,
 		namespace:     namespace,
 		namespace:     namespace,
+		cliConfig:     cliConfig,
 	}
 	}
 
 
 	ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
 	ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
@@ -877,13 +903,13 @@ func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.Resou
 
 
 	res.prID = uint(prID)
 	res.prID = uint(prID)
 
 
-	res.projectID = cliConf.Project
+	res.projectID = cliConfig.Project
 
 
 	if res.projectID == 0 {
 	if res.projectID == 0 {
 		return nil, fmt.Errorf("project id must be set")
 		return nil, fmt.Errorf("project id must be set")
 	}
 	}
 
 
-	res.clusterID = cliConf.Cluster
+	res.clusterID = cliConfig.Cluster
 
 
 	if res.clusterID == 0 {
 	if res.clusterID == 0 {
 		return nil, fmt.Errorf("cluster id must be set")
 		return nil, fmt.Errorf("cluster id must be set")
@@ -926,13 +952,16 @@ func (t *DeploymentHook) isBranchDeploy() bool {
 	return t.branchFrom != "" && t.branchInto != "" && t.branchFrom == t.branchInto
 	return t.branchFrom != "" && t.branchInto != "" && t.branchFrom == t.branchInto
 }
 }
 
 
+// PreApply extends switchboard
 func (t *DeploymentHook) PreApply() error {
 func (t *DeploymentHook) PreApply() error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	if isSystemNamespace(t.namespace) {
 	if isSystemNamespace(t.namespace) {
 		color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
 		color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
 	}
 	}
 
 
 	envList, err := t.client.ListEnvironments(
 	envList, err := t.client.ListEnvironments(
-		context.Background(), t.projectID, t.clusterID,
+		ctx, t.projectID, t.clusterID,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -956,7 +985,7 @@ func (t *DeploymentHook) PreApply() error {
 	}
 	}
 
 
 	nsList, err := t.client.GetK8sNamespaces(
 	nsList, err := t.client.GetK8sNamespaces(
-		context.Background(), t.projectID, t.clusterID,
+		ctx, t.projectID, t.clusterID,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error fetching namespaces: %w", err)
 		return fmt.Errorf("error fetching namespaces: %w", err)
@@ -986,7 +1015,7 @@ func (t *DeploymentHook) PreApply() error {
 		}
 		}
 
 
 		// create the new namespace
 		// create the new namespace
-		_, err := t.client.CreateNewK8sNamespace(context.Background(), t.projectID, t.clusterID, createNS)
+		_, err := t.client.CreateNewK8sNamespace(ctx, t.projectID, t.clusterID, createNS)
 
 
 		if err != nil && !strings.Contains(err.Error(), "namespace already exists") {
 		if err != nil && !strings.Contains(err.Error(), "namespace already exists") {
 			// ignore the error if the namespace already exists
 			// ignore the error if the namespace already exists
@@ -1000,7 +1029,7 @@ func (t *DeploymentHook) PreApply() error {
 
 
 	if t.isBranchDeploy() {
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
 				Branch: t.branchFrom,
@@ -1008,7 +1037,7 @@ func (t *DeploymentHook) PreApply() error {
 		)
 		)
 	} else {
 	} else {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
 				PRNumber: t.prID,
@@ -1039,7 +1068,7 @@ func (t *DeploymentHook) PreApply() error {
 		}
 		}
 
 
 		_, err = t.client.CreateDeployment(
 		_, err = t.client.CreateDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, createReq,
 			t.projectID, t.clusterID, createReq,
 		)
 		)
 	} else if err == nil {
 	} else if err == nil {
@@ -1059,12 +1088,13 @@ func (t *DeploymentHook) PreApply() error {
 			updateReq.PRNumber = 0
 			updateReq.PRNumber = 0
 		}
 		}
 
 
-		_, err = t.client.UpdateDeployment(context.Background(), t.projectID, t.clusterID, updateReq)
+		_, err = t.client.UpdateDeployment(ctx, t.projectID, t.clusterID, updateReq)
 	}
 	}
 
 
 	return err
 	return err
 }
 }
 
 
+// DataQueries extends switchboard
 func (t *DeploymentHook) DataQueries() map[string]interface{} {
 func (t *DeploymentHook) DataQueries() map[string]interface{} {
 	res := make(map[string]interface{})
 	res := make(map[string]interface{})
 
 
@@ -1125,7 +1155,10 @@ func (t *DeploymentHook) DataQueries() map[string]interface{} {
 	return res
 	return res
 }
 }
 
 
+// PostApply extends switchboard
 func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	subdomains := make([]string, 0)
 	subdomains := make([]string, 0)
 
 
 	for _, data := range populatedData {
 	for _, data := range populatedData {
@@ -1153,8 +1186,8 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 	}
 
 
 	for _, res := range t.resourceGroup.Resources {
 	for _, res := range t.resourceGroup.Resources {
-		releaseType := getReleaseType(t.projectID, res)
-		releaseName := getReleaseName(res)
+		releaseType := getReleaseType(ctx, t.projectID, res, t.client)
+		releaseName := getReleaseName(ctx, res, t.client, t.cliConfig)
 
 
 		if releaseType != "" && releaseName != "" {
 		if releaseType != "" && releaseName != "" {
 			req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
 			req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
@@ -1165,17 +1198,20 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 	}
 
 
 	// finalize the deployment
 	// finalize the deployment
-	_, err := t.client.FinalizeDeployment(context.Background(), t.projectID, t.clusterID, req)
+	_, err := t.client.FinalizeDeployment(ctx, t.projectID, t.clusterID, req)
 
 
 	return err
 	return err
 }
 }
 
 
+// OnError extends switchboard
 func (t *DeploymentHook) OnError(error) {
 func (t *DeploymentHook) OnError(error) {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	var deplErr error
 	var deplErr error
 
 
 	if t.isBranchDeploy() {
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
 				Branch: t.branchFrom,
@@ -1183,7 +1219,7 @@ func (t *DeploymentHook) OnError(error) {
 		)
 		)
 	} else {
 	} else {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
 				PRNumber: t.prID,
@@ -1210,16 +1246,19 @@ func (t *DeploymentHook) OnError(error) {
 		}
 		}
 
 
 		// FIXME: try to use the error with a custom logger
 		// FIXME: try to use the error with a custom logger
-		t.client.UpdateDeploymentStatus(context.Background(), t.projectID, t.clusterID, req)
+		t.client.UpdateDeploymentStatus(ctx, t.projectID, t.clusterID, req) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 }
 }
 
 
+// OnConsolidatedErrors extends switchboard
 func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	var deplErr error
 	var deplErr error
 
 
 	if t.isBranchDeploy() {
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
 				Branch: t.branchFrom,
@@ -1227,7 +1266,7 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		)
 		)
 	} else {
 	} else {
 		_, deplErr = t.client.GetDeployment(
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
 				PRNumber: t.prID,
@@ -1252,8 +1291,8 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		for _, res := range t.resourceGroup.Resources {
 		for _, res := range t.resourceGroup.Resources {
 			if _, ok := allErrors[res.Name]; !ok {
 			if _, ok := allErrors[res.Name]; !ok {
 				req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
 				req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
-					ReleaseName: getReleaseName(res),
-					ReleaseType: getReleaseType(t.projectID, res),
+					ReleaseName: getReleaseName(ctx, res, t.client, t.cliConfig),
+					ReleaseType: getReleaseType(ctx, t.projectID, res, t.client),
 				})
 				})
 			}
 			}
 		}
 		}
@@ -1263,23 +1302,29 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		}
 		}
 
 
 		// FIXME: handle the error
 		// FIXME: handle the error
-		t.client.FinalizeDeploymentWithErrors(context.Background(), t.projectID, t.clusterID, req)
+		t.client.FinalizeDeploymentWithErrors(ctx, t.projectID, t.clusterID, req) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 }
 }
 
 
+// CloneEnvGroupHook contains all information needed to clone an env group
 type CloneEnvGroupHook struct {
 type CloneEnvGroupHook struct {
-	client   *api.Client
-	resGroup *switchboardTypes.ResourceGroup
+	client    api.Client
+	resGroup  *switchboardTypes.ResourceGroup
+	cliConfig config.CLIConfig
 }
 }
 
 
-func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
+// NewCloneEnvGroupHook wraps switchboard for cloning env groups
+func NewCloneEnvGroupHook(client api.Client, cliConfig config.CLIConfig, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
 	return &CloneEnvGroupHook{
 	return &CloneEnvGroupHook{
-		client:   client,
-		resGroup: resourceGroup,
+		client:    client,
+		cliConfig: cliConfig,
+		resGroup:  resourceGroup,
 	}
 	}
 }
 }
 
 
 func (t *CloneEnvGroupHook) PreApply() error {
 func (t *CloneEnvGroupHook) PreApply() error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	for _, res := range t.resGroup.Resources {
 	for _, res := range t.resGroup.Resources {
 		if res.Driver == "env-group" {
 		if res.Driver == "env-group" {
 			continue
 			continue
@@ -1293,7 +1338,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 		}
 		}
 
 
 		if appConf != nil && len(appConf.EnvGroups) > 0 {
 		if appConf != nil && len(appConf.EnvGroups) > 0 {
-			target, err := preview.GetTarget(res.Name, res.Target)
+			target, err := preview.GetTarget(ctx, res.Name, res.Target, t.client, t.cliConfig)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -1304,7 +1349,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 				}
 				}
 
 
 				_, err := t.client.GetEnvGroup(
 				_, err := t.client.GetEnvGroup(
-					context.Background(),
+					ctx,
 					target.Project,
 					target.Project,
 					target.Cluster,
 					target.Cluster,
 					target.Namespace,
 					target.Namespace,
@@ -1326,7 +1371,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 							group.Name, group.Namespace, target.Namespace)
 							group.Name, group.Namespace, target.Namespace)
 
 
 					_, err = t.client.CloneEnvGroup(
 					_, err = t.client.CloneEnvGroup(
-						context.Background(), target.Project, target.Cluster, group.Namespace,
+						ctx, target.Project, target.Cluster, group.Namespace,
 						&types.CloneEnvGroupRequest{
 						&types.CloneEnvGroupRequest{
 							SourceName:      group.Name,
 							SourceName:      group.Name,
 							TargetNamespace: target.Namespace,
 							TargetNamespace: target.Namespace,
@@ -1358,10 +1403,10 @@ func (t *CloneEnvGroupHook) OnError(error) {}
 
 
 func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
 func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
 
 
-func getReleaseName(res *switchboardTypes.Resource) string {
+func getReleaseName(ctx context.Context, res *switchboardTypes.Resource, apiClient api.Client, cliConfig config.CLIConfig) string {
 	// can ignore the error because this method is called once
 	// can ignore the error because this method is called once
 	// GetTarget has alrealy been called and validated previously
 	// GetTarget has alrealy been called and validated previously
-	target, _ := preview.GetTarget(res.Name, res.Target)
+	target, _ := preview.GetTarget(ctx, res.Name, res.Target, apiClient, cliConfig)
 
 
 	if target.AppName != "" {
 	if target.AppName != "" {
 		return target.AppName
 		return target.AppName
@@ -1370,10 +1415,10 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 	return res.Name
 	return res.Name
 }
 }
 
 
-func getReleaseType(projectID uint, res *switchboardTypes.Resource) string {
+func getReleaseType(ctx context.Context, projectID uint, res *switchboardTypes.Resource, apiClient api.Client) string {
 	// can ignore the error because this method is called once
 	// can ignore the error because this method is called once
 	// GetSource has alrealy been called and validated previously
 	// GetSource has alrealy been called and validated previously
-	source, _ := preview.GetSource(projectID, res.Name, res.Source)
+	source, _ := preview.GetSource(ctx, projectID, res.Name, res.Source, apiClient)
 
 
 	if source != nil && source.Name != "" {
 	if source != nil && source.Name != "" {
 		return source.Name
 		return source.Name
@@ -1392,7 +1437,8 @@ func isSystemNamespace(namespace string) bool {
 
 
 type ErrorEmitterHook struct{}
 type ErrorEmitterHook struct{}
 
 
-func NewErrorEmitterHook(*api.Client, *switchboardTypes.ResourceGroup) *ErrorEmitterHook {
+// NewErrorEmitterHook handles switchboard errors
+func NewErrorEmitterHook(api.Client, *switchboardTypes.ResourceGroup) *ErrorEmitterHook {
 	return &ErrorEmitterHook{}
 	return &ErrorEmitterHook{}
 }
 }
 
 

+ 293 - 0
cli/cmd/commands/auth.go

@@ -0,0 +1,293 @@
+package commands
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/fatih/color"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	loginBrowser "github.com/porter-dev/porter/cli/cmd/login"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+)
+
+var manual bool = false
+
+func registerCommand_Auth(cliConf config.CLIConfig) *cobra.Command {
+	authCmd := &cobra.Command{
+		Use:   "auth",
+		Short: "Commands for authenticating to a Porter server",
+	}
+
+	loginCmd := &cobra.Command{
+		Use:   "login",
+		Short: "Authorizes a user for a given Porter server",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := login(cmd.Context(), cliConf)
+			if err != nil {
+				color.Red("Error logging in: %s\n", err.Error())
+				os.Exit(1)
+			}
+		},
+	}
+
+	registerCmd := &cobra.Command{
+		Use:   "register",
+		Short: "Creates a user for a given Porter server",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := register(cmd.Context(), cliConf)
+			if err != nil {
+				color.Red("Error registering: %s\n", err.Error())
+				os.Exit(1)
+			}
+		},
+	}
+
+	logoutCmd := &cobra.Command{
+		Use:   "logout",
+		Short: "Logs a user out of a given Porter server",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, logout)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	authCmd.AddCommand(loginCmd)
+	authCmd.AddCommand(registerCmd)
+	authCmd.AddCommand(logoutCmd)
+
+	loginCmd.PersistentFlags().BoolVar(
+		&manual,
+		"manual",
+		false,
+		"whether to prompt for manual authentication (username/pw)",
+	)
+
+	return authCmd
+}
+
+func login(ctx context.Context, cliConf config.CLIConfig) error {
+	client, err := api.NewClientWithConfig(ctx, api.NewClientInput{
+		BaseURL:     fmt.Sprintf("%s/api", cliConf.Host),
+		BearerToken: cliConf.Token,
+	})
+	if err != nil {
+		if !errors.Is(err, api.ErrNoAuthCredential) {
+			return fmt.Errorf("error creating porter API client: %w", err)
+		}
+	}
+
+	user, err := client.AuthCheck(ctx)
+	if err != nil {
+		if !strings.Contains(err.Error(), "Forbidden") {
+			return fmt.Errorf("unexpected error performing authorization check")
+		}
+		fmt.Println(err)
+	}
+
+	if cliConf.Token == "" {
+		// check for the --manual flag
+		if manual {
+			return loginManual(ctx, cliConf, client)
+		}
+
+		// log the user in
+		token, err := loginBrowser.Login(cliConf.Host)
+		if err != nil {
+			return err
+		}
+
+		// set the token in config
+		err = cliConf.SetToken(token)
+
+		if err != nil {
+			return err
+		}
+
+		client, err = api.NewClientWithConfig(ctx, api.NewClientInput{
+			BaseURL:     fmt.Sprintf("%s/api", cliConf.Host),
+			BearerToken: token,
+		})
+		if err != nil {
+			return fmt.Errorf("error creating porter API client: %w", err)
+		}
+
+		user, err = client.AuthCheck(ctx)
+
+		if err != nil {
+			color.Red("Invalid token.")
+			return err
+		}
+
+		_, _ = color.New(color.FgGreen).Println("Successfully logged in!")
+
+		return setProjectForUser(ctx, client, cliConf, user.ID)
+
+	}
+
+	err = cliConf.SetToken(cliConf.Token)
+	if err != nil {
+		return err
+	}
+	_, _ = color.New(color.FgGreen).Println("Successfully logged in!")
+
+	projID, exists, err := api.GetProjectIDFromToken(cliConf.Token)
+	if err != nil {
+		return err
+	}
+
+	// if project ID does not exist for the token, this is a user-issued CLI token, so the project
+	// ID should be queried
+	if !exists {
+		err = setProjectForUser(ctx, client, cliConf, user.ID)
+
+		if err != nil {
+			return err
+		}
+	} else {
+		// if the project ID does exist for the token, this is a project-issued token, and
+		// the project should be set automatically
+		err = cliConf.SetProject(ctx, client, projID)
+
+		if err != nil {
+			return err
+		}
+
+		err = setProjectCluster(ctx, client, cliConf, projID)
+
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func setProjectForUser(ctx context.Context, client api.Client, config config.CLIConfig, _ uint) error {
+	// get a list of projects, and set the current project
+	resp, err := client.ListUserProjects(ctx)
+	if err != nil {
+		return err
+	}
+
+	projects := *resp
+
+	if len(projects) > 0 {
+		config.SetProject(ctx, client, projects[0].ID) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
+
+		err = setProjectCluster(ctx, client, config, projects[0].ID)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func loginManual(ctx context.Context, cliConf config.CLIConfig, client api.Client) error {
+	var username, pw string
+
+	fmt.Println("Please log in with an email and password:")
+
+	username, err := utils.PromptPlaintext("Email: ")
+	if err != nil {
+		return err
+	}
+
+	pw, err = utils.PromptPassword("Password: ")
+
+	if err != nil {
+		return err
+	}
+
+	_, err = client.Login(ctx, &types.LoginUserRequest{
+		Email:    username,
+		Password: pw,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	// set the token to empty since this is manual (cookie-based) login
+	cliConf.SetToken("")
+
+	color.New(color.FgGreen).Println("Successfully logged in!")
+
+	// get a list of projects, and set the current project
+	resp, err := client.ListUserProjects(ctx)
+	if err != nil {
+		return err
+	}
+
+	projects := *resp
+
+	if len(projects) > 0 {
+		cliConf.SetProject(ctx, client, projects[0].ID) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
+
+		err = setProjectCluster(ctx, client, cliConf, projects[0].ID)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func register(ctx context.Context, cliConf config.CLIConfig) error {
+	client, err := api.NewClientWithConfig(ctx, api.NewClientInput{
+		BaseURL:     fmt.Sprintf("%s/api", cliConf.Host),
+		BearerToken: cliConf.Token,
+	})
+	if err != nil {
+		if !errors.Is(err, api.ErrNoAuthCredential) {
+			return fmt.Errorf("error creating porter API client: %w", err)
+		}
+	}
+
+	fmt.Println("Please register your admin account with an email and password:")
+
+	username, err := utils.PromptPlaintext("Email: ")
+	if err != nil {
+		return err
+	}
+
+	pw, err := utils.PromptPasswordWithConfirmation()
+	if err != nil {
+		return err
+	}
+
+	resp, err := client.CreateUser(ctx, &types.CreateUserRequest{
+		Email:    username,
+		Password: pw,
+	})
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Printf("Created user with email %s and id %d\n", username, resp.ID)
+
+	return nil
+}
+
+func logout(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	err := client.Logout(ctx)
+	if err != nil {
+		return err
+	}
+
+	cliConf.SetToken("")
+
+	color.Green("Successfully logged out")
+
+	return nil
+}

+ 54 - 57
cli/cmd/cluster.go → cli/cmd/commands/cluster.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -11,70 +11,67 @@ import (
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-// clusterCmd represents the "porter cluster" base command when called
-// without any subcommands
-var clusterCmd = &cobra.Command{
-	Use:     "cluster",
-	Aliases: []string{"clusters"},
-	Short:   "Commands that read from a connected cluster",
-}
-
-var clusterListCmd = &cobra.Command{
-	Use:   "list",
-	Short: "Lists the linked clusters in the current project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listClusters)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var clusterDeleteCmd = &cobra.Command{
-	Use:   "delete [id]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Deletes the cluster with the given id",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteCluster)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var clusterNamespaceCmd = &cobra.Command{
-	Use:     "namespace",
-	Aliases: []string{"namespaces"},
-	Short:   "Commands that perform operations on cluster namespaces",
-}
+func registerCommand_Cluster(cliConf config.CLIConfig) *cobra.Command {
+	clusterCmd := &cobra.Command{
+		Use:     "cluster",
+		Aliases: []string{"clusters"},
+		Short:   "Commands that read from a connected cluster",
+	}
 
 
-var clusterNamespaceListCmd = &cobra.Command{
-	Use:   "list",
-	Short: "Lists the namespaces in a cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listNamespaces)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	clusterListCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the linked clusters in the current project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listClusters)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	clusterCmd.AddCommand(clusterListCmd)
 
 
-func init() {
-	rootCmd.AddCommand(clusterCmd)
+	clusterDeleteCmd := &cobra.Command{
+		Use:   "delete [id]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Deletes the cluster with the given id",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteCluster)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	clusterCmd.AddCommand(clusterDeleteCmd)
 
 
+	clusterNamespaceCmd := &cobra.Command{
+		Use:     "namespace",
+		Aliases: []string{"namespaces"},
+		Short:   "Commands that perform operations on cluster namespaces",
+	}
 	clusterCmd.AddCommand(clusterNamespaceCmd)
 	clusterCmd.AddCommand(clusterNamespaceCmd)
-	clusterCmd.AddCommand(clusterListCmd)
-	clusterCmd.AddCommand(clusterDeleteCmd)
 
 
+	clusterNamespaceListCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the namespaces in a cluster",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listNamespaces)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 	clusterNamespaceCmd.AddCommand(clusterNamespaceListCmd)
 	clusterNamespaceCmd.AddCommand(clusterNamespaceListCmd)
+
+	return clusterCmd
 }
 }
 
 
-func listClusters(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	resp, err := client.ListProjectClusters(context.Background(), cliConf.Project)
+func listClusters(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	resp, err := client.ListProjectClusters(ctx, cliConf.Project)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -101,7 +98,7 @@ func listClusters(user *types.GetAuthenticatedUserResponse, client *api.Client,
 	return nil
 	return nil
 }
 }
 
 
-func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func deleteCluster(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	userResp, err := utils.PromptPlaintext(
 	userResp, err := utils.PromptPlaintext(
 		fmt.Sprintf(
 		fmt.Sprintf(
 			`Are you sure you'd like to delete the cluster with id %s? %s `,
 			`Are you sure you'd like to delete the cluster with id %s? %s `,
@@ -119,7 +116,7 @@ func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client,
 			return err
 			return err
 		}
 		}
 
 
-		err = client.DeleteProjectCluster(context.Background(), cliConf.Project, uint(id))
+		err = client.DeleteProjectCluster(ctx, cliConf.Project, uint(id))
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -131,7 +128,7 @@ func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client,
 	return nil
 	return nil
 }
 }
 
 
-func listNamespaces(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func listNamespaces(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	pID := cliConf.Project
 	pID := cliConf.Project
 
 
 	// get the service account based on the cluster id
 	// get the service account based on the cluster id
@@ -139,7 +136,7 @@ func listNamespaces(user *types.GetAuthenticatedUserResponse, client *api.Client
 
 
 	// get the list of namespaces
 	// get the list of namespaces
 	namespaceList, err := client.GetK8sNamespaces(
 	namespaceList, err := client.GetK8sNamespaces(
-		context.Background(),
+		ctx,
 		pID,
 		pID,
 		cID,
 		cID,
 	)
 	)

+ 317 - 0
cli/cmd/commands/config.go

@@ -0,0 +1,317 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/briandowns/spinner"
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Config(cliConf config.CLIConfig) *cobra.Command {
+	configCmd := &cobra.Command{
+		Use:   "config",
+		Short: "Commands that control local configuration settings",
+		Run: func(cmd *cobra.Command, args []string) {
+			if err := printConfig(); err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+				os.Exit(1)
+			}
+		},
+	}
+
+	configSetProjectCmd := &cobra.Command{
+		Use:   "set-project [id]",
+		Args:  cobra.MaximumNArgs(1),
+		Short: "Saves the project id in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			client, err := api.NewClientWithConfig(cmd.Context(), api.NewClientInput{
+				BaseURL:        fmt.Sprintf("%s/api", cliConf.Host),
+				BearerToken:    cliConf.Token,
+				CookieFileName: "cookie.json",
+			})
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "error creating porter API client: %s\n", err.Error())
+				os.Exit(1)
+			}
+
+			if len(args) == 0 {
+				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetProject)
+				if err != nil {
+					os.Exit(1)
+				}
+			} else {
+				projID, err := strconv.ParseUint(args[0], 10, 64)
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %s\n", err.Error())
+					os.Exit(1)
+				}
+
+				err = cliConf.SetProject(cmd.Context(), client, uint(projID))
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %s\n", err.Error())
+					os.Exit(1)
+				}
+			}
+		},
+	}
+
+	configSetClusterCmd := &cobra.Command{
+		Use:   "set-cluster [id]",
+		Args:  cobra.MaximumNArgs(1),
+		Short: "Saves the cluster id in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			if len(args) == 0 {
+				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetCluster)
+				if err != nil {
+					os.Exit(1)
+				}
+			} else {
+				clusterID, err := strconv.ParseUint(args[0], 10, 64)
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+					os.Exit(1)
+				}
+
+				err = cliConf.SetCluster(uint(clusterID))
+
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+					os.Exit(1)
+				}
+			}
+		},
+	}
+
+	configSetRegistryCmd := &cobra.Command{
+		Use:   "set-registry [id]",
+		Args:  cobra.MaximumNArgs(1),
+		Short: "Saves the registry id in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			if len(args) == 0 {
+				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetRegistry)
+				if err != nil {
+					os.Exit(1)
+				}
+			} else {
+				registryID, err := strconv.ParseUint(args[0], 10, 64)
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+					os.Exit(1)
+				}
+
+				err = cliConf.SetRegistry(uint(registryID))
+
+				if err != nil {
+					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+					os.Exit(1)
+				}
+			}
+		},
+	}
+
+	configSetHelmRepoCmd := &cobra.Command{
+		Use:   "set-helmrepo [id]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Saves the helm repo id in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			hrID, err := strconv.ParseUint(args[0], 10, 64)
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+				os.Exit(1)
+			}
+
+			err = cliConf.SetHelmRepo(uint(hrID))
+
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
+				os.Exit(1)
+			}
+		},
+	}
+
+	configSetHostCmd := &cobra.Command{
+		Use:   "set-host [host]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Saves the host in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := cliConf.SetHost(args[0])
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %s\n", err.Error())
+				os.Exit(1)
+			}
+		},
+	}
+
+	configSetKubeconfigCmd := &cobra.Command{
+		Use:   "set-kubeconfig [kubeconfig-path]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Saves the path to kubeconfig in the default configuration",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := cliConf.SetKubeconfig(args[0])
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %s\n", err.Error())
+				os.Exit(1)
+			}
+		},
+	}
+
+	configCmd.AddCommand(configSetProjectCmd)
+	configCmd.AddCommand(configSetClusterCmd)
+	configCmd.AddCommand(configSetHostCmd)
+	configCmd.AddCommand(configSetRegistryCmd)
+	configCmd.AddCommand(configSetHelmRepoCmd)
+	configCmd.AddCommand(configSetKubeconfigCmd)
+	return configCmd
+}
+
+func printConfig() error {
+	config, err := os.ReadFile(filepath.Join(home, ".porter", "porter.yaml"))
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(config))
+
+	return nil
+}
+
+func listAndSetProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
+	_ = s.Color("cyan")
+	s.Suffix = " Loading list of projects"
+	s.Start()
+
+	resp, err := client.ListUserProjects(ctx)
+
+	s.Stop()
+
+	if err != nil {
+		return err
+	}
+
+	var projID uint64
+
+	if len(*resp) > 1 {
+		// only give the option to select when more than one option exists
+		projName, err := utils.PromptSelect("Select a project with ID", func() []string {
+			var names []string
+
+			for _, proj := range *resp {
+				names = append(names, fmt.Sprintf("%s - %d", proj.Name, proj.ID))
+			}
+
+			return names
+		}())
+		if err != nil {
+			return err
+		}
+
+		projID, _ = strconv.ParseUint(strings.Split(projName, " - ")[1], 10, 64)
+	} else {
+		projID = uint64((*resp)[0].ID)
+	}
+
+	err = cliConf.SetProject(ctx, client, uint(projID))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func listAndSetCluster(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
+	_ = s.Color("cyan")
+	s.Suffix = " Loading list of clusters"
+	s.Start()
+
+	resp, err := client.ListProjectClusters(ctx, cliConf.Project)
+
+	s.Stop()
+
+	if err != nil {
+		return err
+	}
+
+	var clusterID uint64
+
+	if len(*resp) > 1 {
+		clusterName, err := utils.PromptSelect("Select a cluster with ID", func() []string {
+			var names []string
+
+			for _, cluster := range *resp {
+				names = append(names, fmt.Sprintf("%s - %d", cluster.Name, cluster.ID))
+			}
+
+			return names
+		}())
+		if err != nil {
+			return err
+		}
+
+		clusterID, _ = strconv.ParseUint(strings.Split(clusterName, " - ")[1], 10, 64)
+	} else {
+		clusterID = uint64((*resp)[0].ID)
+	}
+
+	err = cliConf.SetCluster(uint(clusterID))
+	if err != nil {
+		return fmt.Errorf("unable to set cluster: %w", err)
+	}
+
+	return nil
+}
+
+func listAndSetRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
+	_ = s.Color("cyan")
+	s.Suffix = " Loading list of registries"
+	s.Start()
+
+	resp, err := client.ListRegistries(ctx, cliConf.Project)
+
+	s.Stop()
+
+	if err != nil {
+		return err
+	}
+
+	var regID uint64
+
+	if len(*resp) > 1 {
+		regName, err := utils.PromptSelect("Select a registry with ID", func() []string {
+			var names []string
+
+			for _, cluster := range *resp {
+				names = append(names, fmt.Sprintf("%s - %d", cluster.Name, cluster.ID))
+			}
+
+			return names
+		}())
+		if err != nil {
+			return err
+		}
+
+		regID, _ = strconv.ParseUint(strings.Split(regName, " - ")[1], 10, 64)
+	} else {
+		regID = uint64((*resp)[0].ID)
+	}
+
+	err = cliConf.SetRegistry(uint(regID))
+	if err != nil {
+		return fmt.Errorf("error setting registry: %w", err)
+	}
+
+	return nil
+}

+ 252 - 0
cli/cmd/commands/connect.go

@@ -0,0 +1,252 @@
+package commands
+
+import (
+	"context"
+	"os"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/connect"
+	"github.com/spf13/cobra"
+)
+
+var (
+	kubeconfigPath string
+	print          *bool
+	contexts       *[]string
+)
+
+func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
+	connectCmd := &cobra.Command{
+		Use:   "connect",
+		Short: "Commands that connect to external clusters and providers",
+	}
+
+	connectKubeconfigCmd := &cobra.Command{
+		Use:   "kubeconfig",
+		Short: "Uses the local kubeconfig to add a cluster",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectKubeconfig)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectECRCmd := &cobra.Command{
+		Use:   "ecr",
+		Short: "Adds an ECR instance to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectECR)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectDockerhubCmd := &cobra.Command{
+		Use:   "dockerhub",
+		Short: "Adds a Docker Hub registry integration to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectDockerhub)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectRegistryCmd := &cobra.Command{
+		Use:   "registry",
+		Short: "Adds a custom image registry to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectRegistry)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectHelmRepoCmd := &cobra.Command{
+		Use:   "helm",
+		Short: "Adds a custom Helm registry to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectHelmRepo)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectGCRCmd := &cobra.Command{
+		Use:   "gcr",
+		Short: "Adds a GCR instance to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectGCR)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectGARCmd := &cobra.Command{
+		Use:   "gar",
+		Short: "Adds a GAR instance to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectGAR)
+			if err != nil {
+				os.Exit(1)
+			}
+			cmd.Context()
+		},
+	}
+
+	connectDOCRCmd := &cobra.Command{
+		Use:   "docr",
+		Short: "Adds a DOCR instance to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectDOCR)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	connectCmd.AddCommand(connectKubeconfigCmd)
+
+	connectKubeconfigCmd.PersistentFlags().StringVarP(
+		&kubeconfigPath,
+		"kubeconfig",
+		"k",
+		"",
+		"path to kubeconfig",
+	)
+
+	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
+		"context",
+		nil,
+		"the context to connect (defaults to the current context)",
+	)
+
+	connectCmd.AddCommand(connectECRCmd)
+	connectCmd.AddCommand(connectRegistryCmd)
+	connectCmd.AddCommand(connectDockerhubCmd)
+	connectCmd.AddCommand(connectGCRCmd)
+	connectCmd.AddCommand(connectGARCmd)
+	connectCmd.AddCommand(connectDOCRCmd)
+	connectCmd.AddCommand(connectHelmRepoCmd)
+	return connectCmd
+}
+
+func runConnectKubeconfig(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	isLocal := false
+
+	if cliConf.Driver == "local" {
+		isLocal = true
+	}
+
+	id, err := connect.Kubeconfig(
+		ctx,
+		client,
+		kubeconfigPath,
+		*contexts,
+		cliConf.Project,
+		isLocal,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetCluster(id)
+}
+
+func runConnectECR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.ECR(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectGCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.GCR(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectGAR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.GAR(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectDOCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.DOCR(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectDockerhub(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.Dockerhub(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	regID, err := connect.Registry(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
+func runConnectHelmRepo(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+	hrID, err := connect.HelmRepo(
+		ctx,
+		client,
+		cliConf.Project,
+	)
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetHelmRepo(hrID)
+}

+ 60 - 48
cli/cmd/create.go → cli/cmd/commands/create.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -8,6 +8,8 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -20,13 +22,21 @@ import (
 	"sigs.k8s.io/yaml"
 	"sigs.k8s.io/yaml"
 )
 )
 
 
-// createCmd represents the "porter create" base command when called
-// without any subcommands
-var createCmd = &cobra.Command{
-	Use:   "create [kind]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Creates a new application with name given by the --app flag.",
-	Long: fmt.Sprintf(`
+var (
+	name        string
+	values      string
+	source      string
+	image       string
+	registryURL string
+	forceBuild  bool
+)
+
+func registerCommand_Create(cliConf config.CLIConfig) *cobra.Command {
+	createCmd := &cobra.Command{
+		Use:   "create [kind]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Creates a new application with name given by the --app flag.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Creates a new application with name given by the --app flag and a "kind", which can be one of
 Creates a new application with name given by the --app flag and a "kind", which can be one of
@@ -60,32 +70,20 @@ To deploy an application from a Docker registry, use "--source registry" and pas
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter create\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --values values.yaml"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --path ./path/to/app"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source github"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source registry --image gcr.io/snowflake-12345/example-app:latest"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, createFull)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var (
-	name        string
-	values      string
-	source      string
-	image       string
-	registryURL string
-	forceBuild  bool
-)
-
-func init() {
-	rootCmd.AddCommand(createCmd)
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter create\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --values values.yaml"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --path ./path/to/app"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source github"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source registry --image gcr.io/snowflake-12345/example-app:latest"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, createFull)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	createCmd.PersistentFlags().StringVar(
 	createCmd.PersistentFlags().StringVar(
 		&name,
 		&name,
@@ -177,18 +175,30 @@ func init() {
 	)
 	)
 
 
 	createCmd.PersistentFlags().MarkDeprecated("force-build", "--force-build is deprecated")
 	createCmd.PersistentFlags().MarkDeprecated("force-build", "--force-build is deprecated")
+	return createCmd
 }
 }
 
 
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
 
 
-func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func createFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.CreateFull(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	// check the kind
 	// check the kind
 	if _, exists := supportedKinds[args[0]]; !exists {
 	if _, exists := supportedKinds[args[0]]; !exists {
 		return fmt.Errorf("%s is not a supported type: specify web, job, or worker", args[0])
 		return fmt.Errorf("%s is not a supported type: specify web, job, or worker", args[0])
 	}
 	}
 
 
-	var err error
-
 	fullPath, err := filepath.Abs(localPath)
 	fullPath, err := filepath.Abs(localPath)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -251,13 +261,13 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 
 	if source == "local" {
 	if source == "local" {
 		if useCache {
 		if useCache {
-			regID, imageURL, err := createAgent.GetImageRepoURL(name, namespace)
+			regID, imageURL, err := createAgent.GetImageRepoURL(ctx, name, namespace)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 
 
 			err = client.CreateRepository(
 			err = client.CreateRepository(
-				context.Background(),
+				ctx,
 				cliConf.Project,
 				cliConf.Project,
 				regID,
 				regID,
 				&types.CreateRegistryRepositoryRequest{
 				&types.CreateRegistryRepositoryRequest{
@@ -269,21 +279,21 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 				return err
 				return err
 			}
 			}
 
 
-			err = config.SetDockerConfig(createAgent.Client)
+			err = config.SetDockerConfig(ctx, createAgent.Client, project.ID)
 
 
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 		}
 		}
 
 
-		subdomain, err := createAgent.CreateFromDocker(valuesObj, "default", nil)
+		subdomain, err := createAgent.CreateFromDocker(ctx, valuesObj, "default", nil)
 
 
 		return handleSubdomainCreate(subdomain, err)
 		return handleSubdomainCreate(subdomain, err)
 	} else if source == "github" {
 	} else if source == "github" {
-		return createFromGithub(createAgent, valuesObj)
+		return createFromGithub(ctx, createAgent, valuesObj)
 	}
 	}
 
 
-	subdomain, err := createAgent.CreateFromRegistry(image, valuesObj)
+	subdomain, err := createAgent.CreateFromRegistry(ctx, image, valuesObj)
 
 
 	return handleSubdomainCreate(subdomain, err)
 	return handleSubdomainCreate(subdomain, err)
 }
 }
@@ -302,7 +312,7 @@ func handleSubdomainCreate(subdomain string, err error) error {
 	return nil
 	return nil
 }
 }
 
 
-func createFromGithub(createAgent *deploy.CreateAgent, overrideValues map[string]interface{}) error {
+func createFromGithub(ctx context.Context, createAgent *deploy.CreateAgent, overrideValues map[string]interface{}) error {
 	fullPath, err := filepath.Abs(localPath)
 	fullPath, err := filepath.Abs(localPath)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -328,10 +338,12 @@ func createFromGithub(createAgent *deploy.CreateAgent, overrideValues map[string
 		return fmt.Errorf("remote is not a Github repository")
 		return fmt.Errorf("remote is not a Github repository")
 	}
 	}
 
 
-	subdomain, err := createAgent.CreateFromGithub(&deploy.GithubOpts{
-		Branch: gitBranch,
-		Repo:   remoteRepo,
-	}, overrideValues)
+	subdomain, err := createAgent.CreateFromGithub(
+		ctx,
+		&deploy.GithubOpts{
+			Branch: gitBranch,
+			Repo:   remoteRepo,
+		}, overrideValues)
 
 
 	return handleSubdomainCreate(subdomain, err)
 	return handleSubdomainCreate(subdomain, err)
 }
 }

+ 303 - 0
cli/cmd/commands/delete.go

@@ -0,0 +1,303 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Delete(cliConf config.CLIConfig) *cobra.Command {
+	deleteCmd := &cobra.Command{
+		Use:   "delete",
+		Short: "Deletes a deployment",
+		Long: fmt.Sprintf(`
+%s
+
+Destroys a deployment, which is read based on env variables.
+
+  %s
+
+The following are the environment variables that can be used to set certain values while
+deleting a configuration:
+  PORTER_CLUSTER              Cluster ID that contains the project
+  PORTER_PROJECT              Project ID that contains the application
+	`,
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter delete\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter delete"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteDeployment)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	// deleteAppsCmd represents the "porter delete apps" subcommand
+	deleteAppsCmd := &cobra.Command{
+		Use:     "apps",
+		Aliases: []string{"app", "applications", "application"},
+		Short:   "Deletes an existing app",
+		Args:    cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteApp)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	// deleteJobsCmd represents the "porter delete jobs" subcommand
+	deleteJobsCmd := &cobra.Command{
+		Use:     "jobs",
+		Aliases: []string{"job"},
+		Short:   "Deletes an existing job",
+		Args:    cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteJob)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	// deleteAddonsCmd represents the "porter delete addons" subcommand
+	deleteAddonsCmd := &cobra.Command{
+		Use:     "addons",
+		Aliases: []string{"addon"},
+		Short:   "Deletes an existing addon",
+		Args:    cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteAddon)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	// deleteHelmCmd represents the "porter delete helm" subcommand
+	deleteHelmCmd := &cobra.Command{
+		Use:     "helm",
+		Aliases: []string{"helmrepo", "helmrepos"},
+		Short:   "Deletes an existing helm repo",
+		Args:    cobra.ExactArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteHelm)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	deleteCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"default",
+		"Namespace of the application",
+	)
+
+	deleteCmd.AddCommand(deleteAppsCmd)
+	deleteCmd.AddCommand(deleteJobsCmd)
+	deleteCmd.AddCommand(deleteAddonsCmd)
+	deleteCmd.AddCommand(deleteHelmCmd)
+
+	return deleteCmd
+}
+
+func deleteDeployment(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.DeleteDeployment(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	projectID := cliConf.Project
+
+	if projectID == 0 {
+		return fmt.Errorf("project id must be set")
+	}
+
+	clusterID := cliConf.Cluster
+
+	if clusterID == 0 {
+		return fmt.Errorf("cluster id must be set")
+	}
+
+	var deploymentID uint
+
+	if deplIDStr := os.Getenv("PORTER_DEPLOYMENT_ID"); deplIDStr != "" {
+		deplID, err := strconv.ParseUint(deplIDStr, 10, 32)
+		if err != nil {
+			return fmt.Errorf("error parsing deployment ID: %s", deplIDStr)
+		}
+
+		deploymentID = uint(deplID)
+	} else {
+		return fmt.Errorf("Deployment ID must be defined, set by PORTER_DEPLOYMENT_ID")
+	}
+
+	return client.DeleteDeployment(
+		ctx, projectID, clusterID, deploymentID,
+	)
+}
+
+func deleteApp(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.DeleteApp(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	name := args[0]
+
+	resp, err := client.GetRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+	if err != nil {
+		return err
+	}
+
+	rel := *resp
+
+	if rel.Chart.Name() != "web" && rel.Chart.Name() != "worker" {
+		return fmt.Errorf("no app found with name: %s", name)
+	}
+
+	color.New(color.FgBlue).Printf("Deleting app: %s\n", name)
+
+	err = client.DeleteRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func deleteJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.DeleteJob(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	name := args[0]
+
+	resp, err := client.GetRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+	if err != nil {
+		return err
+	}
+
+	rel := *resp
+
+	if rel.Chart.Name() != "job" {
+		return fmt.Errorf("no job found with name: %s", name)
+	}
+
+	color.New(color.FgBlue).Printf("Deleting job: %s\n", name)
+
+	err = client.DeleteRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func deleteAddon(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	name := args[0]
+
+	resp, err := client.GetRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+	if err != nil {
+		return err
+	}
+
+	rel := *resp
+
+	if rel.Chart.Name() == "web" || rel.Chart.Name() == "worker" || rel.Chart.Name() == "job" {
+		return fmt.Errorf("no addon found with name: %s", name)
+	}
+
+	color.New(color.FgBlue).Printf("Deleting addon: %s\n", name)
+
+	err = client.DeleteRelease(
+		ctx, cliConf.Project, cliConf.Cluster, namespace, name,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func deleteHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	name := args[0]
+
+	resp, err := client.ListHelmRepos(ctx, cliConf.Project)
+	if err != nil {
+		return err
+	}
+
+	var repo *types.HelmRepo
+
+	for _, r := range resp {
+		if r.Name == name {
+			repo = r
+			break
+		}
+	}
+
+	if repo == nil {
+		return fmt.Errorf("no helm repo found with name: %s", name)
+	}
+
+	color.New(color.FgBlue).Printf("Deleting helm repo: %s\n", name)
+
+	err = client.DeleteHelmRepo(ctx, cliConf.Project, repo.ID)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 53 - 36
cli/cmd/bluegreen.go → cli/cmd/commands/deploy_bluegreen.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -6,6 +6,9 @@ import (
 	"os"
 	"os"
 	"time"
 	"time"
 
 
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -16,23 +19,21 @@ import (
 	intstrutil "k8s.io/apimachinery/pkg/util/intstr"
 	intstrutil "k8s.io/apimachinery/pkg/util/intstr"
 )
 )
 
 
-var deployCmd = &cobra.Command{
-	Use: "deploy",
-}
-
-var bluegreenCmd = &cobra.Command{
-	Use:   "blue-green-switch",
-	Short: "Automatically switches the traffic of a blue-green deployment once the new application is ready.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, bluegreenSwitch)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+func registerCommand_Deploy(cliConf config.CLIConfig) *cobra.Command {
+	deployCmd := &cobra.Command{
+		Use: "deploy",
+	}
 
 
-func init() {
-	rootCmd.AddCommand(deployCmd)
+	bluegreenCmd := &cobra.Command{
+		Use:   "blue-green-switch",
+		Short: "Automatically switches the traffic of a blue-green deployment once the new application is ready.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, bluegreenSwitch)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 	deployCmd.AddCommand(bluegreenCmd)
 	deployCmd.AddCommand(bluegreenCmd)
 
 
 	bluegreenCmd.PersistentFlags().StringVar(
 	bluegreenCmd.PersistentFlags().StringVar(
@@ -57,11 +58,25 @@ func init() {
 		"",
 		"",
 		"The namespace of the jobs.",
 		"The namespace of the jobs.",
 	)
 	)
+	return deployCmd
 }
 }
 
 
-func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func bluegreenSwitch(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConfig.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.BlueGreenSwitch(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	// get the web release
 	// get the web release
-	webRelease, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, app)
+	webRelease, err := client.GetRelease(ctx, cliConfig.Project, cliConfig.Cluster, namespace, app)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -74,11 +89,11 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	currActiveImage := deploy.GetCurrActiveBlueGreenImage(webRelease.Config)
 	currActiveImage := deploy.GetCurrActiveBlueGreenImage(webRelease.Config)
 
 
 	sharedConf := &PorterRunSharedConfig{
 	sharedConf := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err = sharedConf.setSharedConfig()
-
+	err = sharedConf.setSharedConfig(ctx)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
@@ -94,7 +109,7 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	for time.Now().Before(timeWait) {
 	for time.Now().Before(timeWait) {
 		// refresh the client every 10 minutes
 		// refresh the client every 10 minutes
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
-			err = sharedConf.setSharedConfig()
+			err = sharedConf.setSharedConfig(ctx)
 
 
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 				return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
@@ -104,7 +119,7 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 		}
 		}
 
 
 		depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
 		depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
-			context.Background(),
+			ctx,
 			metav1.ListOptions{
 			metav1.ListOptions{
 				LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 				LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 			},
 			},
@@ -129,13 +144,13 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 					// push the deployment
 					// push the deployment
 					color.New(color.FgGreen).Printf("Switching traffic for app %s\n", app)
 					color.New(color.FgGreen).Printf("Switching traffic for app %s\n", app)
 
 
-					deployAgent, err := updateGetAgent(client)
+					deployAgent, err := updateGetAgent(ctx, client, cliConfig)
 					if err != nil {
 					if err != nil {
 						return err
 						return err
 					}
 					}
 
 
 					if currActiveImage == "" {
 					if currActiveImage == "" {
-						err = deployAgent.UpdateImageAndValues(map[string]interface{}{
+						err = deployAgent.UpdateImageAndValues(ctx, map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 								"enabled":                  true,
 								"enabled":                  true,
 								"disablePrimaryDeployment": true,
 								"disablePrimaryDeployment": true,
@@ -144,7 +159,7 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 							},
 							},
 						})
 						})
 					} else {
 					} else {
-						err = deployAgent.UpdateImageAndValues(map[string]interface{}{
+						err = deployAgent.UpdateImageAndValues(ctx, map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 								"enabled":                  true,
 								"enabled":                  true,
 								"disablePrimaryDeployment": true,
 								"disablePrimaryDeployment": true,
@@ -182,19 +197,21 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	// wait 30 seconds before removing old deployment
 	// wait 30 seconds before removing old deployment
 	time.Sleep(30 * time.Second)
 	time.Sleep(30 * time.Second)
 
 
-	deployAgent, err := updateGetAgent(client)
+	deployAgent, err := updateGetAgent(ctx, client, cliConfig)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = deployAgent.UpdateImageAndValues(map[string]interface{}{
-		"bluegreen": map[string]interface{}{
-			"enabled":                  true,
-			"disablePrimaryDeployment": true,
-			"activeImageTag":           tag,
-			"imageTags":                []string{tag},
-		},
-	})
+	err = deployAgent.UpdateImageAndValues( //nolint - do not want to change logic. New linter error
+		ctx,
+		map[string]interface{}{
+			"bluegreen": map[string]interface{}{
+				"enabled":                  true,
+				"disablePrimaryDeployment": true,
+				"activeImageTag":           tag,
+				"imageTags":                []string{tag},
+			},
+		})
 
 
 	return nil
 	return nil
 }
 }

+ 36 - 0
cli/cmd/commands/docker.go

@@ -0,0 +1,36 @@
+package commands
+
+import (
+	"context"
+	"os"
+
+	api "github.com/porter-dev/porter/api/client"
+	ptypes "github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Docker(cliConf config.CLIConfig) *cobra.Command {
+	dockerCmd := &cobra.Command{
+		Use:   "docker",
+		Short: "Commands to configure Docker for a project",
+	}
+
+	configureCmd := &cobra.Command{
+		Use:   "configure",
+		Short: "Configures the host's Docker instance",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, dockerConfig)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	dockerCmd.AddCommand(configureCmd)
+	return dockerCmd
+}
+
+func dockerConfig(ctx context.Context, user *ptypes.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	return config.SetDockerConfig(ctx, client, cliConf.Project)
+}

+ 14 - 6
cli/cmd/errors.go → cli/cmd/commands/errors.go

@@ -1,8 +1,9 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
 	"errors"
 	"errors"
+	"fmt"
 	"os"
 	"os"
 	"strings"
 	"strings"
 
 
@@ -18,10 +19,17 @@ var (
 	ErrCannotConnect error = errors.New("Unable to connect to the Porter server.")
 	ErrCannotConnect error = errors.New("Unable to connect to the Porter server.")
 )
 )
 
 
-func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error) error {
-	client := config.GetAPIClient()
+func checkLoginAndRunWithConfig(ctx context.Context, cliConf config.CLIConfig, args []string, runner func(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error) error {
+	client, err := api.NewClientWithConfig(ctx, api.NewClientInput{
+		BaseURL:        fmt.Sprintf("%s/api", cliConf.Host),
+		BearerToken:    cliConf.Token,
+		CookieFileName: "cookie.json",
+	})
+	if err != nil {
+		return fmt.Errorf("error creating porter API client: %w", err)
+	}
 
 
-	user, err := client.AuthCheck(context.Background())
+	user, err := client.AuthCheck(ctx)
 	if err != nil {
 	if err != nil {
 		red := color.New(color.FgRed)
 		red := color.New(color.FgRed)
 
 
@@ -39,7 +47,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 		return err
 		return err
 	}
 	}
 
 
-	err = runner(user, client, args)
+	err = runner(ctx, user, client, cliConf, args)
 
 
 	if err != nil {
 	if err != nil {
 		red := color.New(color.FgRed)
 		red := color.New(color.FgRed)
@@ -54,7 +62,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			return nil
 			return nil
 		}
 		}
 
 
-		cliErrors.GetErrorHandler().HandleError(err)
+		cliErrors.GetErrorHandler(cliConf).HandleError(err)
 
 
 		return err
 		return err
 	}
 	}

+ 60 - 33
cli/cmd/get.go → cli/cmd/commands/get.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -6,6 +6,9 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 
 
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
@@ -13,36 +16,34 @@ import (
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 )
 )
 
 
-// getCmd represents the "porter get" base command when called
-// without any subcommands
-var getCmd = &cobra.Command{
-	Use:   "get [release]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Fetches a release.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, get)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+var output string
 
 
-// getValuesCmd represents the "porter get values" command
-var getValuesCmd = &cobra.Command{
-	Use:   "values [release]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Fetches the Helm values for a release.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, getValues)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+func registerCommand_Get(cliConf config.CLIConfig) *cobra.Command {
+	getCmd := &cobra.Command{
+		Use:   "get [release]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Fetches a release.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, get)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var output string
+	// getValuesCmd represents the "porter get values" command
+	getValuesCmd := &cobra.Command{
+		Use:   "values [release]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Fetches the Helm values for a release.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, getValues)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-func init() {
 	getCmd.PersistentFlags().StringVar(
 	getCmd.PersistentFlags().StringVar(
 		&namespace,
 		&namespace,
 		"namespace",
 		"namespace",
@@ -59,7 +60,7 @@ func init() {
 
 
 	getCmd.AddCommand(getValuesCmd)
 	getCmd.AddCommand(getValuesCmd)
 
 
-	rootCmd.AddCommand(getCmd)
+	return getCmd
 }
 }
 
 
 type getReleaseInfo struct {
 type getReleaseInfo struct {
@@ -70,8 +71,21 @@ type getReleaseInfo struct {
 	RevisionID   int       `json:"revision_id" yaml:"revision_id"`
 	RevisionID   int       `json:"revision_id" yaml:"revision_id"`
 }
 }
 
 
-func get(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	rel, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0])
+func get(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.Get(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	rel, err := client.GetRelease(ctx, cliConf.Project, cliConf.Cluster, namespace, args[0])
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -109,8 +123,21 @@ func get(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 	return nil
 	return nil
 }
 }
 
 
-func getValues(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	rel, err := client.GetRelease(context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0])
+func getValues(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.GetValues(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	rel, err := client.GetRelease(ctx, cliConf.Project, cliConf.Cluster, namespace, args[0])
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 59 - 0
cli/cmd/commands/helm.go

@@ -0,0 +1,59 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Helm(cliConf config.CLIConfig) *cobra.Command {
+	helmCmd := &cobra.Command{
+		Use:   "helm",
+		Short: "Use helm to interact with a Porter cluster",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runHelm)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	return helmCmd
+}
+
+func runHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	_, err := exec.LookPath("helm")
+	if err != nil {
+		return fmt.Errorf("error finding helm: %w", err)
+	}
+
+	tmpFile, err := downloadTempKubeconfig(ctx, client, cliConf)
+	if err != nil {
+		return err
+	}
+
+	defer func() {
+		os.Remove(tmpFile)
+	}()
+
+	os.Setenv("KUBECONFIG", tmpFile)
+
+	cmd := exec.Command("helm", args...)
+
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	err = cmd.Run()
+
+	if err != nil {
+		return fmt.Errorf("error running helm: %w", err)
+	}
+
+	return nil
+}

+ 106 - 62
cli/cmd/job.go → cli/cmd/commands/job.go

@@ -1,10 +1,13 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 
 
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -13,14 +16,17 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-var jobCmd = &cobra.Command{
-	Use: "job",
-}
+var imageRepoURI string
 
 
-var batchImageUpdateCmd = &cobra.Command{
-	Use:   "update-images",
-	Short: "Updates the image tag of all jobs in a namespace which use a specific image.",
-	Long: fmt.Sprintf(`
+func registerCommand_Job(cliConf config.CLIConfig) *cobra.Command {
+	jobCmd := &cobra.Command{
+		Use: "job",
+	}
+
+	batchImageUpdateCmd := &cobra.Command{
+		Use:   "update-images",
+		Short: "Updates the image tag of all jobs in a namespace which use a specific image.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Updates the image tag of all jobs in a namespace which use a specific image. Note that for all
 Updates the image tag of all jobs in a namespace which use a specific image. Note that for all
@@ -36,22 +42,22 @@ use the --namespace flag:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job update-images\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --image-repo-uri my-image.registry.io --tag newtag"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --namespace custom-namespace --image-repo-uri my-image.registry.io --tag newtag"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, batchImageUpdate)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job update-images\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --image-repo-uri my-image.registry.io --tag newtag"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --namespace custom-namespace --image-repo-uri my-image.registry.io --tag newtag"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, batchImageUpdate)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var waitCmd = &cobra.Command{
-	Use:   "wait",
-	Short: "Waits for a job to complete.",
-	Long: fmt.Sprintf(`
+	waitCmd := &cobra.Command{
+		Use:   "wait",
+		Short: "Waits for a job to complete.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Waits for a job with a given name and namespace to complete a run. If the job completes successfully,
 Waits for a job with a given name and namespace to complete a run. If the job completes successfully,
@@ -66,22 +72,22 @@ use the --namespace flag:
 
 
   %s
   %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)
-		}
-	},
-}
+			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 := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, waitForJob)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var runJobCmd = &cobra.Command{
-	Use:   "run",
-	Short: "Manually runs a job and waits for it to complete.",
-	Long: fmt.Sprintf(`
+	runJobCmd := &cobra.Command{
+		Use:   "run",
+		Short: "Manually runs a job and waits for it to complete.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Manually runs a job and waits for it to complete a run. If the job completes successfully,
 Manually runs a job and waits for it to complete a run. If the job completes successfully,
@@ -96,22 +102,18 @@ use the --namespace flag:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job run\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter job run --name job-example"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter job run --name job-example --namespace custom-namespace"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runJob)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var imageRepoURI string
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job run\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter job run --name job-example"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter job run --name job-example --namespace custom-namespace"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runJob)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-func init() {
-	rootCmd.AddCommand(jobCmd)
 	jobCmd.AddCommand(batchImageUpdateCmd)
 	jobCmd.AddCommand(batchImageUpdateCmd)
 	jobCmd.AddCommand(waitCmd)
 	jobCmd.AddCommand(waitCmd)
 	jobCmd.AddCommand(runJobCmd)
 	jobCmd.AddCommand(runJobCmd)
@@ -172,13 +174,27 @@ func init() {
 	)
 	)
 
 
 	runJobCmd.MarkPersistentFlagRequired("name")
 	runJobCmd.MarkPersistentFlagRequired("name")
+	return jobCmd
 }
 }
 
 
-func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func batchImageUpdate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.BatchImageUpdate(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	color.New(color.FgGreen).Println("Updating all jobs which use the image:", imageRepoURI)
 	color.New(color.FgGreen).Println("Updating all jobs which use the image:", imageRepoURI)
 
 
 	return client.UpdateBatchImage(
 	return client.UpdateBatchImage(
-		context.TODO(),
+		ctx,
 		cliConf.Project,
 		cliConf.Project,
 		cliConf.Cluster,
 		cliConf.Cluster,
 		namespace,
 		namespace,
@@ -190,8 +206,21 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 }
 }
 
 
 // waits for a job with a given name/namespace
 // waits for a job with a given name/namespace
-func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	return wait.WaitForJob(client, &wait.WaitOpts{
+func waitForJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.WaitForJob(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	return wait.WaitForJob(ctx, client, &wait.WaitOpts{
 		ProjectID: cliConf.Project,
 		ProjectID: cliConf.Project,
 		ClusterID: cliConf.Cluster,
 		ClusterID: cliConf.Cluster,
 		Namespace: namespace,
 		Namespace: namespace,
@@ -199,7 +228,20 @@ func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	})
 	})
 }
 }
 
 
-func runJob(authRes *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func runJob(ctx context.Context, authRes *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.RunJob(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	color.New(color.FgGreen).Printf("Running job %s in namespace %s\n", name, namespace)
 	color.New(color.FgGreen).Printf("Running job %s in namespace %s\n", name, namespace)
 
 
 	waitForSuccessfulDeploy = true
 	waitForSuccessfulDeploy = true
@@ -216,14 +258,16 @@ func runJob(authRes *types.GetAuthenticatedUserResponse, client *api.Client, arg
 		},
 		},
 	}
 	}
 
 
-	err := updateAgent.UpdateImageAndValues(map[string]interface{}{
-		"paused": false,
-	})
+	err = updateAgent.UpdateImageAndValues(
+		ctx,
+		map[string]interface{}{
+			"paused": false,
+		})
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error running job: %w", err)
 		return fmt.Errorf("error running job: %w", err)
 	}
 	}
 
 
-	err = waitForJob(authRes, client, args)
+	err = waitForJob(ctx, authRes, client, cliConf, args)
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error waiting for job to complete: %w", err)
 		return fmt.Errorf("error waiting for job to complete: %w", err)

+ 18 - 18
cli/cmd/kubectl.go → cli/cmd/commands/kubectl.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -8,31 +8,31 @@ import (
 
 
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-var kubectlCmd = &cobra.Command{
-	Use:   "kubectl",
-	Short: "Use kubectl to interact with a Porter cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runKubectl)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(kubectlCmd)
+func registerCommand_Kubectl(cliConf config.CLIConfig) *cobra.Command {
+	kubectlCmd := &cobra.Command{
+		Use:   "kubectl",
+		Short: "Use kubectl to interact with a Porter cluster",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runKubectl)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	return kubectlCmd
 }
 }
 
 
-func runKubectl(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func runKubectl(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	_, err := exec.LookPath("kubectl")
 	_, err := exec.LookPath("kubectl")
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error finding kubectl: %w", err)
 		return fmt.Errorf("error finding kubectl: %w", err)
 	}
 	}
 
 
-	tmpFile, err := downloadTempKubeconfig(client)
+	tmpFile, err := downloadTempKubeconfig(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -57,7 +57,7 @@ func runKubectl(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	return nil
 	return nil
 }
 }
 
 
-func downloadTempKubeconfig(client *api.Client) (string, error) {
+func downloadTempKubeconfig(ctx context.Context, client api.Client, cliConf config.CLIConfig) (string, error) {
 	tmpFile, err := os.CreateTemp("", "porter_kubeconfig_*.yaml")
 	tmpFile, err := os.CreateTemp("", "porter_kubeconfig_*.yaml")
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("error creating temp file for kubeconfig: %w", err)
 		return "", fmt.Errorf("error creating temp file for kubeconfig: %w", err)
@@ -65,7 +65,7 @@ func downloadTempKubeconfig(client *api.Client) (string, error) {
 
 
 	defer tmpFile.Close()
 	defer tmpFile.Close()
 
 
-	resp, err := client.GetKubeconfig(context.Background(), cliConf.Project, cliConf.Cluster, cliConf.Kubeconfig)
+	resp, err := client.GetKubeconfig(ctx, cliConf.Project, cliConf.Cluster, cliConf.Kubeconfig)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("error fetching kubeconfig for cluster: %w", err)
 		return "", fmt.Errorf("error fetching kubeconfig for cluster: %w", err)
 	}
 	}

+ 235 - 0
cli/cmd/commands/list.go

@@ -0,0 +1,235 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"text/tabwriter"
+
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/spf13/cobra"
+	"github.com/stefanmcshane/helm/pkg/release"
+)
+
+var allNamespaces bool
+
+func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
+	listCmd := &cobra.Command{
+		Use:   "list",
+		Short: "List applications, addons or jobs.",
+		Run: func(cmd *cobra.Command, args []string) {
+			if len(args) == 0 || (args[0] == "all") {
+				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAll)
+				if err != nil {
+					os.Exit(1)
+				}
+			} else {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "invalid command: %s\n", args[0])
+			}
+		},
+	}
+
+	listAppsCmd := &cobra.Command{
+		Use:     "apps",
+		Aliases: []string{"applications", "app", "application"},
+		Short:   "Lists applications in a specific namespace, or across all namespaces",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listApps)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	listJobsCmd := &cobra.Command{
+		Use:     "jobs",
+		Aliases: []string{"job"},
+		Short:   "Lists jobs in a specific namespace, or across all namespaces",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listJobs)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	listAddonsCmd := &cobra.Command{
+		Use:     "addons",
+		Aliases: []string{"addon"},
+		Short:   "Lists addons in a specific namespace, or across all namespaces",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAddons)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	listCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"default",
+		"the namespace of the release",
+	)
+
+	listCmd.PersistentFlags().BoolVar(
+		&allNamespaces,
+		"all-namespaces",
+		false,
+		"list resources for all namespaces",
+	)
+
+	listCmd.AddCommand(listAppsCmd)
+	listCmd.AddCommand(listJobsCmd)
+	listCmd.AddCommand(listAddonsCmd)
+
+	return listCmd
+}
+
+func listAll(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.ListAll(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	err = writeReleases(ctx, client, cliConf, "all")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func listApps(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.ListApps(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	err = writeReleases(ctx, client, cliConf, "application")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func listJobs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.ListJobs(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	err = writeReleases(ctx, client, cliConf, "job")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func listAddons(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	err := writeReleases(ctx, client, cliConf, "addon")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func writeReleases(ctx context.Context, client api.Client, cliConf config.CLIConfig, kind string) error {
+	var namespaces []string
+	var releases []*release.Release
+
+	if allNamespaces {
+		resp, err := client.GetK8sNamespaces(ctx, cliConf.Project, cliConf.Cluster)
+		if err != nil {
+			return err
+		}
+
+		namespaceResp := *resp
+
+		for _, ns := range namespaceResp {
+			namespaces = append(namespaces, ns.Name)
+		}
+	} else {
+		namespaces = append(namespaces, namespace)
+	}
+
+	for _, ns := range namespaces {
+		resp, err := client.ListReleases(ctx, cliConf.Project, cliConf.Cluster, ns,
+			&types.ListReleasesRequest{
+				ReleaseListFilter: &types.ReleaseListFilter{
+					Limit: 50,
+					Skip:  0,
+					StatusFilter: []string{
+						"deployed",
+						"uninstalled",
+						"pending",
+						"pending-install",
+						"pending-upgrade",
+						"pending-rollback",
+						"failed",
+					},
+				},
+			},
+		)
+		if err != nil {
+			return err
+		}
+
+		releases = append(releases, resp...)
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 2, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "NAME", "NAMESPACE", "STATUS", "KIND")
+
+	for _, rel := range releases {
+		chartName := rel.Chart.Name()
+
+		if kind == "all" {
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
+		} else if kind == "application" && (chartName == "web" || chartName == "worker") {
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
+		} else if kind == "job" && chartName == "job" {
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
+		} else if kind == "addon" && chartName != "web" && chartName != "worker" && chartName != "job" {
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}

+ 22 - 22
cli/cmd/logs.go → cli/cmd/commands/logs.go

@@ -1,33 +1,31 @@
-package cmd
+package commands
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 
 
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-// logsCmd represents the "porter logs" base command when called
-// without any subcommands
-var logsCmd = &cobra.Command{
-	Use:   "logs [release]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Logs the output from a given application.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, logs)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
 var follow bool
 var follow bool
 
 
-func init() {
-	rootCmd.AddCommand(logsCmd)
+func registerCommand_Logs(cliConf config.CLIConfig) *cobra.Command {
+	logsCmd := &cobra.Command{
+		Use:   "logs [release]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Logs the output from a given application.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, logs)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	logsCmd.PersistentFlags().StringVar(
 	logsCmd.PersistentFlags().StringVar(
 		&namespace,
 		&namespace,
@@ -43,10 +41,11 @@ func init() {
 		false,
 		false,
 		"specify if the logs should be streamed",
 		"specify if the logs should be streamed",
 	)
 	)
+	return logsCmd
 }
 }
 
 
-func logs(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	podsSimple, err := getPods(client, namespace, args[0])
+func logs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+	podsSimple, err := getPods(ctx, client, cliConfig, namespace, args[0])
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 	}
 	}
@@ -95,16 +94,17 @@ func logs(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []stri
 	}
 	}
 
 
 	config := &PorterRunSharedConfig{
 	config := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
 
 
-	_, err = pipePodLogsToStdout(config, namespace, selectedPod.Name, selectedContainerName, follow)
+	_, err = pipePodLogsToStdout(ctx, config, namespace, selectedPod.Name, selectedContainerName, follow)
 
 
 	return err
 	return err
 }
 }

+ 43 - 0
cli/cmd/commands/open.go

@@ -0,0 +1,43 @@
+package commands
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Open(cliConf config.CLIConfig) *cobra.Command {
+	openCmd := &cobra.Command{
+		Use:   "open",
+		Short: "Opens the browser at the currently set Porter instance",
+		Run: func(cmd *cobra.Command, args []string) {
+			ctx := cmd.Context()
+
+			client, err := api.NewClientWithConfig(ctx, api.NewClientInput{
+				BaseURL:        fmt.Sprintf("%s/api", cliConf.Host),
+				BearerToken:    cliConf.Token,
+				CookieFileName: "cookie.json",
+			})
+			if err != nil {
+				_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "error creating porter API client: %v\n", err)
+				os.Exit(1)
+			}
+
+			user, err := client.AuthCheck(ctx)
+			if err != nil {
+				_ = utils.OpenBrowser(fmt.Sprintf("%s/register", cliConf.Host))
+				return
+			}
+
+			_ = utils.OpenBrowser(fmt.Sprintf("%s/login?email=%s", cliConf.Host, user.Email)) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
+		},
+	}
+
+	return openCmd
+}

+ 21 - 0
cli/cmd/commands/portforward.go

@@ -0,0 +1,21 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_PortForward(_ config.CLIConfig) *cobra.Command {
+	portForwardCmd := &cobra.Command{
+		Use: "port-forward [release] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]",
+		Deprecated: fmt.Sprintf("please use the %s command instead.",
+			color.New(color.FgYellow, color.Bold).Sprintf("porter kubectl -- port-forward"),
+		),
+		DisableFlagParsing: true,
+	}
+
+	return portForwardCmd
+}

+ 151 - 0
cli/cmd/commands/project.go

@@ -0,0 +1,151 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Project(cliConf config.CLIConfig) *cobra.Command {
+	projectCmd := &cobra.Command{
+		Use:     "project",
+		Aliases: []string{"projects"},
+		Short:   "Commands that control Porter project settings",
+	}
+
+	createProjectCmd := &cobra.Command{
+		Use:   "create [name]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Creates a project with the authorized user as admin",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, createProject)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	projectCmd.AddCommand(createProjectCmd)
+
+	deleteProjectCmd := &cobra.Command{
+		Use:   "delete [id]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Deletes the project with the given id",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteProject)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	projectCmd.AddCommand(deleteProjectCmd)
+
+	listProjectCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the projects for the logged in user",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listProjects)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+	projectCmd.AddCommand(listProjectCmd)
+
+	return projectCmd
+}
+
+func createProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	resp, err := client.CreateProject(ctx, &types.CreateProjectRequest{
+		Name: args[0],
+	})
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Printf("Created project with name %s and id %d\n", args[0], resp.ID)
+
+	return cliConf.SetProject(ctx, client, resp.ID)
+}
+
+func listProjects(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	resp, err := client.ListUserProjects(ctx)
+	if err != nil {
+		return err
+	}
+
+	projects := *resp
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\n", "ID", "NAME")
+
+	currProjectID := cliConf.Project
+
+	for _, project := range projects {
+		if currProjectID == project.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s (current project)\n", project.ID, project.Name)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\n", project.ID, project.Name)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func deleteProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the project with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+		if err != nil {
+			return err
+		}
+
+		err = client.DeleteProject(ctx, uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted project with id %d\n", id)
+	}
+
+	return nil
+}
+
+func setProjectCluster(ctx context.Context, client api.Client, cliConf config.CLIConfig, projectID uint) error {
+	resp, err := client.ListProjectClusters(ctx, projectID)
+	if err != nil {
+		return err
+	}
+
+	clusters := *resp
+
+	if len(clusters) > 0 {
+		cliConf.SetCluster(clusters[0].ID)
+	}
+
+	return nil
+}

+ 70 - 71
cli/cmd/registry.go → cli/cmd/commands/registry.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -11,78 +11,75 @@ import (
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-// registryCmd represents the "porter registry" base command when called
-// without any subcommands
-var registryCmd = &cobra.Command{
-	Use:     "registry",
-	Aliases: []string{"registries"},
-	Short:   "Commands that read from a connected registry",
-}
-
-var registryListCmd = &cobra.Command{
-	Use:   "list",
-	Short: "Lists the registries linked to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listRegistries)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
+	registryCmd := &cobra.Command{
+		Use:     "registry",
+		Aliases: []string{"registries"},
+		Short:   "Commands that read from a connected registry",
+	}
 
 
-var registryDeleteCmd = &cobra.Command{
-	Use:   "delete [id]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Deletes the registry with the given id",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteRegistry)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	registryListCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the registries linked to a project",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listRegistries)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var registryReposCmd = &cobra.Command{
-	Use:     "repo",
-	Aliases: []string{"repos", "repository", "repositories"},
-	Short:   "Commands that perform operations on image registry repositories",
-}
+	registryDeleteCmd := &cobra.Command{
+		Use:   "delete [id]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Deletes the registry with the given id",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteRegistry)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var registryReposListCmd = &cobra.Command{
-	Use:   "list",
-	Short: "Lists the repositories in an image registry",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listRepos)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	registryReposCmd := &cobra.Command{
+		Use:     "repo",
+		Aliases: []string{"repos", "repository", "repositories"},
+		Short:   "Commands that perform operations on image registry repositories",
+	}
 
 
-var registryImageCmd = &cobra.Command{
-	Use:     "image",
-	Aliases: []string{"images"},
-	Short:   "Commands that perform operations on image in a repository",
-}
+	registryReposListCmd := &cobra.Command{
+		Use:   "list",
+		Short: "Lists the repositories in an image registry",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listRepos)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var registryImageListCmd = &cobra.Command{
-	Use:   "list [repo_name]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Lists the images the specified image repository",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listImages)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	registryImageCmd := &cobra.Command{
+		Use:     "image",
+		Aliases: []string{"images"},
+		Short:   "Commands that perform operations on image in a repository",
+	}
 
 
-func init() {
-	rootCmd.AddCommand(registryCmd)
+	registryImageListCmd := &cobra.Command{
+		Use:   "list [repo_name]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Lists the images the specified image repository",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listImages)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	registryCmd.PersistentFlags().AddFlagSet(utils.RegistryFlagSet)
 	registryCmd.PersistentFlags().AddFlagSet(utils.RegistryFlagSet)
 
 
@@ -94,14 +91,16 @@ func init() {
 
 
 	registryCmd.AddCommand(registryImageCmd)
 	registryCmd.AddCommand(registryImageCmd)
 	registryImageCmd.AddCommand(registryImageListCmd)
 	registryImageCmd.AddCommand(registryImageListCmd)
+
+	return registryCmd
 }
 }
 
 
-func listRegistries(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func listRegistries(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	pID := cliConf.Project
 	pID := cliConf.Project
 
 
 	// get the list of namespaces
 	// get the list of namespaces
 	resp, err := client.ListRegistries(
 	resp, err := client.ListRegistries(
-		context.Background(),
+		ctx,
 		pID,
 		pID,
 	)
 	)
 	if err != nil {
 	if err != nil {
@@ -130,7 +129,7 @@ func listRegistries(user *types.GetAuthenticatedUserResponse, client *api.Client
 	return nil
 	return nil
 }
 }
 
 
-func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func deleteRegistry(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	userResp, err := utils.PromptPlaintext(
 	userResp, err := utils.PromptPlaintext(
 		fmt.Sprintf(
 		fmt.Sprintf(
 			`Are you sure you'd like to delete the registry with id %s? %s `,
 			`Are you sure you'd like to delete the registry with id %s? %s `,
@@ -148,7 +147,7 @@ func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client
 			return err
 			return err
 		}
 		}
 
 
-		err = client.DeleteProjectRegistry(context.Background(), cliConf.Project, uint(id))
+		err = client.DeleteProjectRegistry(ctx, cliConf.Project, uint(id))
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -160,13 +159,13 @@ func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client
 	return nil
 	return nil
 }
 }
 
 
-func listRepos(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func listRepos(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	pID := cliConf.Project
 	pID := cliConf.Project
 	rID := cliConf.Registry
 	rID := cliConf.Registry
 
 
 	// get the list of namespaces
 	// get the list of namespaces
 	resp, err := client.ListRegistryRepositories(
 	resp, err := client.ListRegistryRepositories(
-		context.Background(),
+		ctx,
 		pID,
 		pID,
 		rID,
 		rID,
 	)
 	)
@@ -190,14 +189,14 @@ func listRepos(user *types.GetAuthenticatedUserResponse, client *api.Client, arg
 	return nil
 	return nil
 }
 }
 
 
-func listImages(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func listImages(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	pID := cliConf.Project
 	pID := cliConf.Project
 	rID := cliConf.Registry
 	rID := cliConf.Registry
 	repoName := args[0]
 	repoName := args[0]
 
 
 	// get the list of namespaces
 	// get the list of namespaces
 	resp, err := client.ListImages(
 	resp, err := client.ListImages(
-		context.Background(),
+		ctx,
 		pID,
 		pID,
 		rID,
 		rID,
 		repoName,
 		repoName,

+ 12 - 23
cli/cmd/root.go → cli/cmd/commands/root.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -11,37 +11,24 @@ import (
 	"github.com/Masterminds/semver/v3"
 	"github.com/Masterminds/semver/v3"
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	"github.com/google/go-github/v41/github"
 	"github.com/google/go-github/v41/github"
-	"github.com/porter-dev/porter/cli/cmd/config"
-	"github.com/porter-dev/porter/cli/cmd/utils"
-	"github.com/spf13/cobra"
+	cfg "github.com/porter-dev/porter/cli/cmd/config"
 	"k8s.io/client-go/util/homedir"
 	"k8s.io/client-go/util/homedir"
 )
 )
 
 
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "porter",
-	Short: "Porter is a dashboard for managing Kubernetes clusters.",
-	Long:  `Porter is a tool for creating, versioning, and updating Kubernetes deployments using a visual dashboard. For more information, visit github.com/porter-dev/porter`,
-}
-
 var home = homedir.HomeDir()
 var home = homedir.HomeDir()
 
 
 // Execute adds all child commands to the root command and sets flags appropriately.
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	Setup()
-
-	rootCmd.PersistentFlags().AddFlagSet(utils.DefaultFlagSet)
-
-	if config.Version != "dev" {
+func Execute(ctx context.Context) error {
+	if cfg.Version != "dev" {
 		ghClient := github.NewClient(nil)
 		ghClient := github.NewClient(nil)
-		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
 		defer cancel()
 		defer cancel()
 		release, _, err := ghClient.Repositories.GetLatestRelease(ctx, "porter-dev", "porter")
 		release, _, err := ghClient.Repositories.GetLatestRelease(ctx, "porter-dev", "porter")
 		if err == nil {
 		if err == nil {
 			release.GetURL()
 			release.GetURL()
 			// we do not care for an error here because we do not want to block the user here
 			// we do not care for an error here because we do not want to block the user here
-			constraint, err := semver.NewConstraint(fmt.Sprintf("> %s", strings.TrimPrefix(config.Version, "v")))
+			constraint, err := semver.NewConstraint(fmt.Sprintf("> %s", strings.TrimPrefix(cfg.Version, "v")))
 			if err == nil {
 			if err == nil {
 				latestRelease, err := semver.NewVersion(strings.TrimPrefix(release.GetTagName(), "v"))
 				latestRelease, err := semver.NewVersion(strings.TrimPrefix(release.GetTagName(), "v"))
 				if err == nil {
 				if err == nil {
@@ -59,12 +46,14 @@ func Execute() {
 		}
 		}
 	}
 	}
 
 
+	rootCmd, err := RegisterCommands()
+	if err != nil {
+		return fmt.Errorf("error setting up commands")
+	}
+
 	if err := rootCmd.Execute(); err != nil {
 	if err := rootCmd.Execute(); err != nil {
 		color.New(color.FgRed).Println(err)
 		color.New(color.FgRed).Println(err)
 		os.Exit(1)
 		os.Exit(1)
 	}
 	}
-}
-
-func Setup() {
-	config.InitAndLoadConfig()
+	return nil
 }
 }

+ 101 - 99
cli/cmd/run.go → cli/cmd/commands/run.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -12,6 +12,7 @@ import (
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1 "k8s.io/api/batch/v1"
@@ -41,35 +42,31 @@ var (
 	memoryMi       int
 	memoryMi       int
 )
 )
 
 
-// runCmd represents the "porter run" base command when called
-// without any subcommands
-var runCmd = &cobra.Command{
-	Use:   "run [release] -- COMMAND [args...]",
-	Args:  cobra.MinimumNArgs(2),
-	Short: "Runs a command inside a connected cluster container.",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, run)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-// cleanupCmd represents the "porter run cleanup" subcommand
-var cleanupCmd = &cobra.Command{
-	Use:   "cleanup",
-	Args:  cobra.NoArgs,
-	Short: "Delete any lingering ephemeral pods that were created with \"porter run\".",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, cleanup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+func registerCommand_Run(cliConf config.CLIConfig) *cobra.Command {
+	runCmd := &cobra.Command{
+		Use:   "run [release] -- COMMAND [args...]",
+		Args:  cobra.MinimumNArgs(2),
+		Short: "Runs a command inside a connected cluster container.",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, run)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-func init() {
-	rootCmd.AddCommand(runCmd)
+	// cleanupCmd represents the "porter run cleanup" subcommand
+	cleanupCmd := &cobra.Command{
+		Use:   "cleanup",
+		Args:  cobra.NoArgs,
+		Short: "Delete any lingering ephemeral pods that were created with \"porter run\".",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, cleanup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	runCmd.PersistentFlags().StringVar(
 	runCmd.PersistentFlags().StringVar(
 		&namespace,
 		&namespace,
@@ -126,9 +123,10 @@ func init() {
 	)
 	)
 
 
 	runCmd.AddCommand(cleanupCmd)
 	runCmd.AddCommand(cleanupCmd)
+	return runCmd
 }
 }
 
 
-func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func run(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	execArgs := args[1:]
 	execArgs := args[1:]
 
 
 	color.New(color.FgGreen).Println("Running", strings.Join(execArgs, " "), "for release", args[0])
 	color.New(color.FgGreen).Println("Running", strings.Join(execArgs, " "), "for release", args[0])
@@ -139,7 +137,7 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 
 
 	if len(execArgs) > 0 {
 	if len(execArgs) > 0 {
 		release, err := client.GetRelease(
 		release, err := client.GetRelease(
-			context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0],
+			ctx, cliConf.Project, cliConf.Cluster, namespace, args[0],
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("error fetching release %s: %w", args[0], err)
 			return fmt.Errorf("error fetching release %s: %w", args[0], err)
@@ -155,7 +153,7 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 		}
 		}
 	}
 	}
 
 
-	podsSimple, err := getPods(client, namespace, args[0])
+	podsSimple, err := getPods(ctx, client, cliConf, namespace, args[0])
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
 	}
 	}
@@ -227,10 +225,11 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 	}
 	}
 
 
 	config := &PorterRunSharedConfig{
 	config := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConf,
 	}
 	}
 
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
@@ -240,15 +239,16 @@ func run(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 	}
 	}
 
 
-	return executeRunEphemeral(config, namespace, selectedPod.Name, selectedContainerName, execArgs)
+	return executeRunEphemeral(ctx, config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 }
 }
 
 
-func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
+func cleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) error {
 	config := &PorterRunSharedConfig{
 	config := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err := config.setSharedConfig()
+	err := config.setSharedConfig(ctx)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
@@ -270,20 +270,20 @@ func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []stri
 	color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
 	color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
 
 
 	if proceed == "All namespaces" {
 	if proceed == "All namespaces" {
-		namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+		namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		for _, namespace := range namespaces.Items {
 		for _, namespace := range namespaces.Items {
-			if pods, err := getEphemeralPods(namespace.Name, config.Clientset); err == nil {
+			if pods, err := getEphemeralPods(ctx, namespace.Name, config.Clientset); err == nil {
 				podNames = append(podNames, pods...)
 				podNames = append(podNames, pods...)
 			} else {
 			} else {
 				return err
 				return err
 			}
 			}
 		}
 		}
 	} else {
 	} else {
-		if pods, err := getEphemeralPods(namespace, config.Clientset); err == nil {
+		if pods, err := getEphemeralPods(ctx, namespace, config.Clientset); err == nil {
 			podNames = append(podNames, pods...)
 			podNames = append(podNames, pods...)
 		} else {
 		} else {
 			return err
 			return err
@@ -304,7 +304,7 @@ func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []stri
 		color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
 		color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
 
 
 		err = config.Clientset.CoreV1().Pods(namespace).Delete(
 		err = config.Clientset.CoreV1().Pods(namespace).Delete(
-			context.Background(), podName, metav1.DeleteOptions{},
+			ctx, podName, metav1.DeleteOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -314,11 +314,11 @@ func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []stri
 	return nil
 	return nil
 }
 }
 
 
-func getEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]string, error) {
+func getEphemeralPods(ctx context.Context, namespace string, clientset *kubernetes.Clientset) ([]string, error) {
 	var podNames []string
 	var podNames []string
 
 
 	pods, err := clientset.CoreV1().Pods(namespace).List(
 	pods, err := clientset.CoreV1().Pods(namespace).List(
-		context.Background(), metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
+		ctx, metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -332,17 +332,18 @@ func getEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]stri
 }
 }
 
 
 type PorterRunSharedConfig struct {
 type PorterRunSharedConfig struct {
-	Client     *api.Client
+	Client     api.Client
 	RestConf   *rest.Config
 	RestConf   *rest.Config
 	Clientset  *kubernetes.Clientset
 	Clientset  *kubernetes.Clientset
 	RestClient *rest.RESTClient
 	RestClient *rest.RESTClient
+	CLIConfig  config.CLIConfig
 }
 }
 
 
-func (p *PorterRunSharedConfig) setSharedConfig() error {
-	pID := cliConf.Project
-	cID := cliConf.Cluster
+func (p *PorterRunSharedConfig) setSharedConfig(ctx context.Context) error {
+	pID := p.CLIConfig.Project
+	cID := p.CLIConfig.Cluster
 
 
-	kubeResp, err := p.Client.GetKubeconfig(context.Background(), pID, cID, cliConf.Kubeconfig)
+	kubeResp, err := p.Client.GetKubeconfig(ctx, pID, cID, p.CLIConfig.Kubeconfig)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -390,11 +391,11 @@ type podSimple struct {
 	ContainerNames []string
 	ContainerNames []string
 }
 }
 
 
-func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, error) {
+func getPods(ctx context.Context, client api.Client, cliConf config.CLIConfig, namespace, releaseName string) ([]podSimple, error) {
 	pID := cliConf.Project
 	pID := cliConf.Project
 	cID := cliConf.Cluster
 	cID := cliConf.Cluster
 
 
-	resp, err := client.GetK8sAllPods(context.TODO(), pID, cID, namespace, releaseName)
+	resp, err := client.GetK8sAllPods(ctx, pID, cID, namespace, releaseName)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -461,28 +462,28 @@ func executeRun(config *PorterRunSharedConfig, namespace, name, container string
 	})
 	})
 }
 }
 
 
-func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
-	existing, err := getExistingPod(config, name, namespace)
+func executeRunEphemeral(ctx context.Context, config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
+	existing, err := getExistingPod(ctx, config, name, namespace)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	newPod, err := createEphemeralPodFromExisting(config, existing, container, args)
+	newPod, err := createEphemeralPodFromExisting(ctx, config, existing, container, args)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	podName := newPod.ObjectMeta.Name
 	podName := newPod.ObjectMeta.Name
 
 
 	// delete the ephemeral pod no matter what
 	// delete the ephemeral pod no matter what
-	defer deletePod(config, podName, namespace)
+	defer deletePod(ctx, config, podName, namespace) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
 	color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
-	if err = waitForPod(config, newPod); err != nil {
+	if err = waitForPod(ctx, config, newPod); err != nil {
 		color.New(color.FgRed).Println("failed")
 		color.New(color.FgRed).Println("failed")
-		return handlePodAttachError(err, config, namespace, podName, container)
+		return handlePodAttachError(ctx, err, config, namespace, podName, container)
 	}
 	}
 
 
-	err = checkForPodDeletionCronJob(config)
+	err = checkForPodDeletionCronJob(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -490,7 +491,7 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 	// refresh pod info for latest status
 	// refresh pod info for latest status
 	newPod, err = config.Clientset.CoreV1().
 	newPod, err = config.Clientset.CoreV1().
 		Pods(newPod.Namespace).
 		Pods(newPod.Namespace).
-		Get(context.Background(), newPod.Name, metav1.GetOptions{})
+		Get(ctx, newPod.Name, metav1.GetOptions{})
 
 
 	// pod exited while we were waiting.  maybe an error maybe not.
 	// pod exited while we were waiting.  maybe an error maybe not.
 	// we dont know if the user wanted an interactive shell or not.
 	// we dont know if the user wanted an interactive shell or not.
@@ -498,11 +499,11 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 	if isPodExited(newPod) {
 	if isPodExited(newPod) {
 		color.New(color.FgGreen).Println("complete!")
 		color.New(color.FgGreen).Println("complete!")
 		var writtenBytes int64
 		var writtenBytes int64
-		writtenBytes, _ = pipePodLogsToStdout(config, namespace, podName, container, false)
+		writtenBytes, _ = pipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 
 		if verbose || writtenBytes == 0 {
 		if verbose || writtenBytes == 0 {
 			color.New(color.FgYellow).Println("Could not get logs. Pod events:")
 			color.New(color.FgYellow).Println("Could not get logs. Pod events:")
-			pipeEventsToStdout(config, namespace, podName, container, false)
+			pipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 		}
 		}
 		return nil
 		return nil
 	}
 	}
@@ -543,44 +544,44 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 		})
 		})
 	}); err != nil {
 	}); err != nil {
 		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
 		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
-		return handlePodAttachError(err, config, namespace, podName, container)
+		return handlePodAttachError(ctx, err, config, namespace, podName, container)
 	}
 	}
 
 
 	if verbose {
 	if verbose {
 		color.New(color.FgYellow).Println("Pod events:")
 		color.New(color.FgYellow).Println("Pod events:")
-		pipeEventsToStdout(config, namespace, podName, container, false)
+		pipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 
 
 	return err
 	return err
 }
 }
 
 
-func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
+func checkForPodDeletionCronJob(ctx context.Context, config *PorterRunSharedConfig) error {
 	// try and create the cron job and all of the other required resources as necessary,
 	// try and create the cron job and all of the other required resources as necessary,
 	// starting with the service account, then role and then a role binding
 	// starting with the service account, then role and then a role binding
 
 
-	err := checkForServiceAccount(config)
+	err := checkForServiceAccount(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = checkForClusterRole(config)
+	err = checkForClusterRole(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = checkForRoleBinding(config)
+	err = checkForRoleBinding(ctx, config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+	namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	for _, namespace := range namespaces.Items {
 	for _, namespace := range namespaces.Items {
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -596,7 +597,7 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 			for _, cronJob := range cronJobs.Items {
 			for _, cronJob := range cronJobs.Items {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
-						context.Background(), cronJob.Name, metav1.DeleteOptions{},
+						ctx, cronJob.Name, metav1.DeleteOptions{},
 					)
 					)
 					if err != nil {
 					if err != nil {
 						return err
 						return err
@@ -635,7 +636,7 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
-		context.Background(), cronJob, metav1.CreateOptions{},
+		ctx, cronJob, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -644,15 +645,15 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func checkForServiceAccount(config *PorterRunSharedConfig) error {
-	namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
+func checkForServiceAccount(ctx context.Context, config *PorterRunSharedConfig) error {
+	namespaces, err := config.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	for _, namespace := range namespaces.Items {
 	for _, namespace := range namespaces.Items {
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -668,7 +669,7 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 			for _, svcAccount := range serviceAccounts.Items {
 			for _, svcAccount := range serviceAccounts.Items {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
-						context.Background(), svcAccount.Name, metav1.DeleteOptions{},
+						ctx, svcAccount.Name, metav1.DeleteOptions{},
 					)
 					)
 					if err != nil {
 					if err != nil {
 						return err
 						return err
@@ -684,7 +685,7 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
-		context.Background(), serviceAccount, metav1.CreateOptions{},
+		ctx, serviceAccount, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -693,9 +694,9 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func checkForClusterRole(config *PorterRunSharedConfig) error {
+func checkForClusterRole(ctx context.Context, config *PorterRunSharedConfig) error {
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -725,7 +726,7 @@ func checkForClusterRole(config *PorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
-		context.Background(), role, metav1.CreateOptions{},
+		ctx, role, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -734,9 +735,9 @@ func checkForClusterRole(config *PorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func checkForRoleBinding(config *PorterRunSharedConfig) error {
+func checkForRoleBinding(ctx context.Context, config *PorterRunSharedConfig) error {
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -767,7 +768,7 @@ func checkForRoleBinding(config *PorterRunSharedConfig) error {
 		},
 		},
 	}
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
-		context.Background(), binding, metav1.CreateOptions{},
+		ctx, binding, metav1.CreateOptions{},
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -776,7 +777,7 @@ func checkForRoleBinding(config *PorterRunSharedConfig) error {
 	return nil
 	return nil
 }
 }
 
 
-func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
+func waitForPod(ctx context.Context, config *PorterRunSharedConfig, pod *v1.Pod) error {
 	var (
 	var (
 		w   watch.Interface
 		w   watch.Interface
 		err error
 		err error
@@ -789,7 +790,7 @@ func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		w, err = config.Clientset.CoreV1().
 		w, err = config.Clientset.CoreV1().
 			Pods(pod.Namespace).
 			Pods(pod.Namespace).
-			Watch(context.Background(), metav1.ListOptions{FieldSelector: selector})
+			Watch(ctx, metav1.ListOptions{FieldSelector: selector})
 
 
 		if err == nil {
 		if err == nil {
 			break
 			break
@@ -807,7 +808,7 @@ func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
 			// creating the listener.
 			// creating the listener.
 			pod, err = config.Clientset.CoreV1().
 			pod, err = config.Clientset.CoreV1().
 				Pods(pod.Namespace).
 				Pods(pod.Namespace).
-				Get(context.Background(), pod.Name, metav1.GetOptions{})
+				Get(ctx, pod.Name, metav1.GetOptions{})
 			if isPodReady(pod) || isPodExited(pod) {
 			if isPodReady(pod) || isPodExited(pod) {
 				return nil
 				return nil
 			}
 			}
@@ -840,23 +841,23 @@ func isPodExited(pod *v1.Pod) bool {
 	return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
 	return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
 }
 }
 
 
-func handlePodAttachError(err error, config *PorterRunSharedConfig, namespace, podName, container string) error {
+func handlePodAttachError(ctx context.Context, err error, config *PorterRunSharedConfig, namespace, podName, container string) error {
 	if verbose {
 	if verbose {
 		color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
 		color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
 	}
 	}
 	color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
 	color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
 
 
 	var writtenBytes int64
 	var writtenBytes int64
-	writtenBytes, _ = pipePodLogsToStdout(config, namespace, podName, container, false)
+	writtenBytes, _ = pipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 
 	if verbose || writtenBytes == 0 {
 	if verbose || writtenBytes == 0 {
 		color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
 		color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
-		pipeEventsToStdout(config, namespace, podName, container, false)
+		pipeEventsToStdout(ctx, config, namespace, podName, container, false) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 	return err
 	return err
 }
 }
 
 
-func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
+func pipePodLogsToStdout(ctx context.Context, config *PorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
 	podLogOpts := v1.PodLogOptions{
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Container: container,
 		Follow:    follow,
 		Follow:    follow,
@@ -865,7 +866,7 @@ func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, contain
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 
 	podLogs, err := req.Stream(
 	podLogs, err := req.Stream(
-		context.Background(),
+		ctx,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return 0, err
 		return 0, err
@@ -876,13 +877,13 @@ func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, contain
 	return io.Copy(os.Stdout, podLogs)
 	return io.Copy(os.Stdout, podLogs)
 }
 }
 
 
-func pipeEventsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) error {
+func pipeEventsToStdout(ctx context.Context, config *PorterRunSharedConfig, namespace, name, _ string, _ bool) error {
 	// update the config in case the operation has taken longer than token expiry time
 	// update the config in case the operation has taken longer than token expiry time
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	// creates the clientset
 	// creates the clientset
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
-		context.TODO(),
+		ctx,
 		metav1.ListOptions{
 		metav1.ListOptions{
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 		},
 		},
@@ -898,20 +899,20 @@ func pipeEventsToStdout(config *PorterRunSharedConfig, namespace, name, containe
 	return nil
 	return nil
 }
 }
 
 
-func getExistingPod(config *PorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
+func getExistingPod(ctx context.Context, config *PorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
 	return config.Clientset.CoreV1().Pods(namespace).Get(
 	return config.Clientset.CoreV1().Pods(namespace).Get(
-		context.Background(),
+		ctx,
 		name,
 		name,
 		metav1.GetOptions{},
 		metav1.GetOptions{},
 	)
 	)
 }
 }
 
 
-func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
+func deletePod(ctx context.Context, config *PorterRunSharedConfig, name, namespace string) error {
 	// update the config in case the operation has taken longer than token expiry time
 	// update the config in case the operation has taken longer than token expiry time
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	err := config.Clientset.CoreV1().Pods(namespace).Delete(
 	err := config.Clientset.CoreV1().Pods(namespace).Delete(
-		context.Background(),
+		ctx,
 		name,
 		name,
 		metav1.DeleteOptions{},
 		metav1.DeleteOptions{},
 	)
 	)
@@ -926,6 +927,7 @@ func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
 }
 }
 
 
 func createEphemeralPodFromExisting(
 func createEphemeralPodFromExisting(
+	ctx context.Context,
 	config *PorterRunSharedConfig,
 	config *PorterRunSharedConfig,
 	existing *v1.Pod,
 	existing *v1.Pod,
 	container string,
 	container string,
@@ -1011,7 +1013,7 @@ func createEphemeralPodFromExisting(
 
 
 	// create the pod and return it
 	// create the pod and return it
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
-		context.Background(),
+		ctx,
 		newPod,
 		newPod,
 		metav1.CreateOptions{},
 		metav1.CreateOptions{},
 	)
 	)

+ 75 - 65
cli/cmd/server.go → cli/cmd/commands/server.go

@@ -1,6 +1,7 @@
-package cmd
+package commands
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
@@ -24,68 +25,72 @@ type startOps struct {
 
 
 var opts = &startOps{}
 var opts = &startOps{}
 
 
-var serverCmd = &cobra.Command{
-	Use:     "server",
-	Aliases: []string{"svr"},
-	Short:   "Commands to control a local Porter server",
-}
+func registerCommand_Server(cliConf config.CLIConfig) *cobra.Command {
+	serverCmd := &cobra.Command{
+		Use:     "server",
+		Aliases: []string{"svr"},
+		Short:   "Commands to control a local Porter server",
+	}
+
+	// startCmd represents the start command
+	startCmd := &cobra.Command{
+		Use:   "start",
+		Short: "Starts a Porter server instance on the host",
+		Run: func(cmd *cobra.Command, args []string) {
+			ctx := cmd.Context()
+
+			if cliConf.Driver == "docker" {
+				_ = cliConf.SetDriver("docker")
+
+				err := startDocker(
+					ctx,
+					cliConf,
+					opts.imageTag,
+					opts.db,
+					*opts.port,
+				)
+				if err != nil {
+					red := color.New(color.FgRed)
+					_, _ = red.Println("Error running start:", err.Error())
+					_, _ = red.Println("Shutting down...")
+
+					err = stopDocker(ctx)
 
 
-// startCmd represents the start command
-var startCmd = &cobra.Command{
-	Use:   "start",
-	Short: "Starts a Porter server instance on the host",
-	Run: func(cmd *cobra.Command, args []string) {
-		if cliConf.Driver == "docker" {
-			cliConf.SetDriver("docker")
-
-			err := startDocker(
-				opts.imageTag,
-				opts.db,
-				*opts.port,
-			)
-			if err != nil {
-				red := color.New(color.FgRed)
-				red.Println("Error running start:", err.Error())
-				red.Println("Shutting down...")
-
-				err = stopDocker()
+					if err != nil {
+						_, _ = red.Println("Shutdown unsuccessful:", err.Error())
+					}
 
 
+					os.Exit(1)
+				}
+			} else {
+				_ = cliConf.SetDriver("local")
+				err := startLocal(
+					ctx,
+					cliConf,
+					opts.db,
+					*opts.port,
+				)
 				if err != nil {
 				if err != nil {
-					red.Println("Shutdown unsuccessful:", err.Error())
+					red := color.New(color.FgRed)
+					_, _ = red.Println("Error running start:", err.Error())
+					os.Exit(1)
 				}
 				}
-
-				os.Exit(1)
 			}
 			}
-		} else {
-			cliConf.SetDriver("local")
-			err := startLocal(
-				opts.db,
-				*opts.port,
-			)
-			if err != nil {
-				red := color.New(color.FgRed)
-				red.Println("Error running start:", err.Error())
-				os.Exit(1)
-			}
-		}
-	},
-}
+		},
+	}
 
 
-var stopCmd = &cobra.Command{
-	Use:   "stop",
-	Short: "Stops a Porter instance running on the Docker engine",
-	Run: func(cmd *cobra.Command, args []string) {
-		if cliConf.Driver == "docker" {
-			if err := stopDocker(); err != nil {
-				color.New(color.FgRed).Println("Shutdown unsuccessful:", err.Error())
-				os.Exit(1)
+	stopCmd := &cobra.Command{
+		Use:   "stop",
+		Short: "Stops a Porter instance running on the Docker engine",
+		Run: func(cmd *cobra.Command, args []string) {
+			if cliConf.Driver == "docker" {
+				if err := stopDocker(cmd.Context()); err != nil {
+					_, _ = color.New(color.FgRed).Println("Shutdown unsuccessful:", err.Error())
+					os.Exit(1)
+				}
 			}
 			}
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(serverCmd)
+		},
+	}
 
 
 	serverCmd.AddCommand(startCmd)
 	serverCmd.AddCommand(startCmd)
 	serverCmd.AddCommand(stopCmd)
 	serverCmd.AddCommand(stopCmd)
@@ -112,9 +117,12 @@ func init() {
 		8080,
 		8080,
 		"the host port to run the server on",
 		"the host port to run the server on",
 	)
 	)
+	return serverCmd
 }
 }
 
 
 func startDocker(
 func startDocker(
+	ctx context.Context,
+	cliConf config.CLIConfig,
 	imageTag string,
 	imageTag string,
 	db string,
 	db string,
 	port int,
 	port int,
@@ -141,7 +149,7 @@ func startDocker(
 		Env:            env,
 		Env:            env,
 	}
 	}
 
 
-	_, _, err := docker.StartPorter(startOpts)
+	_, _, err := docker.StartPorter(ctx, startOpts)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -154,6 +162,8 @@ func startDocker(
 }
 }
 
 
 func startLocal(
 func startLocal(
+	ctx context.Context,
+	cliConf config.CLIConfig,
 	db string,
 	db string,
 	port int,
 	port int,
 ) error {
 ) error {
@@ -169,7 +179,7 @@ func startLocal(
 	staticFilePath := filepath.Join(home, ".porter", "static")
 	staticFilePath := filepath.Join(home, ".porter", "static")
 
 
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
-		err := downloadMatchingRelease(porterDir)
+		err := downloadMatchingRelease(ctx, porterDir)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			os.Exit(1)
 			os.Exit(1)
@@ -184,7 +194,7 @@ func startLocal(
 	err := cmdVersionPorter.Run()
 	err := cmdVersionPorter.Run()
 
 
 	if err != nil || writer.Version != config.Version {
 	if err != nil || writer.Version != config.Version {
-		err := downloadMatchingRelease(porterDir)
+		err := downloadMatchingRelease(ctx, porterDir)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			os.Exit(1)
 			os.Exit(1)
@@ -223,13 +233,13 @@ func startLocal(
 	return nil
 	return nil
 }
 }
 
 
-func stopDocker() error {
-	agent, err := docker.NewAgentFromEnv()
+func stopDocker(ctx context.Context) error {
+	agent, err := docker.NewAgentFromEnv(ctx)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = agent.StopPorterContainersWithProcessID("main", false)
+	err = agent.StopPorterContainersWithProcessID(ctx, "main", false)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -242,7 +252,7 @@ func stopDocker() error {
 	return nil
 	return nil
 }
 }
 
 
-func downloadMatchingRelease(porterDir string) error {
+func downloadMatchingRelease(ctx context.Context, porterDir string) error {
 	z := &github.ZIPReleaseGetter{
 	z := &github.ZIPReleaseGetter{
 		AssetName:           "portersvr",
 		AssetName:           "portersvr",
 		AssetFolderDest:     porterDir,
 		AssetFolderDest:     porterDir,
@@ -258,7 +268,7 @@ func downloadMatchingRelease(porterDir string) error {
 		},
 		},
 	}
 	}
 
 
-	err := z.GetRelease(config.Version)
+	err := z.GetRelease(ctx, config.Version)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -278,5 +288,5 @@ func downloadMatchingRelease(porterDir string) error {
 		},
 		},
 	}
 	}
 
 
-	return zStatic.GetRelease(config.Version)
+	return zStatic.GetRelease(ctx, config.Version)
 }
 }

+ 74 - 47
cli/cmd/stack.go → cli/cmd/commands/stack.go

@@ -1,10 +1,13 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 
 
+	"github.com/porter-dev/porter/cli/cmd/config"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -13,49 +16,45 @@ import (
 
 
 var linkedApps []string
 var linkedApps []string
 
 
-// stackCmd represents the "porter stack" base command when called
-// without any subcommands
-var stackCmd = &cobra.Command{
-	Use:     "stack",
-	Aliases: []string{"stacks"},
-	Short:   "Commands that control Porter Stacks",
-}
-
-var stackEnvGroupCmd = &cobra.Command{
-	Use:     "env-group",
-	Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
-	Short:   "Commands to add or remove an env group in a stack",
-	Run: func(cmd *cobra.Command, args []string) {
-		color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
-	},
-}
+func registerCommand_Stack(cliConf config.CLIConfig) *cobra.Command {
+	stackCmd := &cobra.Command{
+		Use:     "stack",
+		Aliases: []string{"stacks"},
+		Short:   "Commands that control Porter Stacks",
+	}
 
 
-var stackEnvGroupAddCmd = &cobra.Command{
-	Use:   "add [name]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Add an env group to a stack",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, stackAddEnvGroup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	stackEnvGroupCmd := &cobra.Command{
+		Use:     "env-group",
+		Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
+		Short:   "Commands to add or remove an env group in a stack",
+		Run: func(cmd *cobra.Command, args []string) {
+			_, _ = color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
+		},
+	}
 
 
-var stackEnvGroupRemoveCmd = &cobra.Command{
-	Use:   "remove [name]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Remove an existing env group from a stack",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, stackRemoveEnvGroup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	stackEnvGroupAddCmd := &cobra.Command{
+		Use:   "add [name]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Add an env group to a stack",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, stackAddEnvGroup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-func init() {
-	rootCmd.AddCommand(stackCmd)
+	stackEnvGroupRemoveCmd := &cobra.Command{
+		Use:   "remove [name]",
+		Args:  cobra.ExactArgs(1),
+		Short: "Remove an existing env group from a stack",
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, stackRemoveEnvGroup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	stackCmd.AddCommand(stackEnvGroupCmd)
 	stackCmd.AddCommand(stackEnvGroupCmd)
 
 
@@ -98,9 +97,24 @@ func init() {
 
 
 	stackEnvGroupCmd.AddCommand(stackEnvGroupAddCmd)
 	stackEnvGroupCmd.AddCommand(stackEnvGroupAddCmd)
 	stackEnvGroupCmd.AddCommand(stackEnvGroupRemoveCmd)
 	stackEnvGroupCmd.AddCommand(stackEnvGroupRemoveCmd)
+
+	return stackCmd
 }
 }
 
 
-func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func stackAddEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.StackAddEnvGroup(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	envGroupName := args[0]
 	envGroupName := args[0]
 
 
 	if len(envGroupName) == 0 {
 	if len(envGroupName) == 0 {
@@ -111,7 +125,7 @@ func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 		return fmt.Errorf("one or more variables are required to create the env group")
 		return fmt.Errorf("one or more variables are required to create the env group")
 	}
 	}
 
 
-	listStacks, err := client.ListStacks(context.Background(), cliConf.Project, cliConf.Cluster, namespace)
+	listStacks, err := client.ListStacks(ctx, cliConf.Project, cliConf.Cluster, namespace)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -152,7 +166,7 @@ func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	}
 	}
 
 
 	err = client.AddEnvGroupToStack(
 	err = client.AddEnvGroupToStack(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, stackID,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, stackID,
 		&types.CreateStackEnvGroupRequest{
 		&types.CreateStackEnvGroupRequest{
 			Name:               envGroupName,
 			Name:               envGroupName,
 			Variables:          normalVariables,
 			Variables:          normalVariables,
@@ -170,7 +184,20 @@ func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	return nil
 	return nil
 }
 }
 
 
-func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func stackRemoveEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.StackRemoveEnvGroup(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	envGroupName := args[0]
 	envGroupName := args[0]
 
 
 	if len(envGroupName) == 0 {
 	if len(envGroupName) == 0 {
@@ -179,7 +206,7 @@ func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 		return fmt.Errorf("empty stack name")
 		return fmt.Errorf("empty stack name")
 	}
 	}
 
 
-	listStacks, err := client.ListStacks(context.Background(), cliConf.Project, cliConf.Cluster, namespace)
+	listStacks, err := client.ListStacks(ctx, cliConf.Project, cliConf.Cluster, namespace)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -198,7 +225,7 @@ func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 		return fmt.Errorf("stack not found")
 		return fmt.Errorf("stack not found")
 	}
 	}
 
 
-	err = client.RemoveEnvGroupFromStack(context.Background(), cliConf.Project, cliConf.Cluster, namespace, stackID,
+	err = client.RemoveEnvGroupFromStack(ctx, cliConf.Project, cliConf.Cluster, namespace, stackID,
 		envGroupName)
 		envGroupName)
 
 
 	if err != nil {
 	if err != nil {

+ 248 - 207
cli/cmd/deploy.go → cli/cmd/commands/update.go

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 
 import (
 import (
 	"context"
 	"context"
@@ -9,6 +9,8 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/briandowns/spinner"
 	"github.com/briandowns/spinner"
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
@@ -23,12 +25,33 @@ import (
 	"k8s.io/client-go/util/homedir"
 	"k8s.io/client-go/util/homedir"
 )
 )
 
 
-// updateCmd represents the "porter update" base command when called
-// without any subcommands
-var updateCmd = &cobra.Command{
-	Use:   "update",
-	Short: "Builds and updates a specified application given by the --app flag.",
-	Long: fmt.Sprintf(`
+var (
+	app                     string
+	getEnvFileDest          string
+	localPath               string
+	tag                     string
+	dockerfile              string
+	method                  string
+	stream                  bool
+	buildFlagsEnv           []string
+	forcePush               bool
+	useCache                bool
+	version                 uint
+	varType                 string
+	normalEnvGroupVars      []string
+	secretEnvGroupVars      []string
+	waitForSuccessfulDeploy bool
+)
+
+func registerCommand_Update(cliConf config.CLIConfig) *cobra.Command {
+	buildFlagsEnv = []string{}
+
+	// updateCmd represents the "porter update" base command when called
+	// without any subcommands
+	updateCmd := &cobra.Command{
+		Use:   "update",
+		Short: "Builds and updates a specified application given by the --app flag.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Builds and updates a specified application given by the --app flag. For example:
 Builds and updates a specified application given by the --app flag. For example:
@@ -61,25 +84,25 @@ specify it as follows:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --path ~/path-to-dir --tag testing"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update --app remote-git-app --source github"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --values my-values.yaml"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --method docker --dockerfile ./docker/prod.Dockerfile"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateFull)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --path ~/path-to-dir --tag testing"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app remote-git-app --source github"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --values my-values.yaml"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --method docker --dockerfile ./docker/prod.Dockerfile"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateFull)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var updateGetEnvCmd = &cobra.Command{
-	Use:   "get-env",
-	Short: "Gets environment variables for a deployment for a specified application given by the --app flag.",
-	Long: fmt.Sprintf(`
+	updateGetEnvCmd := &cobra.Command{
+		Use:   "get-env",
+		Short: "Gets environment variables for a deployment for a specified application given by the --app flag.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Gets environment variables for a deployment for a specified application given by the --app
 Gets environment variables for a deployment for a specified application given by the --app
@@ -92,22 +115,22 @@ destination path for a .env file. For example:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update get-env\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app | xargs"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app --file .env"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateGetEnv)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update get-env\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app | xargs"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app --file .env"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateGetEnv)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var updateBuildCmd = &cobra.Command{
-	Use:   "build",
-	Short: "Builds a new version of the application specified by the --app flag.",
-	Long: fmt.Sprintf(`
+	updateBuildCmd := &cobra.Command{
+		Use:   "build",
+		Short: "Builds a new version of the application specified by the --app flag.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Builds a new version of the application specified by the --app flag. Depending on the
 Builds a new version of the application specified by the --app flag. Depending on the
@@ -133,24 +156,24 @@ for the application:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update build\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker --dockerfile ./prod.Dockerfile"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateBuild)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update build\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker --dockerfile ./prod.Dockerfile"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateBuild)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var updatePushCmd = &cobra.Command{
-	Use:   "push",
-	Short: "Pushes an image to a Docker registry linked to your Porter project.",
-	Args:  cobra.MaximumNArgs(1),
-	Long: fmt.Sprintf(`
+	updatePushCmd := &cobra.Command{
+		Use:   "push",
+		Short: "Pushes an image to a Docker registry linked to your Porter project.",
+		Args:  cobra.MaximumNArgs(1),
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Pushes a local Docker image to a registry linked to your Porter project. This command
 Pushes a local Docker image to a registry linked to your Porter project. This command
@@ -172,24 +195,24 @@ This command will not use your pre-saved authentication set up via "docker login
 are using an image registry that was created outside of Porter, make sure that you have
 are using an image registry that was created outside of Porter, make sure that you have
 linked it via "porter connect".
 linked it via "porter connect".
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
-		color.New(color.FgBlue).Sprintf("porter config set-project"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update push gcr.io/snowflake-123456/nginx:1234567"),
-		color.New(color.Bold).Sprintf("LEGACY USAGE:"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updatePush)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
+			color.New(color.FgBlue).Sprintf("porter config set-project"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update push gcr.io/snowflake-123456/nginx:1234567"),
+			color.New(color.Bold).Sprintf("LEGACY USAGE:"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updatePush)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var updateConfigCmd = &cobra.Command{
-	Use:   "config",
-	Short: "Updates the configuration for an application specified by the --app flag.",
-	Long: fmt.Sprintf(`
+	updateConfigCmd := &cobra.Command{
+		Use:   "config",
+		Short: "Updates the configuration for an application specified by the --app flag.",
+		Long: fmt.Sprintf(`
 %s
 %s
 
 
 Updates the configuration for an application specified by the --app flag, using the configuration
 Updates the configuration for an application specified by the --app flag, using the configuration
@@ -204,72 +227,50 @@ the image that the application uses if no --values file is specified:
 
 
   %s
   %s
 `,
 `,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update config\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --values my-values.yaml"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --tag custom-tag"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateUpgrade)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var updateEnvGroupCmd = &cobra.Command{
-	Use:     "env-group",
-	Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
-	Short:   "Updates an environment group's variables, specified by the --name flag.",
-	Run: func(cmd *cobra.Command, args []string) {
-		color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
-	},
-}
-
-var updateSetEnvGroupCmd = &cobra.Command{
-	Use:   "set",
-	Short: "Sets the desired value of an environment variable in an env group in the form VAR=VALUE.",
-	Args:  cobra.MaximumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateSetEnvGroup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+			color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update config\":"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --values my-values.yaml"),
+			color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --tag custom-tag"),
+		),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateUpgrade)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-var updateUnsetEnvGroupCmd = &cobra.Command{
-	Use:   "unset",
-	Short: "Removes an environment variable from an env group.",
-	Args:  cobra.MinimumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, updateUnsetEnvGroup)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
+	updateEnvGroupCmd := &cobra.Command{
+		Use:     "env-group",
+		Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
+		Short:   "Updates an environment group's variables, specified by the --name flag.",
+		Run: func(cmd *cobra.Command, args []string) {
+			_, _ = color.New(color.FgRed).Fprintln(os.Stderr, "need to specify an operation to continue")
+		},
+	}
 
 
-var (
-	app                     string
-	getEnvFileDest          string
-	localPath               string
-	tag                     string
-	dockerfile              string
-	method                  string
-	stream                  bool
-	buildFlagsEnv           []string
-	forcePush               bool
-	useCache                bool
-	version                 uint
-	varType                 string
-	normalEnvGroupVars      []string
-	secretEnvGroupVars      []string
-	waitForSuccessfulDeploy bool
-)
+	updateSetEnvGroupCmd := &cobra.Command{
+		Use:   "set",
+		Short: "Sets the desired value of an environment variable in an env group in the form VAR=VALUE.",
+		Args:  cobra.MaximumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateSetEnvGroup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
-func init() {
-	buildFlagsEnv = []string{}
-	rootCmd.AddCommand(updateCmd)
+	updateUnsetEnvGroupCmd := &cobra.Command{
+		Use:   "unset",
+		Short: "Removes an environment variable from an env group.",
+		Args:  cobra.MinimumNArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateUnsetEnvGroup)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
 
 
 	updateCmd.PersistentFlags().StringVar(
 	updateCmd.PersistentFlags().StringVar(
 		&app,
 		&app,
@@ -438,9 +439,24 @@ func init() {
 	updateCmd.AddCommand(updatePushCmd)
 	updateCmd.AddCommand(updatePushCmd)
 	updateCmd.AddCommand(updateConfigCmd)
 	updateCmd.AddCommand(updateConfigCmd)
 	updateCmd.AddCommand(updateEnvGroupCmd)
 	updateCmd.AddCommand(updateEnvGroupCmd)
+
+	return updateCmd
 }
 }
 
 
-func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.UpdateFull(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
 	fullPath, err := filepath.Abs(localPath)
 	fullPath, err := filepath.Abs(localPath)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -459,25 +475,22 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 
 	color.New(color.FgGreen).Println("Deploying app:", app)
 	color.New(color.FgGreen).Println("Deploying app:", app)
 
 
-	updateAgent, err := updateGetAgent(client)
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = updateBuildWithAgent(updateAgent)
-
+	err = updateBuildWithAgent(ctx, updateAgent)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = updatePushWithAgent(updateAgent)
-
+	err = updatePushWithAgent(ctx, updateAgent)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = updateUpgradeWithAgent(updateAgent)
-
+	err = updateUpgradeWithAgent(ctx, updateAgent)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -486,7 +499,7 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 		// solves timing issue where replicasets were not on the cluster, before our initial check
 		// solves timing issue where replicasets were not on the cluster, before our initial check
 		time.Sleep(10 * time.Second)
 		time.Sleep(10 * time.Second)
 
 
-		err := checkDeploymentStatus(client)
+		err := checkDeploymentStatus(ctx, client, cliConf)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -495,13 +508,13 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	return nil
 	return nil
 }
 }
 
 
-func updateGetEnv(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	updateAgent, err := updateGetAgent(client)
+func updateGetEnv(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+	buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 		UseNewConfig: false,
 		UseNewConfig: false,
 	})
 	})
 	if err != nil {
 	if err != nil {
@@ -510,7 +523,6 @@ func updateGetEnv(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 
 
 	// set the environment variables in the process
 	// set the environment variables in the process
 	err = updateAgent.SetBuildEnv(buildEnv)
 	err = updateAgent.SetBuildEnv(buildEnv)
-
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -519,16 +531,29 @@ func updateGetEnv(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 	return updateAgent.WriteBuildEnv(getEnvFileDest)
 	return updateAgent.WriteBuildEnv(getEnvFileDest)
 }
 }
 
 
-func updateBuild(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	updateAgent, err := updateGetAgent(client)
+func updateBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.UpdateBuild(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	return updateBuildWithAgent(updateAgent)
+	return updateBuildWithAgent(ctx, updateAgent)
 }
 }
 
 
-func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func updatePush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	if app == "" {
 	if app == "" {
 		if len(args) == 0 {
 		if len(args) == 0 {
 			return fmt.Errorf("please provide the docker image name")
 			return fmt.Errorf("please provide the docker image name")
@@ -536,7 +561,7 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 
 		image := args[0]
 		image := args[0]
 
 
-		registries, err := client.ListRegistries(context.Background(), cliConf.Project)
+		registries, err := client.ListRegistries(ctx, cliConf.Project)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -555,7 +580,7 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 			return fmt.Errorf("could not find registry for image: %s", image)
 			return fmt.Errorf("could not find registry for image: %s", image)
 		}
 		}
 
 
-		err = client.CreateRepository(context.Background(), cliConf.Project, regID,
+		err = client.CreateRepository(ctx, cliConf.Project, regID,
 			&types.CreateRegistryRepositoryRequest{
 			&types.CreateRegistryRepositoryRequest{
 				ImageRepoURI: strings.Split(image, ":")[0],
 				ImageRepoURI: strings.Split(image, ":")[0],
 			},
 			},
@@ -565,12 +590,12 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 			return err
 			return err
 		}
 		}
 
 
-		agent, err := docker.NewAgentWithAuthGetter(client, cliConf.Project)
+		agent, err := docker.NewAgentWithAuthGetter(ctx, client, cliConf.Project)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		err = agent.PushImage(image)
+		err = agent.PushImage(ctx, image)
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -579,21 +604,34 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 		return nil
 		return nil
 	}
 	}
 
 
-	updateAgent, err := updateGetAgent(client)
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	return updatePushWithAgent(updateAgent)
+	return updatePushWithAgent(ctx, updateAgent)
 }
 }
 
 
-func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	updateAgent, err := updateGetAgent(client)
+func updateUpgrade(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
+	}
+
+	if project.ValidateApplyV2 {
+		err = v2.UpdateUpgrade(ctx)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	err = updateUpgradeWithAgent(updateAgent)
+	err = updateUpgradeWithAgent(ctx, updateAgent)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -603,7 +641,7 @@ func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 		// solves timing issue where replicasets were not on the cluster, before our initial check
 		// solves timing issue where replicasets were not on the cluster, before our initial check
 		time.Sleep(10 * time.Second)
 		time.Sleep(10 * time.Second)
 
 
-		err := checkDeploymentStatus(client)
+		err := checkDeploymentStatus(ctx, client, cliConf)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -612,7 +650,7 @@ func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 	return nil
 	return nil
 }
 }
 
 
-func updateSetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func updateSetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	if len(normalEnvGroupVars) == 0 && len(secretEnvGroupVars) == 0 && len(args) == 0 {
 	if len(normalEnvGroupVars) == 0 && len(secretEnvGroupVars) == 0 && len(args) == 0 {
 		return fmt.Errorf("please provide one or more variables to update")
 		return fmt.Errorf("please provide one or more variables to update")
 	}
 	}
@@ -623,7 +661,7 @@ func updateSetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client
 	s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
 	s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
 	s.Start()
 	s.Start()
 
 
-	envGroupResp, err := client.GetEnvGroup(context.Background(), cliConf.Project, cliConf.Cluster, namespace,
+	envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
 		&types.GetEnvGroupRequest{
 		&types.GetEnvGroupRequest{
 			Name: name, Version: version,
 			Name: name, Version: version,
 		},
 		},
@@ -696,7 +734,7 @@ func updateSetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client
 	s.Start()
 	s.Start()
 
 
 	_, err = client.CreateEnvGroup(
 	_, err = client.CreateEnvGroup(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
 	)
 	)
 
 
 	s.Stop()
 	s.Stop()
@@ -720,7 +758,7 @@ func validateVarValue(in string) (string, string, error) {
 	return key, value, nil
 	return key, value, nil
 }
 }
 
 
-func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func updateUnsetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
 	if len(args) == 0 {
 	if len(args) == 0 {
 		return fmt.Errorf("required variable name")
 		return fmt.Errorf("required variable name")
 	}
 	}
@@ -731,7 +769,7 @@ func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 	s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
 	s.Suffix = fmt.Sprintf(" Fetching env group '%s' in namespace '%s'", name, namespace)
 	s.Start()
 	s.Start()
 
 
-	envGroupResp, err := client.GetEnvGroup(context.Background(), cliConf.Project, cliConf.Cluster, namespace,
+	envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
 		&types.GetEnvGroupRequest{
 		&types.GetEnvGroupRequest{
 			Name: name, Version: version,
 			Name: name, Version: version,
 		},
 		},
@@ -757,7 +795,7 @@ func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 	s.Start()
 	s.Start()
 
 
 	_, err = client.CreateEnvGroup(
 	_, err = client.CreateEnvGroup(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
 	)
 	)
 
 
 	s.Stop()
 	s.Stop()
@@ -772,7 +810,7 @@ func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 }
 }
 
 
 // HELPER METHODS
 // HELPER METHODS
-func updateGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
+func updateGetAgent(ctx context.Context, client api.Client, cliConf config.CLIConfig) (*deploy.DeployAgent, error) {
 	var buildMethod deploy.DeployBuildType
 	var buildMethod deploy.DeployBuildType
 
 
 	if method != "" {
 	if method != "" {
@@ -789,7 +827,7 @@ func updateGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
 	}
 	}
 
 
 	// initialize the update agent
 	// initialize the update agent
-	return deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
+	return deploy.NewDeployAgent(ctx, client, app, &deploy.DeployOpts{
 		SharedOpts: &deploy.SharedOpts{
 		SharedOpts: &deploy.SharedOpts{
 			ProjectID:       cliConf.Project,
 			ProjectID:       cliConf.Project,
 			ClusterID:       cliConf.Cluster,
 			ClusterID:       cliConf.Cluster,
@@ -805,22 +843,22 @@ func updateGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
 	})
 	})
 }
 }
 
 
-func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
+func updateBuildWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
 	// build the deployment
 	// build the deployment
 	color.New(color.FgGreen).Println("Building docker image for", app)
 	color.New(color.FgGreen).Println("Building docker image for", app)
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		_ = updateAgent.StreamEvent(ctx, types.SubEvent{
 			EventID: "build",
 			EventID: "build",
 			Name:    "Build",
 			Name:    "Build",
 			Index:   100,
 			Index:   100,
 			Status:  types.EventStatusInProgress,
 			Status:  types.EventStatusInProgress,
 			Info:    "",
 			Info:    "",
-		})
+		}) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 	}
 
 
 	if useCache {
 	if useCache {
-		err := config.SetDockerConfig(updateAgent.Client)
+		err := config.SetDockerConfig(ctx, updateAgent.Client, updateAgent.Opts.ProjectID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -832,14 +870,14 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 		return err
 		return err
 	}
 	}
 
 
-	buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+	buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 		UseNewConfig: true,
 		UseNewConfig: true,
 		NewConfig:    valuesObj,
 		NewConfig:    valuesObj,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		if stream {
 		if stream {
 			// another concern: is it safe to ignore the error here?
 			// another concern: is it safe to ignore the error here?
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "build",
 				EventID: "build",
 				Name:    "Build",
 				Name:    "Build",
 				Index:   110,
 				Index:   110,
@@ -855,7 +893,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 
 
 	if err != nil {
 	if err != nil {
 		if stream {
 		if stream {
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "build",
 				EventID: "build",
 				Name:    "Build",
 				Name:    "Build",
 				Index:   120,
 				Index:   120,
@@ -866,9 +904,9 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 		return err
 		return err
 	}
 	}
 
 
-	if err := updateAgent.Build(nil); err != nil {
+	if err := updateAgent.Build(ctx, nil); err != nil {
 		if stream {
 		if stream {
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "build",
 				EventID: "build",
 				Name:    "Build",
 				Name:    "Build",
 				Index:   130,
 				Index:   130,
@@ -880,7 +918,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 	}
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 			EventID: "build",
 			EventID: "build",
 			Name:    "Build",
 			Name:    "Build",
 			Index:   140,
 			Index:   140,
@@ -892,7 +930,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 	return nil
 	return nil
 }
 }
 
 
-func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
+func updatePushWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
 	if useCache {
 	if useCache {
 		color.New(color.FgGreen).Println("Skipping image push for", app, "as use-cache is set")
 		color.New(color.FgGreen).Println("Skipping image push for", app, "as use-cache is set")
 
 
@@ -903,18 +941,19 @@ func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
 	color.New(color.FgGreen).Println("Pushing new image for", app)
 	color.New(color.FgGreen).Println("Pushing new image for", app)
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
-			EventID: "push",
-			Name:    "Push",
-			Index:   200,
-			Status:  types.EventStatusInProgress,
-			Info:    "",
-		})
+		updateAgent.StreamEvent( //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
+			ctx, types.SubEvent{
+				EventID: "push",
+				Name:    "Push",
+				Index:   200,
+				Status:  types.EventStatusInProgress,
+				Info:    "",
+			})
 	}
 	}
 
 
-	if err := updateAgent.Push(); err != nil {
+	if err := updateAgent.Push(ctx); err != nil {
 		if stream {
 		if stream {
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "push",
 				EventID: "push",
 				Name:    "Push",
 				Name:    "Push",
 				Index:   210,
 				Index:   210,
@@ -926,7 +965,7 @@ func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 	}
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 			EventID: "push",
 			EventID: "push",
 			Name:    "Push",
 			Name:    "Push",
 			Index:   220,
 			Index:   220,
@@ -938,12 +977,12 @@ func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
 	return nil
 	return nil
 }
 }
 
 
-func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
+func updateUpgradeWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
 	// push the deployment
 	// push the deployment
 	color.New(color.FgGreen).Println("Upgrading configuration for", app)
 	color.New(color.FgGreen).Println("Upgrading configuration for", app)
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 			EventID: "upgrade",
 			EventID: "upgrade",
 			Name:    "Upgrade",
 			Name:    "Upgrade",
 			Index:   300,
 			Index:   300,
@@ -962,7 +1001,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 
 
 	if err != nil {
 	if err != nil {
 		if stream {
 		if stream {
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "upgrade",
 				EventID: "upgrade",
 				Name:    "Upgrade",
 				Name:    "Upgrade",
 				Index:   310,
 				Index:   310,
@@ -975,6 +1014,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 
 
 	if len(updateAgent.Opts.AdditionalEnv) > 0 {
 	if len(updateAgent.Opts.AdditionalEnv) > 0 {
 		syncedEnv, err := deploy.GetSyncedEnv(
 		syncedEnv, err := deploy.GetSyncedEnv(
+			ctx,
 			updateAgent.Client,
 			updateAgent.Client,
 			updateAgent.Release.Config,
 			updateAgent.Release.Config,
 			updateAgent.Opts.ProjectID,
 			updateAgent.Opts.ProjectID,
@@ -1018,11 +1058,11 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 		})
 		})
 	}
 	}
 
 
-	err = updateAgent.UpdateImageAndValues(valuesObj)
+	err = updateAgent.UpdateImageAndValues(ctx, valuesObj)
 
 
 	if err != nil {
 	if err != nil {
 		if stream {
 		if stream {
-			updateAgent.StreamEvent(types.SubEvent{
+			updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 				EventID: "upgrade",
 				EventID: "upgrade",
 				Name:    "Upgrade",
 				Name:    "Upgrade",
 				Index:   320,
 				Index:   320,
@@ -1034,7 +1074,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 	}
 
 
 	if stream {
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		updateAgent.StreamEvent(ctx, types.SubEvent{ //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 			EventID: "upgrade",
 			EventID: "upgrade",
 			Name:    "Upgrade",
 			Name:    "Upgrade",
 			Index:   330,
 			Index:   330,
@@ -1048,14 +1088,15 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 	return nil
 	return nil
 }
 }
 
 
-func checkDeploymentStatus(client *api.Client) error {
+func checkDeploymentStatus(ctx context.Context, client api.Client, cliConfig config.CLIConfig) error {
 	color.New(color.FgBlue).Println("waiting for deployment to be ready, this may take a few minutes and will time out if it takes longer than 30 minutes")
 	color.New(color.FgBlue).Println("waiting for deployment to be ready, this may take a few minutes and will time out if it takes longer than 30 minutes")
 
 
 	sharedConf := &PorterRunSharedConfig{
 	sharedConf := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 	}
 
 
-	err := sharedConf.setSharedConfig()
+	err := sharedConf.setSharedConfig(ctx)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("could not retrieve kubernetes credentials: %w", err)
 		return fmt.Errorf("could not retrieve kubernetes credentials: %w", err)
 	}
 	}
@@ -1065,7 +1106,7 @@ func checkDeploymentStatus(client *api.Client) error {
 	success := false
 	success := false
 
 
 	depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
 	depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
-		context.Background(),
+		ctx,
 		metav1.ListOptions{
 		metav1.ListOptions{
 			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 		},
 		},
@@ -1101,7 +1142,7 @@ func checkDeploymentStatus(client *api.Client) error {
 	}
 	}
 
 
 	pods, err := sharedConf.Clientset.CoreV1().Pods(namespace).List(
 	pods, err := sharedConf.Clientset.CoreV1().Pods(namespace).List(
-		context.Background(), metav1.ListOptions{
+		ctx, metav1.ListOptions{
 			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 		},
 		},
 	)
 	)
@@ -1120,7 +1161,7 @@ func checkDeploymentStatus(client *api.Client) error {
 			for _, ref := range pod.OwnerReferences {
 			for _, ref := range pod.OwnerReferences {
 				if ref.Kind == "ReplicaSet" {
 				if ref.Kind == "ReplicaSet" {
 					rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
 					rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
-						context.Background(),
+						ctx,
 						ref.Name,
 						ref.Name,
 						metav1.GetOptions{},
 						metav1.GetOptions{},
 					)
 					)
@@ -1147,7 +1188,7 @@ func checkDeploymentStatus(client *api.Client) error {
 	for time.Now().Before(timeWait) {
 	for time.Now().Before(timeWait) {
 		// refresh the client every 10 minutes
 		// refresh the client every 10 minutes
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
-			err = sharedConf.setSharedConfig()
+			err = sharedConf.setSharedConfig(ctx)
 
 
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("could not retrieve kube credentials: %s", err.Error())
 				return fmt.Errorf("could not retrieve kube credentials: %s", err.Error())
@@ -1157,7 +1198,7 @@ func checkDeploymentStatus(client *api.Client) error {
 		}
 		}
 
 
 		rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
 		rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
-			context.Background(),
+			ctx,
 			rsName,
 			rsName,
 			metav1.GetOptions{},
 			metav1.GetOptions{},
 		)
 		)

+ 20 - 0
cli/cmd/commands/version.go

@@ -0,0 +1,20 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+func registerCommand_Version(_ config.CLIConfig) *cobra.Command {
+	versionCmd := &cobra.Command{
+		Use:     "version",
+		Aliases: []string{"v", "--version"},
+		Short:   "Prints the version of the Porter CLI",
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Println(config.Version)
+		},
+	}
+	return versionCmd
+}

+ 0 - 303
cli/cmd/config.go

@@ -1,303 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/briandowns/spinner"
-	"github.com/fatih/color"
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	cliConfig "github.com/porter-dev/porter/cli/cmd/config"
-	"github.com/porter-dev/porter/cli/cmd/utils"
-	"github.com/spf13/cobra"
-)
-
-var cliConf = cliConfig.GetCLIConfig()
-
-var configCmd = &cobra.Command{
-	Use:   "config",
-	Short: "Commands that control local configuration settings",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := printConfig(); err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-var configSetProjectCmd = &cobra.Command{
-	Use:   "set-project [id]",
-	Args:  cobra.MaximumNArgs(1),
-	Short: "Saves the project id in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		if len(args) == 0 {
-			err := checkLoginAndRun(args, listAndSetProject)
-			if err != nil {
-				os.Exit(1)
-			}
-		} else {
-			projID, err := strconv.ParseUint(args[0], 10, 64)
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-
-			err = cliConf.SetProject(uint(projID))
-
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-		}
-	},
-}
-
-var configSetClusterCmd = &cobra.Command{
-	Use:   "set-cluster [id]",
-	Args:  cobra.MaximumNArgs(1),
-	Short: "Saves the cluster id in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		if len(args) == 0 {
-			err := checkLoginAndRun(args, listAndSetCluster)
-			if err != nil {
-				os.Exit(1)
-			}
-		} else {
-			clusterID, err := strconv.ParseUint(args[0], 10, 64)
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-
-			err = cliConf.SetCluster(uint(clusterID))
-
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-		}
-	},
-}
-
-var configSetRegistryCmd = &cobra.Command{
-	Use:   "set-registry [id]",
-	Args:  cobra.MaximumNArgs(1),
-	Short: "Saves the registry id in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		if len(args) == 0 {
-			err := checkLoginAndRun(args, listAndSetRegistry)
-			if err != nil {
-				os.Exit(1)
-			}
-		} else {
-			registryID, err := strconv.ParseUint(args[0], 10, 64)
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-
-			err = cliConf.SetRegistry(uint(registryID))
-
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-				os.Exit(1)
-			}
-		}
-	},
-}
-
-var configSetHelmRepoCmd = &cobra.Command{
-	Use:   "set-helmrepo [id]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Saves the helm repo id in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		hrID, err := strconv.ParseUint(args[0], 10, 64)
-		if err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-
-		err = cliConf.SetHelmRepo(uint(hrID))
-
-		if err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-var configSetHostCmd = &cobra.Command{
-	Use:   "set-host [host]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Saves the host in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := cliConf.SetHost(args[0])
-		if err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-var configSetKubeconfigCmd = &cobra.Command{
-	Use:   "set-kubeconfig [kubeconfig-path]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Saves the path to kubeconfig in the default configuration",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := cliConf.SetKubeconfig(args[0])
-		if err != nil {
-			color.New(color.FgRed).Fprintf(os.Stderr, "An error occurred: %v\n", err)
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(configCmd)
-
-	configCmd.AddCommand(configSetProjectCmd)
-	configCmd.AddCommand(configSetClusterCmd)
-	configCmd.AddCommand(configSetHostCmd)
-	configCmd.AddCommand(configSetRegistryCmd)
-	configCmd.AddCommand(configSetHelmRepoCmd)
-	configCmd.AddCommand(configSetKubeconfigCmd)
-}
-
-func printConfig() error {
-	config, err := ioutil.ReadFile(filepath.Join(home, ".porter", "porter.yaml"))
-	if err != nil {
-		return err
-	}
-
-	fmt.Println(string(config))
-
-	return nil
-}
-
-func listAndSetProject(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
-	s.Color("cyan")
-	s.Suffix = " Loading list of projects"
-	s.Start()
-
-	resp, err := client.ListUserProjects(context.Background())
-
-	s.Stop()
-
-	if err != nil {
-		return err
-	}
-
-	var projID uint64
-
-	if len(*resp) > 1 {
-		// only give the option to select when more than one option exists
-		projName, err := utils.PromptSelect("Select a project with ID", func() []string {
-			var names []string
-
-			for _, proj := range *resp {
-				names = append(names, fmt.Sprintf("%s - %d", proj.Name, proj.ID))
-			}
-
-			return names
-		}())
-		if err != nil {
-			return err
-		}
-
-		projID, _ = strconv.ParseUint(strings.Split(projName, " - ")[1], 10, 64)
-	} else {
-		projID = uint64((*resp)[0].ID)
-	}
-
-	cliConf.SetProject(uint(projID))
-
-	return nil
-}
-
-func listAndSetCluster(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
-	s.Color("cyan")
-	s.Suffix = " Loading list of clusters"
-	s.Start()
-
-	resp, err := client.ListProjectClusters(context.Background(), cliConf.Project)
-
-	s.Stop()
-
-	if err != nil {
-		return err
-	}
-
-	var clusterID uint64
-
-	if len(*resp) > 1 {
-		clusterName, err := utils.PromptSelect("Select a cluster with ID", func() []string {
-			var names []string
-
-			for _, cluster := range *resp {
-				names = append(names, fmt.Sprintf("%s - %d", cluster.Name, cluster.ID))
-			}
-
-			return names
-		}())
-		if err != nil {
-			return err
-		}
-
-		clusterID, _ = strconv.ParseUint(strings.Split(clusterName, " - ")[1], 10, 64)
-	} else {
-		clusterID = uint64((*resp)[0].ID)
-	}
-
-	cliConf.SetCluster(uint(clusterID))
-
-	return nil
-}
-
-func listAndSetRegistry(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
-	s.Color("cyan")
-	s.Suffix = " Loading list of registries"
-	s.Start()
-
-	resp, err := client.ListRegistries(context.Background(), cliConf.Project)
-
-	s.Stop()
-
-	if err != nil {
-		return err
-	}
-
-	var regID uint64
-
-	if len(*resp) > 1 {
-		regName, err := utils.PromptSelect("Select a registry with ID", func() []string {
-			var names []string
-
-			for _, cluster := range *resp {
-				names = append(names, fmt.Sprintf("%s - %d", cluster.Name, cluster.ID))
-			}
-
-			return names
-		}())
-		if err != nil {
-			return err
-		}
-
-		regID, _ = strconv.ParseUint(strings.Split(regName, " - ")[1], 10, 64)
-	} else {
-		regID = uint64((*resp)[0].ID)
-	}
-
-	cliConf.SetRegistry(uint(regID))
-
-	return nil
-}

+ 128 - 98
cli/cmd/config/config.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
@@ -18,9 +17,6 @@ import (
 
 
 var home = homedir.HomeDir()
 var home = homedir.HomeDir()
 
 
-// config is a shared object used by all commands
-var config = &CLIConfig{}
-
 // CLIConfig is the set of shared configuration options for the CLI commands.
 // CLIConfig is the set of shared configuration options for the CLI commands.
 // This config is used by viper: calling Set() function for any parameter will
 // This config is used by viper: calling Set() function for any parameter will
 // update the corresponding field in the viper config file.
 // update the corresponding field in the viper config file.
@@ -45,78 +41,31 @@ type CLIConfig struct {
 // 2. env
 // 2. env
 // 3. config
 // 3. config
 // 4. default
 // 4. default
-//
-// It populates the shared config object above
-func InitAndLoadConfig() {
-	initAndLoadConfig(config)
-}
-
-func InitAndLoadNewConfig() *CLIConfig {
-	newConfig := &CLIConfig{}
-
-	initAndLoadConfig(newConfig)
-
-	return newConfig
+func InitAndLoadConfig() (CLIConfig, error) {
+	return initAndLoadConfig()
 }
 }
 
 
-func initAndLoadConfig(_config *CLIConfig) {
-	initFlagSet()
-
-	// check that the .porter folder exists; create if not
-	porterDir := filepath.Join(home, ".porter")
+func initAndLoadConfig() (CLIConfig, error) {
+	var config CLIConfig
 
 
-	if _, err := os.Stat(porterDir); os.IsNotExist(err) {
-		os.Mkdir(porterDir, 0o700)
-	} else if err != nil {
-		color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
-		os.Exit(1)
+	porterDir, err := getOrCreatePorterDirectoryAndConfig()
+	if err != nil {
+		return config, fmt.Errorf("unable to get or create porter directory: %w", err)
 	}
 	}
-
 	viper.SetConfigName("porter")
 	viper.SetConfigName("porter")
 	viper.SetConfigType("yaml")
 	viper.SetConfigType("yaml")
 	viper.AddConfigPath(porterDir)
 	viper.AddConfigPath(porterDir)
 
 
-	// Bind the flagset initialized above
-	viper.BindPFlags(utils.DriverFlagSet)
-	viper.BindPFlags(utils.DefaultFlagSet)
-	viper.BindPFlags(utils.RegistryFlagSet)
-	viper.BindPFlags(utils.HelmRepoFlagSet)
-
-	// Bind the environment variables with prefix "PORTER_"
-	viper.SetEnvPrefix("PORTER")
-	viper.BindEnv("host")
-	viper.BindEnv("project")
-	viper.BindEnv("cluster")
-	viper.BindEnv("token")
-
-	err := viper.ReadInConfig()
-	if err != nil {
-		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
-			// create blank config file
-			err := ioutil.WriteFile(filepath.Join(home, ".porter", "porter.yaml"), []byte{}, 0o644)
-			if err != nil {
-				color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
-				os.Exit(1)
-			}
-		} else {
-			// Config file was found but another error was produced
-			color.New(color.FgRed).Fprintf(os.Stderr, "%v\n", err)
-			os.Exit(1)
-		}
-	}
-
-	// unmarshal the config into the shared config struct
-	viper.Unmarshal(_config)
-}
-
-// initFlagSet initializes the shared flags used by multiple commands
-func initFlagSet() {
 	utils.DriverFlagSet.StringVar(
 	utils.DriverFlagSet.StringVar(
 		&config.Driver,
 		&config.Driver,
 		"driver",
 		"driver",
 		"local",
 		"local",
 		"driver to use (local or docker)",
 		"driver to use (local or docker)",
 	)
 	)
+	err = viper.BindPFlags(utils.DriverFlagSet)
+	if err != nil {
+		return config, err
+	}
 
 
 	utils.DefaultFlagSet.StringVar(
 	utils.DefaultFlagSet.StringVar(
 		&config.Host,
 		&config.Host,
@@ -146,6 +95,11 @@ func initFlagSet() {
 		"token for Porter authentication",
 		"token for Porter authentication",
 	)
 	)
 
 
+	err = viper.BindPFlags(utils.DefaultFlagSet)
+	if err != nil {
+		return config, err
+	}
+
 	utils.RegistryFlagSet.UintVar(
 	utils.RegistryFlagSet.UintVar(
 		&config.Registry,
 		&config.Registry,
 		"registry",
 		"registry",
@@ -153,32 +107,109 @@ func initFlagSet() {
 		"registry ID of connected Porter registry",
 		"registry ID of connected Porter registry",
 	)
 	)
 
 
+	err = viper.BindPFlags(utils.RegistryFlagSet)
+	if err != nil {
+		return config, err
+	}
+
 	utils.HelmRepoFlagSet.UintVar(
 	utils.HelmRepoFlagSet.UintVar(
 		&config.HelmRepo,
 		&config.HelmRepo,
 		"helmrepo",
 		"helmrepo",
 		0,
 		0,
 		"helm repo ID of connected Porter Helm repository",
 		"helm repo ID of connected Porter Helm repository",
 	)
 	)
-}
+	err = viper.BindPFlags(utils.HelmRepoFlagSet)
+	if err != nil {
+		return config, err
+	}
+
+	viper.SetEnvPrefix("PORTER")
+	err = viper.BindEnv("host")
+	if err != nil {
+		return config, err
+	}
+	err = viper.BindEnv("project")
+	if err != nil {
+		return config, err
+	}
+	err = viper.BindEnv("cluster")
+	if err != nil {
+		return config, err
+	}
+	err = viper.BindEnv("token")
+	if err != nil {
+		return config, err
+	}
+
+	err = createAndLoadPorterYaml(porterDir)
+	if err != nil {
+		return config, fmt.Errorf("unable to load porter config: %w", err)
+	}
 
 
-func GetCLIConfig() *CLIConfig {
-	if config == nil {
-		panic("GetCLIConfig() called before initialisation")
+	err = viper.Unmarshal(&config)
+	if err != nil {
+		return config, fmt.Errorf("unable to unmarshal porter config: %w", err)
 	}
 	}
 
 
-	return config
+	return config, nil
 }
 }
 
 
-func GetAPIClient() *api.Client {
-	config := GetCLIConfig()
+// getOrCreatePorterDirectoryAndConfig checks that the .porter folder exists; create if not
+func getOrCreatePorterDirectoryAndConfig() (string, error) {
+	porterDir := filepath.Join(home, ".porter")
 
 
-	if token := config.Token; token != "" {
-		return api.NewClientWithToken(config.Host+"/api", token)
+	_, err := os.Stat(porterDir)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return "", fmt.Errorf("error reading porter directory: %w", err)
+		}
+		err = os.Mkdir(porterDir, 0o700)
+		if err != nil {
+			return "", fmt.Errorf("error creating porter directory: %w", err)
+		}
 	}
 	}
+	return porterDir, nil
+}
+
+// createAndLoadPorterYaml loads a porter.yaml config into Viper if it exists, or creates the file if it does not
+func createAndLoadPorterYaml(porterDir string) error {
+	err := viper.ReadInConfig()
+	if err != nil {
+		_, ok := err.(viper.ConfigFileNotFoundError)
+		if !ok {
+			return fmt.Errorf("unknown error reading ~/.porter/porter.yaml config: %w", err)
+		}
 
 
-	return api.NewClient(config.Host+"/api", "cookie.json")
+		err := os.WriteFile(filepath.Join(porterDir, "porter.yaml"), []byte{}, 0o644) //nolint:gosec // do not want to change program logic. Should be addressed later
+		if err != nil {
+			return fmt.Errorf("unable to create ~/.porter/porter.yaml config: %w", err)
+		}
+	}
+	return nil
 }
 }
 
 
+// func GetCLIConfig() *CLIConfig {
+// 	if config == nil {
+// 		panic("GetCLIConfig() called before initialisation")
+// 	}
+
+// 	return config
+// }
+
+// func GetAPIClient() api.Client {
+// 	ctx := ctx
+
+// 	config := GetCLIConfig()
+
+// 	client := api.NewClientWithConfig(ctx, api.NewClientInput{
+// 		BaseURL:        fmt.Sprintf("%s/api", config.Host),
+// 		BearerToken:    config.Token,
+// 		CookieFileName: "cookie.json",
+// 	})
+
+// 	return client
+// }
+
 func (c *CLIConfig) SetDriver(driver string) error {
 func (c *CLIConfig) SetDriver(driver string) error {
 	viper.Set("driver", driver)
 	viper.Set("driver", driver)
 	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
 	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)
@@ -187,7 +218,7 @@ func (c *CLIConfig) SetDriver(driver string) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Driver = driver
+	c.Driver = driver
 
 
 	return nil
 	return nil
 }
 }
@@ -210,20 +241,21 @@ func (c *CLIConfig) SetHost(host string) error {
 
 
 	color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
 	color.New(color.FgGreen).Printf("Set the current host as %s\n", host)
 
 
-	config.Host = host
-	config.Project = 0
-	config.Cluster = 0
-	config.Token = ""
+	c.Host = host
+	c.Project = 0
+	c.Cluster = 0
+	c.Token = ""
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (c *CLIConfig) SetProject(projectID uint) error {
+// SetProject sets a project for all API commands
+func (c *CLIConfig) SetProject(ctx context.Context, apiClient api.Client, projectID uint) error {
 	viper.Set("project", projectID)
 	viper.Set("project", projectID)
 
 
 	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
 	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
 
 
-	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
+	if c.Kubeconfig != "" || viper.IsSet("kubeconfig") {
 		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 	}
 	}
 
 
@@ -232,16 +264,13 @@ func (c *CLIConfig) SetProject(projectID uint) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Project = projectID
+	c.Project = projectID
 
 
-	client := GetAPIClient()
-	if client != nil {
-		resp, err := client.ListProjectClusters(context.Background(), projectID)
-		if err == nil {
-			clusters := *resp
-			if len(clusters) == 1 {
-				c.SetCluster(clusters[0].ID)
-			}
+	resp, err := apiClient.ListProjectClusters(ctx, projectID)
+	if err == nil {
+		clusters := *resp
+		if len(clusters) == 1 {
+			_ = c.SetCluster(clusters[0].ID)
 		}
 		}
 	}
 	}
 
 
@@ -253,7 +282,7 @@ func (c *CLIConfig) SetCluster(clusterID uint) error {
 
 
 	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
 	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
 
 
-	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
+	if c.Kubeconfig != "" || viper.IsSet("kubeconfig") {
 		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 	}
 	}
 
 
@@ -262,7 +291,7 @@ func (c *CLIConfig) SetCluster(clusterID uint) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Cluster = clusterID
+	c.Cluster = clusterID
 
 
 	return nil
 	return nil
 }
 }
@@ -274,7 +303,7 @@ func (c *CLIConfig) SetToken(token string) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Token = token
+	c.Token = token
 
 
 	return nil
 	return nil
 }
 }
@@ -287,7 +316,7 @@ func (c *CLIConfig) SetRegistry(registryID uint) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Registry = registryID
+	c.Registry = registryID
 
 
 	return nil
 	return nil
 }
 }
@@ -300,7 +329,7 @@ func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.HelmRepo = helmRepoID
+	c.HelmRepo = helmRepoID
 
 
 	return nil
 	return nil
 }
 }
@@ -323,21 +352,22 @@ func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
 		return err
 		return err
 	}
 	}
 
 
-	config.Kubeconfig = kubeconfig
+	c.Kubeconfig = kubeconfig
 
 
 	return nil
 	return nil
 }
 }
 
 
-func ValidateCLIEnvironment() error {
-	if GetCLIConfig().Token == "" {
+// ValidateCLIEnvironment checks that all required variables are present for running the CLI
+func (c *CLIConfig) ValidateCLIEnvironment() error {
+	if c.Token == "" {
 		return fmt.Errorf("no auth token present, please run 'porter auth login' to authenticate")
 		return fmt.Errorf("no auth token present, please run 'porter auth login' to authenticate")
 	}
 	}
 
 
-	if GetCLIConfig().Project == 0 {
+	if c.Project == 0 {
 		return fmt.Errorf("no project selected, please run 'porter config set-project' to select a project")
 		return fmt.Errorf("no project selected, please run 'porter config set-project' to select a project")
 	}
 	}
 
 
-	if GetCLIConfig().Cluster == 0 {
+	if c.Cluster == 0 {
 		return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
 		return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
 	}
 	}
 
 

+ 18 - 19
cli/cmd/config/docker.go

@@ -3,7 +3,6 @@ package config
 import (
 import (
 	"context"
 	"context"
 	"encoding/base64"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/url"
 	"net/url"
@@ -19,15 +18,14 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/github"
 	"github.com/porter-dev/porter/cli/cmd/github"
 )
 )
 
 
-func SetDockerConfig(client *api.Client) error {
-	pID := GetCLIConfig().Project
-
+// SetDockerConfig sets up the docker config.json
+func SetDockerConfig(ctx context.Context, client api.Client, pID uint) error {
 	// get all registries that should be added
 	// get all registries that should be added
 	regToAdd := make([]string, 0)
 	regToAdd := make([]string, 0)
 
 
 	// get the list of namespaces
 	// get the list of namespaces
 	resp, err := client.ListRegistries(
 	resp, err := client.ListRegistries(
-		context.Background(),
+		ctx,
 		pID,
 		pID,
 	)
 	)
 	if err != nil {
 	if err != nil {
@@ -77,14 +75,15 @@ func SetDockerConfig(client *api.Client) error {
 	}
 	}
 
 
 	// read the file bytes
 	// read the file bytes
-	configBytes, err := ioutil.ReadFile(dockerConfigFile)
-	if err != nil {
-		return err
-	}
+	// // TODO: STEFAN - figure out why we are parsing the ~/.docker/config.json into the CLI config. Are we using the variables somewhere?
+	// configBytes, err := ioutil.ReadFile(dockerConfigFile)
+	// if err != nil {
+	// 	return err
+	// }
 
 
 	// check if the docker credential helper exists
 	// check if the docker credential helper exists
 	if !commandExists("docker-credential-porter") {
 	if !commandExists("docker-credential-porter") {
-		err := downloadCredMatchingRelease()
+		err := downloadCredMatchingRelease(ctx)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			os.Exit(1)
 			os.Exit(1)
@@ -99,7 +98,7 @@ func SetDockerConfig(client *api.Client) error {
 	err = cmdVersionCred.Run()
 	err = cmdVersionCred.Run()
 
 
 	if err != nil || writer.Version != Version {
 	if err != nil || writer.Version != Version {
-		err := downloadCredMatchingRelease()
+		err := downloadCredMatchingRelease(ctx)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			os.Exit(1)
 			os.Exit(1)
@@ -110,11 +109,11 @@ func SetDockerConfig(client *api.Client) error {
 		Filename: dockerConfigFile,
 		Filename: dockerConfigFile,
 	}
 	}
 
 
-	err = json.Unmarshal(configBytes, GetCLIConfig())
-
-	if err != nil {
-		return err
-	}
+	// // TODO: STEFAN - figure out why we are parsing the ~/.docker/config.json into the CLI config. Are we using the variables somewhere?
+	// err = json.Unmarshal(configBytes, GetCLIConfig())
+	// if err != nil {
+	// 	return err
+	// }
 
 
 	if configFile.CredentialHelpers == nil {
 	if configFile.CredentialHelpers == nil {
 		configFile.CredentialHelpers = make(map[string]string)
 		configFile.CredentialHelpers = make(map[string]string)
@@ -138,7 +137,7 @@ func SetDockerConfig(client *api.Client) error {
 
 
 			if !isAuthenticated {
 			if !isAuthenticated {
 				// get a dockerhub token from the Porter API
 				// get a dockerhub token from the Porter API
-				tokenResp, err := client.GetDockerhubAuthorizationToken(context.Background(), GetCLIConfig().Project)
+				tokenResp, err := client.GetDockerhubAuthorizationToken(ctx, pID)
 				if err != nil {
 				if err != nil {
 					return err
 					return err
 				}
 				}
@@ -176,7 +175,7 @@ func commandExists(cmd string) bool {
 	return err == nil
 	return err == nil
 }
 }
 
 
-func downloadCredMatchingRelease() error {
+func downloadCredMatchingRelease(ctx context.Context) error {
 	// download the porter cred helper
 	// download the porter cred helper
 	z := &github.ZIPReleaseGetter{
 	z := &github.ZIPReleaseGetter{
 		AssetName:           "docker-credential-porter",
 		AssetName:           "docker-credential-porter",
@@ -193,5 +192,5 @@ func downloadCredMatchingRelease() error {
 		},
 		},
 	}
 	}
 
 
-	return z.GetRelease(Version)
+	return z.GetRelease(ctx, Version)
 }
 }

+ 0 - 242
cli/cmd/connect.go

@@ -1,242 +0,0 @@
-package cmd
-
-import (
-	"os"
-
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/cli/cmd/connect"
-	"github.com/spf13/cobra"
-)
-
-var (
-	kubeconfigPath string
-	print          *bool
-	contexts       *[]string
-)
-
-var connectCmd = &cobra.Command{
-	Use:   "connect",
-	Short: "Commands that connect to external clusters and providers",
-}
-
-var connectKubeconfigCmd = &cobra.Command{
-	Use:   "kubeconfig",
-	Short: "Uses the local kubeconfig to add a cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectKubeconfig)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectECRCmd = &cobra.Command{
-	Use:   "ecr",
-	Short: "Adds an ECR instance to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectECR)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectDockerhubCmd = &cobra.Command{
-	Use:   "dockerhub",
-	Short: "Adds a Docker Hub registry integration to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectDockerhub)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectRegistryCmd = &cobra.Command{
-	Use:   "registry",
-	Short: "Adds a custom image registry to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectRegistry)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectHelmRepoCmd = &cobra.Command{
-	Use:   "helm",
-	Short: "Adds a custom Helm registry to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectHelmRepo)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectGCRCmd = &cobra.Command{
-	Use:   "gcr",
-	Short: "Adds a GCR instance to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectGCR)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectGARCmd = &cobra.Command{
-	Use:   "gar",
-	Short: "Adds a GAR instance to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectGAR)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var connectDOCRCmd = &cobra.Command{
-	Use:   "docr",
-	Short: "Adds a DOCR instance to a project",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runConnectDOCR)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(connectCmd)
-
-	connectCmd.AddCommand(connectKubeconfigCmd)
-
-	connectKubeconfigCmd.PersistentFlags().StringVarP(
-		&kubeconfigPath,
-		"kubeconfig",
-		"k",
-		"",
-		"path to kubeconfig",
-	)
-
-	contexts = connectKubeconfigCmd.PersistentFlags().StringArray(
-		"context",
-		nil,
-		"the context to connect (defaults to the current context)",
-	)
-
-	connectCmd.AddCommand(connectECRCmd)
-	connectCmd.AddCommand(connectRegistryCmd)
-	connectCmd.AddCommand(connectDockerhubCmd)
-	connectCmd.AddCommand(connectGCRCmd)
-	connectCmd.AddCommand(connectGARCmd)
-	connectCmd.AddCommand(connectDOCRCmd)
-	connectCmd.AddCommand(connectHelmRepoCmd)
-}
-
-func runConnectKubeconfig(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	isLocal := false
-
-	if cliConf.Driver == "local" {
-		isLocal = true
-	}
-
-	id, err := connect.Kubeconfig(
-		client,
-		kubeconfigPath,
-		*contexts,
-		cliConf.Project,
-		isLocal,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetCluster(id)
-}
-
-func runConnectECR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.ECR(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectGCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.GCR(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectGAR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.GAR(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectDOCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.DOCR(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectDockerhub(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.Dockerhub(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectRegistry(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	regID, err := connect.Registry(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetRegistry(regID)
-}
-
-func runConnectHelmRepo(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	hrID, err := connect.HelmRepo(
-		client,
-		cliConf.Project,
-	)
-	if err != nil {
-		return err
-	}
-
-	return cliConf.SetHelmRepo(hrID)
-}

+ 4 - 3
cli/cmd/connect/dockerhub.go

@@ -13,7 +13,8 @@ import (
 )
 )
 
 
 func Dockerhub(
 func Dockerhub(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -46,7 +47,7 @@ func Dockerhub(
 
 
 	// create the basic auth integration
 	// create the basic auth integration
 	integration, err := client.CreateBasicAuthIntegration(
 	integration, err := client.CreateBasicAuthIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateBasicRequest{
 		&types.CreateBasicRequest{
 			Username: username,
 			Username: username,
@@ -60,7 +61,7 @@ func Dockerhub(
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 
 
 	reg, err := client.CreateRegistry(
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateRegistryRequest{
 		&types.CreateRegistryRequest{
 			URL:                fmt.Sprintf("index.docker.io/%s", repoName),
 			URL:                fmt.Sprintf("index.docker.io/%s", repoName),

+ 4 - 3
cli/cmd/connect/docr.go

@@ -13,7 +13,8 @@ import (
 
 
 // DOCR creates a DOCR integration
 // DOCR creates a DOCR integration
 func DOCR(
 func DOCR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -64,7 +65,7 @@ Registry URL: `))
 	}
 	}
 
 
 	reg, err := client.CreateRegistry(
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateRegistryRequest{
 		&types.CreateRegistryRequest{
 			Name:            regName,
 			Name:            regName,
@@ -76,7 +77,7 @@ Registry URL: `))
 	return reg.ID, nil
 	return reg.ID, nil
 }
 }
 
 
-func triggerDigitalOceanOAuth(client *api.Client, projectID uint) (*types.OAuthIntegration, error) {
+func triggerDigitalOceanOAuth(client api.Client, projectID uint) (*types.OAuthIntegration, error) {
 	var doAuth *types.OAuthIntegration
 	var doAuth *types.OAuthIntegration
 
 
 	oauthURL := fmt.Sprintf("%s/projects/%d/oauth/digitalocean", client.BaseURL, projectID)
 	oauthURL := fmt.Sprintf("%s/projects/%d/oauth/digitalocean", client.BaseURL, projectID)

+ 12 - 10
cli/cmd/connect/ecr.go

@@ -21,7 +21,8 @@ import (
 
 
 // ECR creates an ECR integration
 // ECR creates an ECR integration
 func ECR(
 func ECR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -52,13 +53,13 @@ Would you like to proceed? %s `,
 		creds, err := agent.CreateIAMECRUser(region)
 		creds, err := agent.CreateIAMECRUser(region)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
-			return ecrManual(client, projectID, region)
+			return ecrManual(ctx, client, projectID, region)
 		}
 		}
 
 
 		waitForAuthorizationToken(region, creds)
 		waitForAuthorizationToken(region, creds)
 
 
 		integration, err := client.CreateAWSIntegration(
 		integration, err := client.CreateAWSIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateAWSRequest{
 			&types.CreateAWSRequest{
 				AWSAccessKeyID:     creds.AWSAccessKeyID,
 				AWSAccessKeyID:     creds.AWSAccessKeyID,
@@ -72,14 +73,15 @@ Would you like to proceed? %s `,
 
 
 		color.New(color.FgGreen).Printf("created aws integration with id %d\n", integration.ID)
 		color.New(color.FgGreen).Printf("created aws integration with id %d\n", integration.ID)
 
 
-		return linkRegistry(client, projectID, integration.ID)
+		return linkRegistry(ctx, client, projectID, integration.ID)
 	}
 	}
 
 
-	return ecrManual(client, projectID, region)
+	return ecrManual(ctx, client, projectID, region)
 }
 }
 
 
 func ecrManual(
 func ecrManual(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 	region string,
 	region string,
 ) (uint, error) {
 ) (uint, error) {
@@ -102,7 +104,7 @@ func ecrManual(
 
 
 	// create the aws integration
 	// create the aws integration
 	integration, err := client.CreateAWSIntegration(
 	integration, err := client.CreateAWSIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateAWSRequest{
 		&types.CreateAWSRequest{
 			AWSAccessKeyID:     accessKeyID,
 			AWSAccessKeyID:     accessKeyID,
@@ -116,10 +118,10 @@ func ecrManual(
 
 
 	color.New(color.FgGreen).Printf("created aws integration with id %d\n", integration.ID)
 	color.New(color.FgGreen).Printf("created aws integration with id %d\n", integration.ID)
 
 
-	return linkRegistry(client, projectID, integration.ID)
+	return linkRegistry(ctx, client, projectID, integration.ID)
 }
 }
 
 
-func linkRegistry(client *api.Client, projectID uint, intID uint) (uint, error) {
+func linkRegistry(ctx context.Context, client api.Client, projectID uint, intID uint) (uint, error) {
 	// create the registry
 	// create the registry
 	// query for registry name
 	// query for registry name
 	regName, err := utils.PromptPlaintext(fmt.Sprintf(`Give this registry a name: `))
 	regName, err := utils.PromptPlaintext(fmt.Sprintf(`Give this registry a name: `))
@@ -128,7 +130,7 @@ func linkRegistry(client *api.Client, projectID uint, intID uint) (uint, error)
 	}
 	}
 
 
 	reg, err := client.CreateRegistry(
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateRegistryRequest{
 		&types.CreateRegistryRequest{
 			Name:             regName,
 			Name:             regName,

+ 4 - 3
cli/cmd/connect/gar.go

@@ -15,7 +15,8 @@ import (
 
 
 // GAR creates a GAR integration
 // GAR creates a GAR integration
 func GAR(
 func GAR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -39,7 +40,7 @@ Key file location: `)
 
 
 		// create the gcp integration
 		// create the gcp integration
 		integration, err := client.CreateGCPIntegration(
 		integration, err := client.CreateGCPIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateGCPRequest{
 			&types.CreateGCPRequest{
 				GCPKeyData: string(bytes),
 				GCPKeyData: string(bytes),
@@ -81,7 +82,7 @@ Artifact registry region: `)
 		}
 		}
 
 
 		reg, err := client.CreateRegistry(
 		reg, err := client.CreateRegistry(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateRegistryRequest{
 			&types.CreateRegistryRequest{
 				Name:             regName,
 				Name:             regName,

+ 4 - 3
cli/cmd/connect/gcr.go

@@ -15,7 +15,8 @@ import (
 
 
 // GCR creates a GCR integration
 // GCR creates a GCR integration
 func GCR(
 func GCR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -39,7 +40,7 @@ Key file location: `))
 
 
 		// create the gcp integration
 		// create the gcp integration
 		integration, err := client.CreateGCPIntegration(
 		integration, err := client.CreateGCPIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateGCPRequest{
 			&types.CreateGCPRequest{
 				GCPKeyData: string(bytes),
 				GCPKeyData: string(bytes),
@@ -65,7 +66,7 @@ Registry URL: `))
 		}
 		}
 
 
 		reg, err := client.CreateRegistry(
 		reg, err := client.CreateRegistry(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateRegistryRequest{
 			&types.CreateRegistryRequest{
 				Name:             regName,
 				Name:             regName,

+ 4 - 3
cli/cmd/connect/helmrepo.go

@@ -13,7 +13,8 @@ import (
 )
 )
 
 
 func HelmRepo(
 func HelmRepo(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -53,7 +54,7 @@ Password:`)
 	if username != "" && password != "" {
 	if username != "" && password != "" {
 		// create the basic auth integration
 		// create the basic auth integration
 		integration, err := client.CreateBasicAuthIntegration(
 		integration, err := client.CreateBasicAuthIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			&types.CreateBasicRequest{
 			&types.CreateBasicRequest{
 				Username: username,
 				Username: username,
@@ -70,7 +71,7 @@ Password:`)
 	}
 	}
 
 
 	reg, err := client.CreateHelmRepo(
 	reg, err := client.CreateHelmRepo(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateUpdateHelmRepoRequest{
 		&types.CreateUpdateHelmRepoRequest{
 			URL:                repoURL,
 			URL:                repoURL,

+ 8 - 5
cli/cmd/connect/kubeconfig.go

@@ -22,7 +22,8 @@ import (
 // Kubeconfig creates a service account for a project by parsing the local
 // Kubeconfig creates a service account for a project by parsing the local
 // kubeconfig and resolving actions that must be performed.
 // kubeconfig and resolving actions that must be performed.
 func Kubeconfig(
 func Kubeconfig(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	kubeconfigPath string,
 	kubeconfigPath string,
 	contexts []string,
 	contexts []string,
 	projectID uint,
 	projectID uint,
@@ -41,7 +42,7 @@ func Kubeconfig(
 
 
 	// send kubeconfig to client
 	// send kubeconfig to client
 	resp, err := client.CreateProjectCandidates(
 	resp, err := client.CreateProjectCandidates(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateClusterCandidateRequest{
 		&types.CreateClusterCandidateRequest{
 			Kubeconfig: string(rawBytes),
 			Kubeconfig: string(rawBytes),
@@ -166,6 +167,7 @@ func Kubeconfig(
 					}
 					}
 				case types.GCPKeyData:
 				case types.GCPKeyData:
 					err := resolveGCPKeyAction(
 					err := resolveGCPKeyAction(
+						ctx,
 						cc.Server,
 						cc.Server,
 						cc.Name,
 						cc.Name,
 						allResolver,
 						allResolver,
@@ -189,7 +191,7 @@ func Kubeconfig(
 			}
 			}
 
 
 			resp, err := client.CreateProjectCluster(
 			resp, err := client.CreateProjectCluster(
-				context.Background(),
+				ctx,
 				projectID,
 				projectID,
 				cc.ID,
 				cc.ID,
 				allResolver,
 				allResolver,
@@ -203,7 +205,7 @@ func Kubeconfig(
 			cluster = &clExt
 			cluster = &clExt
 		} else {
 		} else {
 			resp, err := client.GetProjectCluster(
 			resp, err := client.GetProjectCluster(
-				context.Background(),
+				ctx,
 				projectID,
 				projectID,
 				cc.CreatedClusterID,
 				cc.CreatedClusterID,
 			)
 			)
@@ -306,6 +308,7 @@ func resolveTokenDataAction(
 
 
 // resolves a gcp key data action
 // resolves a gcp key data action
 func resolveGCPKeyAction(
 func resolveGCPKeyAction(
+	ctx context.Context,
 	endpoint string,
 	endpoint string,
 	clusterName string,
 	clusterName string,
 	resolver *types.ClusterResolverAll,
 	resolver *types.ClusterResolverAll,
@@ -325,7 +328,7 @@ Would you like to proceed? %s `,
 	}
 	}
 
 
 	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
 	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
-		agent, err := gcpLocal.NewDefaultAgent()
+		agent, err := gcpLocal.NewDefaultAgent(ctx)
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)
 			return resolveGCPKeyActionManual(endpoint, clusterName, resolver)

+ 4 - 3
cli/cmd/connect/registry.go

@@ -13,7 +13,8 @@ import (
 
 
 // Helm connects a Helm repository using HTTP basic authentication
 // Helm connects a Helm repository using HTTP basic authentication
 func Registry(
 func Registry(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	projectID uint,
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
@@ -41,7 +42,7 @@ Username: `))
 
 
 	// create the basic auth integration
 	// create the basic auth integration
 	integration, err := client.CreateBasicAuthIntegration(
 	integration, err := client.CreateBasicAuthIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateBasicRequest{
 		&types.CreateBasicRequest{
 			Username: username,
 			Username: username,
@@ -55,7 +56,7 @@ Username: `))
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 
 
 	reg, err := client.CreateRegistry(
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		&types.CreateRegistryRequest{
 		&types.CreateRegistryRequest{
 			URL:                repoURL,
 			URL:                repoURL,

+ 0 - 262
cli/cmd/delete.go

@@ -1,262 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"strconv"
-
-	"github.com/fatih/color"
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/spf13/cobra"
-)
-
-// deleteCmd represents the "porter delete" base command
-var deleteCmd = &cobra.Command{
-	Use:   "delete",
-	Short: "Deletes a deployment",
-	Long: fmt.Sprintf(`
-%s
-
-Destroys a deployment, which is read based on env variables.
-
-  %s
-
-The following are the environment variables that can be used to set certain values while
-deleting a configuration:
-  PORTER_CLUSTER              Cluster ID that contains the project
-  PORTER_PROJECT              Project ID that contains the application
-	`,
-		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter delete\":"),
-		color.New(color.FgGreen, color.Bold).Sprintf("porter delete"),
-	),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteDeployment)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-// deleteAppsCmd represents the "porter delete apps" subcommand
-var deleteAppsCmd = &cobra.Command{
-	Use:     "apps",
-	Aliases: []string{"app", "applications", "application"},
-	Short:   "Deletes an existing app",
-	Args:    cobra.ExactArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteApp)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-// deleteJobsCmd represents the "porter delete jobs" subcommand
-var deleteJobsCmd = &cobra.Command{
-	Use:     "jobs",
-	Aliases: []string{"job"},
-	Short:   "Deletes an existing job",
-	Args:    cobra.ExactArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteJob)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-// deleteAddonsCmd represents the "porter delete addons" subcommand
-var deleteAddonsCmd = &cobra.Command{
-	Use:     "addons",
-	Aliases: []string{"addon"},
-	Short:   "Deletes an existing addon",
-	Args:    cobra.ExactArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteAddon)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-// deleteHelmCmd represents the "porter delete helm" subcommand
-var deleteHelmCmd = &cobra.Command{
-	Use:     "helm",
-	Aliases: []string{"helmrepo", "helmrepos"},
-	Short:   "Deletes an existing helm repo",
-	Args:    cobra.ExactArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, deleteHelm)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	deleteCmd.PersistentFlags().StringVar(
-		&namespace,
-		"namespace",
-		"default",
-		"Namespace of the application",
-	)
-
-	deleteCmd.AddCommand(deleteAppsCmd)
-	deleteCmd.AddCommand(deleteJobsCmd)
-	deleteCmd.AddCommand(deleteAddonsCmd)
-	deleteCmd.AddCommand(deleteHelmCmd)
-
-	rootCmd.AddCommand(deleteCmd)
-}
-
-func deleteDeployment(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	projectID := cliConf.Project
-
-	if projectID == 0 {
-		return fmt.Errorf("project id must be set")
-	}
-
-	clusterID := cliConf.Cluster
-
-	if clusterID == 0 {
-		return fmt.Errorf("cluster id must be set")
-	}
-
-	var deploymentID uint
-
-	if deplIDStr := os.Getenv("PORTER_DEPLOYMENT_ID"); deplIDStr != "" {
-		deplID, err := strconv.ParseUint(deplIDStr, 10, 32)
-		if err != nil {
-			return fmt.Errorf("error parsing deployment ID: %s", deplIDStr)
-		}
-
-		deploymentID = uint(deplID)
-	} else {
-		return fmt.Errorf("Deployment ID must be defined, set by PORTER_DEPLOYMENT_ID")
-	}
-
-	return client.DeleteDeployment(
-		context.Background(), projectID, clusterID, deploymentID,
-	)
-}
-
-func deleteApp(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	name := args[0]
-
-	resp, err := client.GetRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-	if err != nil {
-		return err
-	}
-
-	rel := *resp
-
-	if rel.Chart.Name() != "web" && rel.Chart.Name() != "worker" {
-		return fmt.Errorf("no app found with name: %s", name)
-	}
-
-	color.New(color.FgBlue).Printf("Deleting app: %s\n", name)
-
-	err = client.DeleteRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func deleteJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	name := args[0]
-
-	resp, err := client.GetRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-	if err != nil {
-		return err
-	}
-
-	rel := *resp
-
-	if rel.Chart.Name() != "job" {
-		return fmt.Errorf("no job found with name: %s", name)
-	}
-
-	color.New(color.FgBlue).Printf("Deleting job: %s\n", name)
-
-	err = client.DeleteRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func deleteAddon(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	name := args[0]
-
-	resp, err := client.GetRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-	if err != nil {
-		return err
-	}
-
-	rel := *resp
-
-	if rel.Chart.Name() == "web" || rel.Chart.Name() == "worker" || rel.Chart.Name() == "job" {
-		return fmt.Errorf("no addon found with name: %s", name)
-	}
-
-	color.New(color.FgBlue).Printf("Deleting addon: %s\n", name)
-
-	err = client.DeleteRelease(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, name,
-	)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func deleteHelm(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	name := args[0]
-
-	resp, err := client.ListHelmRepos(context.Background(), cliConf.Project)
-	if err != nil {
-		return err
-	}
-
-	var repo *types.HelmRepo
-
-	for _, r := range resp {
-		if r.Name == name {
-			repo = r
-			break
-		}
-	}
-
-	if repo == nil {
-		return fmt.Errorf("no helm repo found with name: %s", name)
-	}
-
-	color.New(color.FgBlue).Printf("Deleting helm repo: %s\n", name)
-
-	err = client.DeleteHelmRepo(context.Background(), cliConf.Project, repo.ID)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

+ 7 - 3
cli/cmd/deploy/build.go

@@ -1,6 +1,7 @@
 package deploy
 package deploy
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
@@ -16,7 +17,7 @@ import (
 type BuildAgent struct {
 type BuildAgent struct {
 	*SharedOpts
 	*SharedOpts
 
 
-	APIClient   *api.Client
+	APIClient   api.Client
 	ImageRepo   string
 	ImageRepo   string
 	Env         map[string]string
 	Env         map[string]string
 	ImageExists bool
 	ImageExists bool
@@ -24,6 +25,7 @@ type BuildAgent struct {
 
 
 // BuildDocker uses the local Docker daemon to build the image
 // BuildDocker uses the local Docker daemon to build the image
 func (b *BuildAgent) BuildDocker(
 func (b *BuildAgent) BuildDocker(
+	ctx context.Context,
 	dockerAgent *docker.Agent,
 	dockerAgent *docker.Agent,
 	basePath,
 	basePath,
 	buildCtx,
 	buildCtx,
@@ -52,15 +54,17 @@ func (b *BuildAgent) BuildDocker(
 	}
 	}
 
 
 	return dockerAgent.BuildLocal(
 	return dockerAgent.BuildLocal(
+		ctx,
 		opts,
 		opts,
 	)
 	)
 }
 }
 
 
 // BuildPack uses the cloud-native buildpack client to build a container image
 // BuildPack uses the cloud-native buildpack client to build a container image
-func (b *BuildAgent) BuildPack(dockerAgent *docker.Agent, dst, tag, prevTag string, buildConfig *types.BuildConfig) error {
+func (b *BuildAgent) BuildPack(ctx context.Context, dockerAgent *docker.Agent, dst, tag, prevTag string, buildConfig *types.BuildConfig) error {
 	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
 	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
 	if b.ImageExists {
 	if b.ImageExists {
 		err := dockerAgent.TagImage(
 		err := dockerAgent.TagImage(
+			ctx,
 			fmt.Sprintf("%s:%s", b.ImageRepo, prevTag),
 			fmt.Sprintf("%s:%s", b.ImageRepo, prevTag),
 			fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"),
 			fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"),
 		)
 		)
@@ -81,7 +85,7 @@ func (b *BuildAgent) BuildPack(dockerAgent *docker.Agent, dst, tag, prevTag stri
 	}
 	}
 
 
 	// call builder
 	// call builder
-	return packAgent.Build(opts, buildConfig, fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"))
+	return packAgent.Build(ctx, opts, buildConfig, fmt.Sprintf("%s:%s", b.ImageRepo, "pack-cache"))
 }
 }
 
 
 // ResolveDockerPaths returns a path to the dockerfile that is either relative or absolute, and a path
 // ResolveDockerPaths returns a path to the dockerfile that is either relative or absolute, and a path

+ 37 - 32
cli/cmd/deploy/create.go

@@ -15,7 +15,7 @@ import (
 
 
 // CreateAgent handles the creation of a new application on Porter
 // CreateAgent handles the creation of a new application on Porter
 type CreateAgent struct {
 type CreateAgent struct {
-	Client     *api.Client
+	Client     api.Client
 	CreateOpts *CreateOpts
 	CreateOpts *CreateOpts
 }
 }
 
 
@@ -43,6 +43,7 @@ type GithubOpts struct {
 // This function attempts to find a matching repository in the list of linked repositories
 // This function attempts to find a matching repository in the list of linked repositories
 // on Porter. If one is found, it will use that repository as the app source.
 // on Porter. If one is found, it will use that repository as the app source.
 func (c *CreateAgent) CreateFromGithub(
 func (c *CreateAgent) CreateFromGithub(
+	ctx context.Context,
 	ghOpts *GithubOpts,
 	ghOpts *GithubOpts,
 	overrideValues map[string]interface{},
 	overrideValues map[string]interface{},
 ) (string, error) {
 ) (string, error) {
@@ -50,7 +51,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 
 	// get all linked github repos and find matching repo
 	// get all linked github repos and find matching repo
 	resp, err := c.Client.ListGitInstallationIDs(
 	resp, err := c.Client.ListGitInstallationIDs(
-		context.Background(),
+		ctx,
 		c.CreateOpts.ProjectID,
 		c.CreateOpts.ProjectID,
 	)
 	)
 	if err != nil {
 	if err != nil {
@@ -64,7 +65,7 @@ func (c *CreateAgent) CreateFromGithub(
 	for _, gitInstallationID := range gitInstallations {
 	for _, gitInstallationID := range gitInstallations {
 		// for each git repo, search for a matching username/owner
 		// for each git repo, search for a matching username/owner
 		resp, err := c.Client.ListGitRepos(
 		resp, err := c.Client.ListGitRepos(
-			context.Background(),
+			ctx,
 			c.CreateOpts.ProjectID,
 			c.CreateOpts.ProjectID,
 			gitInstallationID,
 			gitInstallationID,
 		)
 		)
@@ -90,7 +91,7 @@ func (c *CreateAgent) CreateFromGithub(
 		return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
 		return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
 	}
 	}
 
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -107,18 +108,18 @@ func (c *CreateAgent) CreateFromGithub(
 		}
 		}
 	}
 	}
 
 
-	regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
+	regID, imageURL, err := c.GetImageRepoURL(ctx, opts.ReleaseName, opts.Namespace)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	err = c.Client.DeployTemplate(
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ProjectID,
 		opts.ClusterID,
 		opts.ClusterID,
 		opts.Namespace,
 		opts.Namespace,
@@ -152,6 +153,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 
 // CreateFromRegistry deploys a new application from an existing Docker repository + tag.
 // CreateFromRegistry deploys a new application from an existing Docker repository + tag.
 func (c *CreateAgent) CreateFromRegistry(
 func (c *CreateAgent) CreateFromRegistry(
+	ctx context.Context,
 	image string,
 	image string,
 	overrideValues map[string]interface{},
 	overrideValues map[string]interface{},
 ) (string, error) {
 ) (string, error) {
@@ -168,7 +170,7 @@ func (c *CreateAgent) CreateFromRegistry(
 
 
 	opts := c.CreateOpts
 	opts := c.CreateOpts
 
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -178,13 +180,13 @@ func (c *CreateAgent) CreateFromRegistry(
 		"tag":        imageSpl[1],
 		"tag":        imageSpl[1],
 	}
 	}
 
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	err = c.Client.DeployTemplate(
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ProjectID,
 		opts.ClusterID,
 		opts.ClusterID,
 		opts.Namespace,
 		opts.Namespace,
@@ -209,6 +211,7 @@ func (c *CreateAgent) CreateFromRegistry(
 // CreateFromDocker uses a local build context and a local Docker daemon to build a new
 // CreateFromDocker uses a local build context and a local Docker daemon to build a new
 // container image, and then deploys it onto Porter.
 // container image, and then deploys it onto Porter.
 func (c *CreateAgent) CreateFromDocker(
 func (c *CreateAgent) CreateFromDocker(
+	ctx context.Context,
 	overrideValues map[string]interface{},
 	overrideValues map[string]interface{},
 	imageTag string,
 	imageTag string,
 	extraBuildConfig *types.BuildConfig,
 	extraBuildConfig *types.BuildConfig,
@@ -241,12 +244,12 @@ func (c *CreateAgent) CreateFromDocker(
 	}
 	}
 
 
 	// overwrite with docker image repository and tag
 	// overwrite with docker image repository and tag
-	regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
+	regID, imageURL, err := c.GetImageRepoURL(ctx, opts.ReleaseName, opts.Namespace)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -257,12 +260,12 @@ func (c *CreateAgent) CreateFromDocker(
 	}
 	}
 
 
 	// create docker agent
 	// create docker agent
-	agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
+	agent, err := docker.NewAgentWithAuthGetter(ctx, c.Client, opts.ProjectID)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	env, err := GetEnvForRelease(c.Client, mergedValues, opts.ProjectID, opts.ClusterID, opts.Namespace)
+	env, err := GetEnvForRelease(ctx, c.Client, mergedValues, opts.ProjectID, opts.ClusterID, opts.Namespace)
 	if err != nil {
 	if err != nil {
 		env = make(map[string]string)
 		env = make(map[string]string)
 	}
 	}
@@ -307,9 +310,9 @@ func (c *CreateAgent) CreateFromDocker(
 			return "", err
 			return "", err
 		}
 		}
 
 
-		err = buildAgent.BuildDocker(agent, basePath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
+		err = buildAgent.BuildDocker(ctx, agent, basePath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
 	} else {
 	} else {
-		err = buildAgent.BuildPack(agent, opts.LocalPath, imageTag, "", extraBuildConfig)
+		err = buildAgent.BuildPack(ctx, agent, opts.LocalPath, imageTag, "", extraBuildConfig)
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
@@ -319,7 +322,7 @@ func (c *CreateAgent) CreateFromDocker(
 	if !opts.SharedOpts.UseCache {
 	if !opts.SharedOpts.UseCache {
 		// create repository
 		// create repository
 		err = c.Client.CreateRepository(
 		err = c.Client.CreateRepository(
-			context.Background(),
+			ctx,
 			opts.ProjectID,
 			opts.ProjectID,
 			regID,
 			regID,
 			&types.CreateRegistryRepositoryRequest{
 			&types.CreateRegistryRepositoryRequest{
@@ -331,20 +334,20 @@ func (c *CreateAgent) CreateFromDocker(
 			return "", err
 			return "", err
 		}
 		}
 
 
-		err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, imageTag))
+		err = agent.PushImage(ctx, fmt.Sprintf("%s:%s", imageURL, imageTag))
 
 
 		if err != nil {
 		if err != nil {
 			return "", err
 			return "", err
 		}
 		}
 	}
 	}
 
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	err = c.Client.DeployTemplate(
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ProjectID,
 		opts.ClusterID,
 		opts.ClusterID,
 		opts.Namespace,
 		opts.Namespace,
@@ -378,11 +381,11 @@ func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
 // GetImageRepoURL creates the image repository url by finding the first valid image
 // GetImageRepoURL creates the image repository url by finding the first valid image
 // registry linked to Porter, and then generates a new name of the form:
 // registry linked to Porter, and then generates a new name of the form:
 // `{registry}/{name}-{namespace}`
 // `{registry}/{name}-{namespace}`
-func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
+func (c *CreateAgent) GetImageRepoURL(ctx context.Context, name, namespace string) (uint, string, error) {
 	// get all image registries linked to the project
 	// get all image registries linked to the project
 	// get the list of namespaces
 	// get the list of namespaces
 	resp, err := c.Client.ListRegistries(
 	resp, err := c.Client.ListRegistries(
-		context.Background(),
+		ctx,
 		c.CreateOpts.ProjectID,
 		c.CreateOpts.ProjectID,
 	)
 	)
 
 
@@ -436,9 +439,9 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 
 
 // GetLatestTemplateVersion retrieves the latest template version for a specific
 // GetLatestTemplateVersion retrieves the latest template version for a specific
 // Porter template from the chart repository.
 // Porter template from the chart repository.
-func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
+func (c *CreateAgent) GetLatestTemplateVersion(ctx context.Context, templateName string) (string, error) {
 	resp, err := c.Client.ListTemplates(
 	resp, err := c.Client.ListTemplates(
-		context.Background(),
+		ctx,
 		c.CreateOpts.ProjectID,
 		c.CreateOpts.ProjectID,
 		&types.ListTemplatesRequest{},
 		&types.ListTemplatesRequest{},
 	)
 	)
@@ -466,9 +469,9 @@ func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, err
 
 
 // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
 // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
 // template.
 // template.
-func (c *CreateAgent) GetLatestTemplateDefaultValues(projectID uint, templateName, templateVersion string) (map[string]interface{}, error) {
+func (c *CreateAgent) GetLatestTemplateDefaultValues(ctx context.Context, projectID uint, templateName, templateVersion string) (map[string]interface{}, error) {
 	chart, err := c.Client.GetTemplate(
 	chart, err := c.Client.GetTemplate(
-		context.Background(),
+		ctx,
 		projectID,
 		projectID,
 		templateName,
 		templateName,
 		templateVersion,
 		templateVersion,
@@ -481,20 +484,21 @@ func (c *CreateAgent) GetLatestTemplateDefaultValues(projectID uint, templateNam
 	return chart.Values, nil
 	return chart.Values, nil
 }
 }
 
 
-func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
+// GetMergedValues merges exsting values with their overrides
+func (c *CreateAgent) GetMergedValues(ctx context.Context, overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
 	// deploy the template
 	// deploy the template
-	latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
+	latestVersion, err := c.GetLatestTemplateVersion(ctx, c.CreateOpts.Kind)
 	if err != nil {
 	if err != nil {
 		return "", nil, err
 		return "", nil, err
 	}
 	}
 
 
 	// get the values of the template
 	// get the values of the template
-	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.ProjectID, c.CreateOpts.Kind, latestVersion)
+	values, err := c.GetLatestTemplateDefaultValues(ctx, c.CreateOpts.ProjectID, c.CreateOpts.Kind, latestVersion)
 	if err != nil {
 	if err != nil {
 		return "", nil, err
 		return "", nil, err
 	}
 	}
 
 
-	err = coalesceEnvGroups(c.Client, c.CreateOpts.ProjectID, c.CreateOpts.ClusterID,
+	err = coalesceEnvGroups(ctx, c.Client, c.CreateOpts.ProjectID, c.CreateOpts.ClusterID,
 		c.CreateOpts.Namespace, c.CreateOpts.EnvGroups, values)
 		c.CreateOpts.Namespace, c.CreateOpts.EnvGroups, values)
 
 
 	if err != nil {
 	if err != nil {
@@ -507,7 +511,8 @@ func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (st
 	return latestVersion, mergedValues, err
 	return latestVersion, mergedValues, err
 }
 }
 
 
-func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
+// CreateSubdomainIfRequired checks if a subdomain needs created, then creates one
+func (c *CreateAgent) CreateSubdomainIfRequired(ctx context.Context, mergedValues map[string]interface{}) (string, error) {
 	subdomain := ""
 	subdomain := ""
 
 
 	// check for automatic subdomain creation if web kind
 	// check for automatic subdomain creation if web kind
@@ -543,7 +548,7 @@ func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interfac
 					} else {
 					} else {
 						// in the case of ingress enabled but no custom domain, create subdomain
 						// in the case of ingress enabled but no custom domain, create subdomain
 						dnsRecord, err := c.Client.CreateDNSRecord(
 						dnsRecord, err := c.Client.CreateDNSRecord(
-							context.Background(),
+							ctx,
 							c.CreateOpts.ProjectID,
 							c.CreateOpts.ProjectID,
 							c.CreateOpts.ClusterID,
 							c.CreateOpts.ClusterID,
 							c.CreateOpts.Namespace,
 							c.CreateOpts.Namespace,

+ 29 - 25
cli/cmd/deploy/deploy.go

@@ -32,7 +32,7 @@ const (
 type DeployAgent struct {
 type DeployAgent struct {
 	App string
 	App string
 
 
-	Client         *client.Client
+	Client         client.Client
 	Opts           *DeployOpts
 	Opts           *DeployOpts
 	Release        *types.GetReleaseResponse
 	Release        *types.GetReleaseResponse
 	agent          *docker.Agent
 	agent          *docker.Agent
@@ -53,7 +53,7 @@ type DeployOpts struct {
 
 
 // NewDeployAgent creates a new DeployAgent given a Porter API client, application
 // NewDeployAgent creates a new DeployAgent given a Porter API client, application
 // name, and DeployOpts.
 // name, and DeployOpts.
-func NewDeployAgent(client *client.Client, app string, opts *DeployOpts) (*DeployAgent, error) {
+func NewDeployAgent(ctx context.Context, client client.Client, app string, opts *DeployOpts) (*DeployAgent, error) {
 	deployAgent := &DeployAgent{
 	deployAgent := &DeployAgent{
 		App:    app,
 		App:    app,
 		Opts:   opts,
 		Opts:   opts,
@@ -75,7 +75,7 @@ func NewDeployAgent(client *client.Client, app string, opts *DeployOpts) (*Deplo
 	))
 	))
 
 
 	// get docker agent
 	// get docker agent
-	agent, err := docker.NewAgentWithAuthGetter(client, opts.ProjectID)
+	agent, err := docker.NewAgentWithAuthGetter(ctx, client, opts.ProjectID)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -134,10 +134,10 @@ func NewDeployAgent(client *client.Client, app string, opts *DeployOpts) (*Deplo
 
 
 	deployAgent.tag = opts.OverrideTag
 	deployAgent.tag = opts.OverrideTag
 
 
-	err = coalesceEnvGroups(deployAgent.Client, deployAgent.Opts.ProjectID, deployAgent.Opts.ClusterID,
+	err = coalesceEnvGroups(ctx, deployAgent.Client, deployAgent.Opts.ProjectID, deployAgent.Opts.ClusterID,
 		deployAgent.Opts.Namespace, deployAgent.Opts.EnvGroups, deployAgent.Release.Config)
 		deployAgent.Opts.Namespace, deployAgent.Opts.EnvGroups, deployAgent.Release.Config)
 
 
-	deployAgent.imageExists = deployAgent.agent.CheckIfImageExists(deployAgent.imageRepo, deployAgent.tag)
+	deployAgent.imageExists = deployAgent.agent.CheckIfImageExists(ctx, deployAgent.imageRepo, deployAgent.tag)
 
 
 	return deployAgent, err
 	return deployAgent, err
 }
 }
@@ -154,7 +154,7 @@ type GetBuildEnvOpts struct {
 //  2. container.env.build from the release config
 //  2. container.env.build from the release config
 //  3. container.env.synced from the release config
 //  3. container.env.synced from the release config
 //  4. any additional env var that was passed into the DeployAgent as opts.SharedOpts.AdditionalEnv
 //  4. any additional env var that was passed into the DeployAgent as opts.SharedOpts.AdditionalEnv
-func (d *DeployAgent) GetBuildEnv(opts *GetBuildEnvOpts) (map[string]string, error) {
+func (d *DeployAgent) GetBuildEnv(ctx context.Context, opts *GetBuildEnvOpts) (map[string]string, error) {
 	conf := d.Release.Config
 	conf := d.Release.Config
 
 
 	if opts.UseNewConfig {
 	if opts.UseNewConfig {
@@ -163,7 +163,7 @@ func (d *DeployAgent) GetBuildEnv(opts *GetBuildEnvOpts) (map[string]string, err
 		}
 		}
 	}
 	}
 
 
-	env, err := GetEnvForRelease(d.Client, conf, d.Opts.ProjectID, d.Opts.ClusterID, d.Opts.Namespace)
+	env, err := GetEnvForRelease(ctx, d.Client, conf, d.Opts.ProjectID, d.Opts.ClusterID, d.Opts.Namespace)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -240,7 +240,7 @@ func (d *DeployAgent) WriteBuildEnv(fileDest string) error {
 
 
 // Build uses the deploy agent options to build a new container image from either
 // Build uses the deploy agent options to build a new container image from either
 // buildpack or docker.
 // buildpack or docker.
-func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
+func (d *DeployAgent) Build(ctx context.Context, overrideBuildConfig *types.BuildConfig) error {
 	// retrieve current image to use for cache
 	// retrieve current image to use for cache
 	currImageSection := d.Release.Config["image"].(map[string]interface{})
 	currImageSection := d.Release.Config["image"].(map[string]interface{})
 	currentTag := currImageSection["tag"].(string)
 	currentTag := currImageSection["tag"].(string)
@@ -263,7 +263,7 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 		}
 		}
 
 
 		zipResp, err := d.Client.GetRepoZIPDownloadURL(
 		zipResp, err := d.Client.GetRepoZIPDownloadURL(
-			context.Background(),
+			ctx,
 			d.Opts.ProjectID,
 			d.Opts.ProjectID,
 			int64(d.Release.GitActionConfig.GitRepoID),
 			int64(d.Release.GitActionConfig.GitRepoID),
 			"github",
 			"github",
@@ -294,7 +294,7 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 		}
 		}
 	}
 	}
 
 
-	currTag, err := d.pullCurrentReleaseImage()
+	currTag, err := d.pullCurrentReleaseImage(ctx)
 
 
 	// if image is not found, don't return an error
 	// if image is not found, don't return an error
 	if err != nil && err != docker.PullImageErrNotFound {
 	if err != nil && err != docker.PullImageErrNotFound {
@@ -311,6 +311,7 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 
 
 	if d.Opts.Method == DeployBuildTypeDocker {
 	if d.Opts.Method == DeployBuildTypeDocker {
 		return buildAgent.BuildDocker(
 		return buildAgent.BuildDocker(
+			ctx,
 			d.agent,
 			d.agent,
 			basePath,
 			basePath,
 			buildCtx,
 			buildCtx,
@@ -326,21 +327,21 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 		buildConfig = overrideBuildConfig
 		buildConfig = overrideBuildConfig
 	}
 	}
 
 
-	return buildAgent.BuildPack(d.agent, buildCtx, d.tag, currTag, buildConfig)
+	return buildAgent.BuildPack(ctx, d.agent, buildCtx, d.tag, currTag, buildConfig)
 }
 }
 
 
 // Push pushes a local image to the remote repository linked in the release
 // Push pushes a local image to the remote repository linked in the release
-func (d *DeployAgent) Push() error {
-	return d.agent.PushImage(fmt.Sprintf("%s:%s", d.imageRepo, d.tag))
+func (d *DeployAgent) Push(ctx context.Context) error {
+	return d.agent.PushImage(ctx, fmt.Sprintf("%s:%s", d.imageRepo, d.tag))
 }
 }
 
 
 // UpdateImageAndValues updates the current image for a release, along with new
 // UpdateImageAndValues updates the current image for a release, along with new
 // configuration passed in via overrrideValues. If overrideValues is nil, it just
 // configuration passed in via overrrideValues. If overrideValues is nil, it just
 // reuses the configuration set for the application. If overrideValues is not nil,
 // reuses the configuration set for the application. If overrideValues is not nil,
 // it will merge the overriding values with the existing configuration.
 // it will merge the overriding values with the existing configuration.
-func (d *DeployAgent) UpdateImageAndValues(overrideValues map[string]interface{}) error {
+func (d *DeployAgent) UpdateImageAndValues(ctx context.Context, overrideValues map[string]interface{}) error {
 	// we should fetch the latest release and its config
 	// we should fetch the latest release and its config
-	release, err := d.Client.GetRelease(context.TODO(), d.Opts.ProjectID, d.Opts.ClusterID, d.Opts.Namespace, d.App)
+	release, err := d.Client.GetRelease(ctx, d.Opts.ProjectID, d.Opts.ClusterID, d.Opts.Namespace, d.App)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -396,7 +397,7 @@ func (d *DeployAgent) UpdateImageAndValues(overrideValues map[string]interface{}
 	}
 	}
 
 
 	return d.Client.UpgradeRelease(
 	return d.Client.UpgradeRelease(
-		context.Background(),
+		ctx,
 		d.Opts.ProjectID,
 		d.Opts.ProjectID,
 		d.Opts.ClusterID,
 		d.Opts.ClusterID,
 		d.Release.Namespace,
 		d.Release.Namespace,
@@ -421,7 +422,8 @@ type SyncedEnvSectionKey struct {
 // GetEnvForRelease gets the env vars for a standard Porter template config. These env
 // GetEnvForRelease gets the env vars for a standard Porter template config. These env
 // vars are found at `container.env.normal` and `container.env.synced`.
 // vars are found at `container.env.normal` and `container.env.synced`.
 func GetEnvForRelease(
 func GetEnvForRelease(
-	client *client.Client,
+	ctx context.Context,
+	client client.Client,
 	config map[string]interface{},
 	config map[string]interface{},
 	projID, clusterID uint,
 	projID, clusterID uint,
 	namespace string,
 	namespace string,
@@ -440,7 +442,7 @@ func GetEnvForRelease(
 
 
 	// next, get the env vars specified by "container.env.synced"
 	// next, get the env vars specified by "container.env.synced"
 	// look for container.env.synced
 	// look for container.env.synced
-	syncedEnv, err := GetSyncedEnv(client, config, projID, clusterID, namespace, true)
+	syncedEnv, err := GetSyncedEnv(ctx, client, config, projID, clusterID, namespace, true)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("error while fetching container.env.synced variables: %w", err)
 		return nil, fmt.Errorf("error while fetching container.env.synced variables: %w", err)
 	}
 	}
@@ -453,7 +455,7 @@ func GetEnvForRelease(
 }
 }
 
 
 func GetNormalEnv(
 func GetNormalEnv(
-	client *client.Client,
+	client client.Client,
 	config map[string]interface{},
 	config map[string]interface{},
 	projID, clusterID uint,
 	projID, clusterID uint,
 	namespace string,
 	namespace string,
@@ -487,7 +489,8 @@ func GetNormalEnv(
 }
 }
 
 
 func GetSyncedEnv(
 func GetSyncedEnv(
-	client *client.Client,
+	ctx context.Context,
+	client client.Client,
 	config map[string]interface{},
 	config map[string]interface{},
 	projID, clusterID uint,
 	projID, clusterID uint,
 	namespace string,
 	namespace string,
@@ -590,7 +593,7 @@ func GetSyncedEnv(
 
 
 		for _, syncedEG := range syncedArr {
 		for _, syncedEG := range syncedArr {
 			// for each synced environment group, get the environment group from the client
 			// for each synced environment group, get the environment group from the client
-			eg, err := client.GetEnvGroup(context.Background(), projID, clusterID, namespace,
+			eg, err := client.GetEnvGroup(ctx, projID, clusterID, namespace,
 				&types.GetEnvGroupRequest{
 				&types.GetEnvGroupRequest{
 					Name: syncedEG.Name,
 					Name: syncedEG.Name,
 				},
 				},
@@ -638,7 +641,7 @@ func (d *DeployAgent) getReleaseImage() (string, error) {
 	return repoStr, nil
 	return repoStr, nil
 }
 }
 
 
-func (d *DeployAgent) pullCurrentReleaseImage() (string, error) {
+func (d *DeployAgent) pullCurrentReleaseImage(ctx context.Context) (string, error) {
 	// pull the currently deployed image to use cache, if possible
 	// pull the currently deployed image to use cache, if possible
 	imageConfig, err := GetNestedMap(d.Release.Config, "image")
 	imageConfig, err := GetNestedMap(d.Release.Config, "image")
 	if err != nil {
 	if err != nil {
@@ -665,7 +668,7 @@ func (d *DeployAgent) pullCurrentReleaseImage() (string, error) {
 
 
 	fmt.Printf("attempting to pull image: %s\n", fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
 	fmt.Printf("attempting to pull image: %s\n", fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
 
 
-	return tagStr, d.agent.PullImage(fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
+	return tagStr, d.agent.PullImage(ctx, fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
 }
 }
 
 
 func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
 func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
@@ -706,9 +709,10 @@ func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
 	return res, nil
 	return res, nil
 }
 }
 
 
-func (d *DeployAgent) StreamEvent(event types.SubEvent) error {
+// StreamEvent streams events from the deploy agent
+func (d *DeployAgent) StreamEvent(ctx context.Context, event types.SubEvent) error {
 	return d.Client.CreateEvent(
 	return d.Client.CreateEvent(
-		context.Background(),
+		ctx,
 		d.Opts.ProjectID, d.Opts.ClusterID,
 		d.Opts.ProjectID, d.Opts.ClusterID,
 		d.Release.Namespace, d.Release.Name,
 		d.Release.Namespace, d.Release.Name,
 		&types.UpdateReleaseStepsRequest{
 		&types.UpdateReleaseStepsRequest{

+ 3 - 2
cli/cmd/deploy/shared.go

@@ -23,7 +23,8 @@ type SharedOpts struct {
 }
 }
 
 
 func coalesceEnvGroups(
 func coalesceEnvGroups(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID, clusterID uint,
 	projectID, clusterID uint,
 	namespace string,
 	namespace string,
 	envGroups []types.EnvGroupMeta,
 	envGroups []types.EnvGroupMeta,
@@ -35,7 +36,7 @@ func coalesceEnvGroups(
 		}
 		}
 
 
 		envGroup, err := client.GetEnvGroup(
 		envGroup, err := client.GetEnvGroup(
-			context.Background(),
+			ctx,
 			projectID,
 			projectID,
 			clusterID,
 			clusterID,
 			namespace,
 			namespace,

+ 4 - 3
cli/cmd/deploy/wait/job.go

@@ -17,9 +17,10 @@ type WaitOpts struct {
 }
 }
 
 
 // WaitForJob waits for a job with a given name/namespace to complete its run
 // WaitForJob waits for a job with a given name/namespace to complete its run
-func WaitForJob(client *api.Client, opts *WaitOpts) error {
+// nolint:revive // bad naming convention
+func WaitForJob(ctx context.Context, client api.Client, opts *WaitOpts) error {
 	// get the job release
 	// get the job release
-	jobRelease, err := client.GetRelease(context.Background(), opts.ProjectID, opts.ClusterID, opts.Namespace, opts.Name)
+	jobRelease, err := client.GetRelease(ctx, opts.ProjectID, opts.ClusterID, opts.Namespace, opts.Name)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -47,7 +48,7 @@ func WaitForJob(client *api.Client, opts *WaitOpts) error {
 
 
 	for time.Now().Before(timeWait) {
 	for time.Now().Before(timeWait) {
 		// get the jobs for that job chart
 		// get the jobs for that job chart
-		jobs, err := client.GetJobs(context.Background(), opts.ProjectID, opts.ClusterID, opts.Namespace, opts.Name)
+		jobs, err := client.GetJobs(ctx, opts.ProjectID, opts.ClusterID, opts.Namespace, opts.Name)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 0 - 36
cli/cmd/docker.go

@@ -1,36 +0,0 @@
-package cmd
-
-import (
-	"os"
-
-	api "github.com/porter-dev/porter/api/client"
-	ptypes "github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/cli/cmd/config"
-	"github.com/spf13/cobra"
-)
-
-var dockerCmd = &cobra.Command{
-	Use:   "docker",
-	Short: "Commands to configure Docker for a project",
-}
-
-var configureCmd = &cobra.Command{
-	Use:   "configure",
-	Short: "Configures the host's Docker instance",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, dockerConfig)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(dockerCmd)
-
-	dockerCmd.AddCommand(configureCmd)
-}
-
-func dockerConfig(user *ptypes.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	return config.SetDockerConfig(client)
-}

+ 41 - 41
cli/cmd/docker/agent.go

@@ -28,15 +28,14 @@ import (
 type Agent struct {
 type Agent struct {
 	*client.Client
 	*client.Client
 	authGetter *AuthGetter
 	authGetter *AuthGetter
-	ctx        context.Context
 	label      string
 	label      string
 }
 }
 
 
 // CreateLocalVolumeIfNotExist creates a volume using driver type "local" with the
 // CreateLocalVolumeIfNotExist creates a volume using driver type "local" with the
 // given name if it does not exist. If the volume does exist but does not contain
 // given name if it does not exist. If the volume does exist but does not contain
 // the required label (a.label), an error is thrown.
 // the required label (a.label), an error is thrown.
-func (a *Agent) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error) {
-	volListBody, err := a.VolumeList(a.ctx, filters.Args{})
+func (a *Agent) CreateLocalVolumeIfNotExist(ctx context.Context, name string) (*types.Volume, error) {
+	volListBody, err := a.VolumeList(ctx, filters.Args{})
 	if err != nil {
 	if err != nil {
 		return nil, a.handleDockerClientErr(err, "Could not list volumes")
 		return nil, a.handleDockerClientErr(err, "Could not list volumes")
 	}
 	}
@@ -49,14 +48,14 @@ func (a *Agent) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error)
 		}
 		}
 	}
 	}
 
 
-	return a.CreateLocalVolume(name)
+	return a.CreateLocalVolume(ctx, name)
 }
 }
 
 
 // CreateLocalVolume creates a volume using driver type "local" with no
 // CreateLocalVolume creates a volume using driver type "local" with no
 // configured options. The equivalent of:
 // configured options. The equivalent of:
 //
 //
 // docker volume create --driver local [name]
 // docker volume create --driver local [name]
-func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
+func (a *Agent) CreateLocalVolume(ctx context.Context, name string) (*types.Volume, error) {
 	labels := make(map[string]string)
 	labels := make(map[string]string)
 	labels[a.label] = "true"
 	labels[a.label] = "true"
 
 
@@ -66,7 +65,7 @@ func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
 		Labels: labels,
 		Labels: labels,
 	}
 	}
 
 
-	vol, err := a.VolumeCreate(a.ctx, opts)
+	vol, err := a.VolumeCreate(ctx, opts)
 	if err != nil {
 	if err != nil {
 		return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
 		return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
 	}
 	}
@@ -75,15 +74,15 @@ func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
 }
 }
 
 
 // RemoveLocalVolume removes a volume by name
 // RemoveLocalVolume removes a volume by name
-func (a *Agent) RemoveLocalVolume(name string) error {
-	return a.VolumeRemove(a.ctx, name, true)
+func (a *Agent) RemoveLocalVolume(ctx context.Context, name string) error {
+	return a.VolumeRemove(ctx, name, true)
 }
 }
 
 
 // CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
 // CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
 // given name if it does not exist. If the volume does exist but does not contain
 // given name if it does not exist. If the volume does exist but does not contain
 // the required label (a.label), an error is thrown.
 // the required label (a.label), an error is thrown.
-func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error) {
-	networks, err := a.NetworkList(a.ctx, types.NetworkListOptions{})
+func (a *Agent) CreateBridgeNetworkIfNotExist(ctx context.Context, name string) (id string, err error) {
+	networks, err := a.NetworkList(ctx, types.NetworkListOptions{})
 	if err != nil {
 	if err != nil {
 		return "", a.handleDockerClientErr(err, "Could not list volumes")
 		return "", a.handleDockerClientErr(err, "Could not list volumes")
 	}
 	}
@@ -96,12 +95,12 @@ func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error
 		}
 		}
 	}
 	}
 
 
-	return a.CreateBridgeNetwork(name)
+	return a.CreateBridgeNetwork(ctx, name)
 }
 }
 
 
 // CreateBridgeNetwork creates a volume using the default driver type (bridge)
 // CreateBridgeNetwork creates a volume using the default driver type (bridge)
 // with the CLI label attached
 // with the CLI label attached
-func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
+func (a *Agent) CreateBridgeNetwork(ctx context.Context, name string) (id string, err error) {
 	labels := make(map[string]string)
 	labels := make(map[string]string)
 	labels[a.label] = "true"
 	labels[a.label] = "true"
 
 
@@ -110,7 +109,7 @@ func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
 		Attachable: true,
 		Attachable: true,
 	}
 	}
 
 
-	net, err := a.NetworkCreate(a.ctx, name, opts)
+	net, err := a.NetworkCreate(ctx, name, opts)
 	if err != nil {
 	if err != nil {
 		return "", a.handleDockerClientErr(err, "Could not create network "+name)
 		return "", a.handleDockerClientErr(err, "Could not create network "+name)
 	}
 	}
@@ -119,9 +118,9 @@ func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
 }
 }
 
 
 // ConnectContainerToNetwork attaches a container to a specified network
 // ConnectContainerToNetwork attaches a container to a specified network
-func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName string) error {
+func (a *Agent) ConnectContainerToNetwork(ctx context.Context, networkID, containerID, containerName string) error {
 	// check if the container is connected already
 	// check if the container is connected already
-	net, err := a.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
+	net, err := a.NetworkInspect(ctx, networkID, types.NetworkInspectOptions{})
 	if err != nil {
 	if err != nil {
 		return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
 		return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
 	}
 	}
@@ -133,11 +132,12 @@ func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName
 		}
 		}
 	}
 	}
 
 
-	return a.NetworkConnect(a.ctx, networkID, containerID, &network.EndpointSettings{})
+	return a.NetworkConnect(ctx, networkID, containerID, &network.EndpointSettings{})
 }
 }
 
 
-func (a *Agent) TagImage(old, new string) error {
-	return a.ImageTag(a.ctx, old, new)
+// TagImage tags an image
+func (a *Agent) TagImage(ctx context.Context, old, new string) error {
+	return a.ImageTag(ctx, old, new)
 }
 }
 
 
 // PullImageEvent represents a response from the Docker API with an image pull event
 // PullImageEvent represents a response from the Docker API with an image pull event
@@ -167,13 +167,13 @@ func getRegistryRepositoryPair(imageRepo string) ([]string, error) {
 }
 }
 
 
 // CheckIfImageExists checks if the image exists in the registry
 // CheckIfImageExists checks if the image exists in the registry
-func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
-	registryToken, err := a.getContainerRegistryToken(imageRepo)
+func (a *Agent) CheckIfImageExists(ctx context.Context, imageRepo, imageTag string) bool {
+	registryToken, err := a.getContainerRegistryToken(ctx, imageRepo)
 	if err != nil {
 	if err != nil {
 		return false
 		return false
 	}
 	}
 
 
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ctx, cancel := context.WithTimeout(ctx, time.Second*5)
 	defer cancel()
 	defer cancel()
 
 
 	if strings.Contains(imageRepo, "gcr.io") {
 	if strings.Contains(imageRepo, "gcr.io") {
@@ -248,12 +248,12 @@ func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
 	}
 	}
 
 
 	image := imageRepo + ":" + imageTag
 	image := imageRepo + ":" + imageTag
-	encodedRegistryAuth, err := a.getEncodedRegistryAuth(image)
+	encodedRegistryAuth, err := a.getEncodedRegistryAuth(ctx, image)
 	if err != nil {
 	if err != nil {
 		return false
 		return false
 	}
 	}
 
 
-	_, err = a.DistributionInspect(context.Background(), image, encodedRegistryAuth)
+	_, err = a.DistributionInspect(ctx, image, encodedRegistryAuth)
 
 
 	if err == nil {
 	if err == nil {
 		return true
 		return true
@@ -266,14 +266,14 @@ func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
 }
 }
 
 
 // PullImage pulls an image specified by the image string
 // PullImage pulls an image specified by the image string
-func (a *Agent) PullImage(image string) error {
-	opts, err := a.getPullOptions(image)
+func (a *Agent) PullImage(ctx context.Context, image string) error {
+	opts, err := a.getPullOptions(ctx, image)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	// pull the specified image
 	// pull the specified image
-	out, err := a.ImagePull(a.ctx, image, opts)
+	out, err := a.ImagePull(ctx, image, opts)
 	if err != nil {
 	if err != nil {
 		if client.IsErrNotFound(err) ||
 		if client.IsErrNotFound(err) ||
 			(strings.Contains(image, "gcr.io") && strings.Contains(err.Error(), "or it may not exist")) {
 			(strings.Contains(image, "gcr.io") && strings.Contains(err.Error(), "or it may not exist")) {
@@ -293,14 +293,14 @@ func (a *Agent) PullImage(image string) error {
 }
 }
 
 
 // PushImage pushes an image specified by the image string
 // PushImage pushes an image specified by the image string
-func (a *Agent) PushImage(image string) error {
-	opts, err := a.getPushOptions(image)
+func (a *Agent) PushImage(ctx context.Context, image string) error {
+	opts, err := a.getPushOptions(ctx, image)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	out, err := a.ImagePush(
 	out, err := a.ImagePush(
-		context.Background(),
+		ctx,
 		image,
 		image,
 		opts,
 		opts,
 	)
 	)
@@ -323,13 +323,13 @@ func (a *Agent) PushImage(image string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (a *Agent) getPullOptions(image string) (types.ImagePullOptions, error) {
+func (a *Agent) getPullOptions(ctx context.Context, image string) (types.ImagePullOptions, error) {
 	// check if agent has an auth getter; otherwise, assume public usage
 	// check if agent has an auth getter; otherwise, assume public usage
 	if a.authGetter == nil {
 	if a.authGetter == nil {
 		return types.ImagePullOptions{}, nil
 		return types.ImagePullOptions{}, nil
 	}
 	}
 
 
-	authConfigEncoded, err := a.getEncodedRegistryAuth(image)
+	authConfigEncoded, err := a.getEncodedRegistryAuth(ctx, image)
 	if err != nil {
 	if err != nil {
 		return types.ImagePullOptions{}, err
 		return types.ImagePullOptions{}, err
 	}
 	}
@@ -340,13 +340,13 @@ func (a *Agent) getPullOptions(image string) (types.ImagePullOptions, error) {
 	}, nil
 	}, nil
 }
 }
 
 
-func (a *Agent) getContainerRegistryToken(image string) (string, error) {
+func (a *Agent) getContainerRegistryToken(ctx context.Context, image string) (string, error) {
 	serverURL, err := GetServerURLFromTag(image)
 	serverURL, err := GetServerURLFromTag(image)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	_, secret, err := a.authGetter.GetCredentials(serverURL)
+	_, secret, err := a.authGetter.GetCredentials(ctx, serverURL)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -354,14 +354,14 @@ func (a *Agent) getContainerRegistryToken(image string) (string, error) {
 	return secret, nil
 	return secret, nil
 }
 }
 
 
-func (a *Agent) getEncodedRegistryAuth(image string) (string, error) {
+func (a *Agent) getEncodedRegistryAuth(ctx context.Context, image string) (string, error) {
 	// get using server url
 	// get using server url
 	serverURL, err := GetServerURLFromTag(image)
 	serverURL, err := GetServerURLFromTag(image)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	user, secret, err := a.authGetter.GetCredentials(serverURL)
+	user, secret, err := a.authGetter.GetCredentials(ctx, serverURL)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -385,8 +385,8 @@ func (a *Agent) getEncodedRegistryAuth(image string) (string, error) {
 	return base64.URLEncoding.EncodeToString(authConfigBytes), nil
 	return base64.URLEncoding.EncodeToString(authConfigBytes), nil
 }
 }
 
 
-func (a *Agent) getPushOptions(image string) (types.ImagePushOptions, error) {
-	pullOpts, err := a.getPullOptions(image)
+func (a *Agent) getPushOptions(ctx context.Context, image string) (types.ImagePushOptions, error) {
+	pullOpts, err := a.getPullOptions(ctx, image)
 
 
 	return types.ImagePushOptions(pullOpts), err
 	return types.ImagePushOptions(pullOpts), err
 }
 }
@@ -430,9 +430,9 @@ func GetServerURLFromTag(image string) (string, error) {
 }
 }
 
 
 // WaitForContainerStop waits until a container has stopped to exit
 // WaitForContainerStop waits until a container has stopped to exit
-func (a *Agent) WaitForContainerStop(id string) error {
+func (a *Agent) WaitForContainerStop(ctx context.Context, id string) error {
 	// wait for container to stop before exit
 	// wait for container to stop before exit
-	statusCh, errCh := a.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
+	statusCh, errCh := a.ContainerWait(ctx, id, container.WaitConditionNotRunning)
 
 
 	select {
 	select {
 	case err := <-errCh:
 	case err := <-errCh:
@@ -448,9 +448,9 @@ func (a *Agent) WaitForContainerStop(id string) error {
 // WaitForContainerHealthy waits until a container is returning a healthy status. Streak
 // WaitForContainerHealthy waits until a container is returning a healthy status. Streak
 // is the maximum number of failures in a row, while timeout is the length of time between
 // is the maximum number of failures in a row, while timeout is the length of time between
 // checks.
 // checks.
-func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
+func (a *Agent) WaitForContainerHealthy(ctx context.Context, id string, streak int) error {
 	for {
 	for {
-		cont, err := a.ContainerInspect(a.ctx, id)
+		cont, err := a.ContainerInspect(ctx, id)
 		if err != nil {
 		if err != nil {
 			return a.handleDockerClientErr(err, "Error waiting for stopped container")
 			return a.handleDockerClientErr(err, "Error waiting for stopped container")
 		}
 		}

+ 33 - 26
cli/cmd/docker/auth.go

@@ -44,28 +44,30 @@ type CredentialsCache interface {
 
 
 // AuthGetter retrieves
 // AuthGetter retrieves
 type AuthGetter struct {
 type AuthGetter struct {
-	Client    *api.Client
+	Client    api.Client
 	Cache     CredentialsCache
 	Cache     CredentialsCache
 	ProjectID uint
 	ProjectID uint
 }
 }
 
 
-func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret string, err error) {
+// GetCredentials returns registry credentials
+func (a *AuthGetter) GetCredentials(ctx context.Context, serverURL string) (user string, secret string, err error) {
 	if strings.Contains(serverURL, "gcr.io") {
 	if strings.Contains(serverURL, "gcr.io") {
-		return a.GetGCRCredentials(serverURL, a.ProjectID)
+		return a.GetGCRCredentials(ctx, serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "pkg.dev") {
 	} else if strings.Contains(serverURL, "pkg.dev") {
-		return a.GetGARCredentials(serverURL, a.ProjectID)
+		return a.GetGARCredentials(ctx, serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "registry.digitalocean.com") {
 	} else if strings.Contains(serverURL, "registry.digitalocean.com") {
-		return a.GetDOCRCredentials(serverURL, a.ProjectID)
+		return a.GetDOCRCredentials(ctx, serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "index.docker.io") {
 	} else if strings.Contains(serverURL, "index.docker.io") {
-		return a.GetDockerHubCredentials(serverURL, a.ProjectID)
+		return a.GetDockerHubCredentials(ctx, serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "azurecr.io") {
 	} else if strings.Contains(serverURL, "azurecr.io") {
-		return a.GetACRCredentials(serverURL, a.ProjectID)
+		return a.GetACRCredentials(ctx, serverURL, a.ProjectID)
 	}
 	}
 
 
-	return a.GetECRCredentials(serverURL, a.ProjectID)
+	return a.GetECRCredentials(ctx, serverURL, a.ProjectID)
 }
 }
 
 
-func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetGCRCredentials returns GCR credentials
+func (a *AuthGetter) GetGCRCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	if err != nil {
 	if err != nil {
 		return "", "", err
 		return "", "", err
 	}
 	}
@@ -78,7 +80,7 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 		token = cachedEntry.AuthorizationToken
 	} else {
 	} else {
 		// get a token from the server
 		// get a token from the server
-		tokenResp, err := a.Client.GetGCRAuthorizationToken(context.Background(), projID, &types.GetRegistryGCRTokenRequest{
+		tokenResp, err := a.Client.GetGCRAuthorizationToken(ctx, projID, &types.GetRegistryGCRTokenRequest{
 			ServerURL: serverURL,
 			ServerURL: serverURL,
 		})
 		})
 		if err != nil {
 		if err != nil {
@@ -91,7 +93,7 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 			ProxyEndpoint:      serverURL,
 		})
 		})
 	}
 	}
@@ -99,7 +101,8 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 	return "oauth2accesstoken", token, nil
 	return "oauth2accesstoken", token, nil
 }
 }
 
 
-func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetGARCredentials returns GAR credentials
+func (a *AuthGetter) GetGARCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	if err != nil {
 	if err != nil {
 		return "", "", err
 		return "", "", err
 	}
 	}
@@ -123,7 +126,7 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 		token = cachedEntry.AuthorizationToken
 	} else {
 	} else {
 		// get a token from the server
 		// get a token from the server
-		tokenResp, err := a.Client.GetGARAuthorizationToken(context.Background(), projID, &types.GetRegistryGARTokenRequest{
+		tokenResp, err := a.Client.GetGARAuthorizationToken(ctx, projID, &types.GetRegistryGARTokenRequest{
 			ServerURL: serverURL,
 			ServerURL: serverURL,
 		})
 		})
 		if err != nil {
 		if err != nil {
@@ -136,7 +139,7 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 			ProxyEndpoint:      serverURL,
 		})
 		})
 	}
 	}
@@ -144,7 +147,8 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 	return "oauth2accesstoken", token, nil
 	return "oauth2accesstoken", token, nil
 }
 }
 
 
-func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetDOCRCredentials returns DOCR credentials
+func (a *AuthGetter) GetDOCRCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	cachedEntry := a.Cache.Get(serverURL)
 	cachedEntry := a.Cache.Get(serverURL)
 
 
 	var token string
 	var token string
@@ -154,7 +158,7 @@ func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user str
 	} else {
 	} else {
 
 
 		// get a token from the server
 		// get a token from the server
-		tokenResp, err := a.Client.GetDOCRAuthorizationToken(context.Background(), projID, &types.GetRegistryGCRTokenRequest{
+		tokenResp, err := a.Client.GetDOCRAuthorizationToken(ctx, projID, &types.GetRegistryGCRTokenRequest{
 			ServerURL: serverURL,
 			ServerURL: serverURL,
 		})
 		})
 		if err != nil {
 		if err != nil {
@@ -163,7 +167,7 @@ func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user str
 
 
 		token = tokenResp.Token
 		token = tokenResp.Token
 
 
-		if t := *tokenResp.ExpiresAt; len(token) > 0 && !t.IsZero() {
+		if t := tokenResp.ExpiresAt; len(token) > 0 && !t.IsZero() {
 			// set the token in cache
 			// set the token in cache
 			a.Cache.Set(serverURL, &AuthEntry{
 			a.Cache.Set(serverURL, &AuthEntry{
 				AuthorizationToken: token,
 				AuthorizationToken: token,
@@ -180,7 +184,8 @@ func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user str
 
 
 var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
 var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
 
 
-func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetECRCredentials returns ECR credentials
+func (a *AuthGetter) GetECRCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	// parse the server url for region
 	// parse the server url for region
 	matches := ecrPattern.FindStringSubmatch(serverURL)
 	matches := ecrPattern.FindStringSubmatch(serverURL)
 
 
@@ -201,7 +206,7 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 		token = cachedEntry.AuthorizationToken
 	} else {
 	} else {
 		// get a token from the server
 		// get a token from the server
-		tokenResp, err := a.Client.GetECRAuthorizationToken(context.Background(), projID, &types.GetRegistryECRTokenRequest{
+		tokenResp, err := a.Client.GetECRAuthorizationToken(ctx, projID, &types.GetRegistryECRTokenRequest{
 			Region:    matches[3],
 			Region:    matches[3],
 			AccountID: matches[1],
 			AccountID: matches[1],
 		})
 		})
@@ -215,7 +220,7 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 			ProxyEndpoint:      serverURL,
 		})
 		})
 	}
 	}
@@ -223,7 +228,8 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 	return decodeDockerToken(token)
 	return decodeDockerToken(token)
 }
 }
 
 
-func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetDockerHubCredentials returns dockerhub credentials
+func (a *AuthGetter) GetDockerHubCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	cachedEntry := a.Cache.Get(serverURL)
 	cachedEntry := a.Cache.Get(serverURL)
 	var token string
 	var token string
 
 
@@ -231,7 +237,7 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 		token = cachedEntry.AuthorizationToken
 		token = cachedEntry.AuthorizationToken
 	} else {
 	} else {
 		// get a token from the server
 		// get a token from the server
-		tokenResp, err := a.Client.GetDockerhubAuthorizationToken(context.Background(), projID)
+		tokenResp, err := a.Client.GetDockerhubAuthorizationToken(ctx, projID)
 		if err != nil {
 		if err != nil {
 			return "", "", err
 			return "", "", err
 		}
 		}
@@ -242,7 +248,7 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 		a.Cache.Set(serverURL, &AuthEntry{
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 			ProxyEndpoint:      serverURL,
 		})
 		})
 	}
 	}
@@ -250,7 +256,8 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 	return decodeDockerToken(token)
 	return decodeDockerToken(token)
 }
 }
 
 
-func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+// GetACRCredentials returns ACR credentials
+func (a *AuthGetter) GetACRCredentials(ctx context.Context, serverURL string, projID uint) (user string, secret string, err error) {
 	cachedEntry := a.Cache.Get(serverURL)
 	cachedEntry := a.Cache.Get(serverURL)
 	var token string
 	var token string
 
 
@@ -258,7 +265,7 @@ func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 		token = cachedEntry.AuthorizationToken
 	} else {
 	} else {
 		req := &types.GetRegistryACRTokenRequest{ServerURL: serverURL}
 		req := &types.GetRegistryACRTokenRequest{ServerURL: serverURL}
-		tokenResp, err := a.Client.GetACRAuthorizationToken(context.Background(), projID, req)
+		tokenResp, err := a.Client.GetACRAuthorizationToken(ctx, projID, req)
 		if err != nil {
 		if err != nil {
 			return "", "", err
 			return "", "", err
 		}
 		}
@@ -269,7 +276,7 @@ func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 			ProxyEndpoint:      serverURL,
 		})
 		})
 	}
 	}

+ 2 - 2
cli/cmd/docker/builder.go

@@ -33,7 +33,7 @@ type BuildOpts struct {
 }
 }
 
 
 // BuildLocal
 // BuildLocal
-func (a *Agent) BuildLocal(opts *BuildOpts) (err error) {
+func (a *Agent) BuildLocal(ctx context.Context, opts *BuildOpts) (err error) {
 	dockerfilePath := opts.DockerfilePath
 	dockerfilePath := opts.DockerfilePath
 
 
 	// attempt to read dockerignore file and paths
 	// attempt to read dockerignore file and paths
@@ -84,7 +84,7 @@ func (a *Agent) BuildLocal(opts *BuildOpts) (err error) {
 	inlineCacheVal := "1"
 	inlineCacheVal := "1"
 	buildArgs["BUILDKIT_INLINE_CACHE"] = &inlineCacheVal
 	buildArgs["BUILDKIT_INLINE_CACHE"] = &inlineCacheVal
 
 
-	out, err := a.ImageBuild(context.Background(), tar, types.ImageBuildOptions{
+	out, err := a.ImageBuild(ctx, tar, types.ImageBuildOptions{
 		Dockerfile: dockerfilePath,
 		Dockerfile: dockerfilePath,
 		BuildArgs:  buildArgs,
 		BuildArgs:  buildArgs,
 		Tags: []string{
 		Tags: []string{

+ 4 - 5
cli/cmd/docker/config.go

@@ -11,8 +11,7 @@ const label = "CreatedByPorterCLI"
 
 
 // NewAgentFromEnv creates a new Docker agent using the environment variables set
 // NewAgentFromEnv creates a new Docker agent using the environment variables set
 // on the host
 // on the host
-func NewAgentFromEnv() (*Agent, error) {
-	ctx := context.Background()
+func NewAgentFromEnv(ctx context.Context) (*Agent, error) {
 	cli, err := client.NewClientWithOpts(
 	cli, err := client.NewClientWithOpts(
 		client.FromEnv,
 		client.FromEnv,
 		client.WithAPIVersionNegotiation(),
 		client.WithAPIVersionNegotiation(),
@@ -23,13 +22,13 @@ func NewAgentFromEnv() (*Agent, error) {
 
 
 	return &Agent{
 	return &Agent{
 		Client: cli,
 		Client: cli,
-		ctx:    ctx,
 		label:  label,
 		label:  label,
 	}, nil
 	}, nil
 }
 }
 
 
-func NewAgentWithAuthGetter(client *api.Client, projID uint) (*Agent, error) {
-	agent, err := NewAgentFromEnv()
+// NewAgentWithAuthGetter returns a docker agent which can connect to a given registry
+func NewAgentWithAuthGetter(ctx context.Context, client api.Client, projID uint) (*Agent, error) {
+	agent, err := NewAgentFromEnv(ctx)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 47 - 46
cli/cmd/docker/porter.go

@@ -1,6 +1,7 @@
 package docker
 package docker
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -33,8 +34,8 @@ type PorterStartOpts struct {
 
 
 // StartPorter creates a new Docker agent using the host environment, and creates a
 // StartPorter creates a new Docker agent using the host environment, and creates a
 // new Porter instance
 // new Porter instance
-func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
-	agent, err = NewAgentFromEnv()
+func StartPorter(ctx context.Context, opts *PorterStartOpts) (agent *Agent, id string, err error) {
+	agent, err = NewAgentFromEnv(ctx)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
@@ -46,7 +47,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 	// the volumes passed to the Porter container
 	// the volumes passed to the Porter container
 	volumesMap := make(map[string]struct{})
 	volumesMap := make(map[string]struct{})
 
 
-	netID, err := agent.CreateBridgeNetworkIfNotExist("porter_network_" + opts.ProcessID)
+	netID, err := agent.CreateBridgeNetworkIfNotExist(ctx, "porter_network_"+opts.ProcessID)
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
@@ -54,7 +55,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 	switch opts.DB {
 	switch opts.DB {
 	case SQLite:
 	case SQLite:
 		// check if sqlite volume exists, create it if not
 		// check if sqlite volume exists, create it if not
-		vol, err := agent.CreateLocalVolumeIfNotExist("porter_sqlite_" + opts.ProcessID)
+		vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_sqlite_"+opts.ProcessID)
 		if err != nil {
 		if err != nil {
 			return nil, "", err
 			return nil, "", err
 		}
 		}
@@ -77,7 +78,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		}...)
 		}...)
 	case Postgres:
 	case Postgres:
 		// check if postgres volume exists, create it if not
 		// check if postgres volume exists, create it if not
-		vol, err := agent.CreateLocalVolumeIfNotExist("porter_postgres_" + opts.ProcessID)
+		vol, err := agent.CreateLocalVolumeIfNotExist(ctx, "porter_postgres_"+opts.ProcessID)
 		if err != nil {
 		if err != nil {
 			return nil, "", err
 			return nil, "", err
 		}
 		}
@@ -109,12 +110,12 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 			},
 			},
 		}
 		}
 
 
-		pgID, err := agent.StartPostgresContainer(startOpts)
+		pgID, err := agent.StartPostgresContainer(ctx, startOpts)
 		if err != nil {
 		if err != nil {
 			return nil, "", err
 			return nil, "", err
 		}
 		}
 
 
-		err = agent.WaitForContainerHealthy(pgID, 10)
+		err = agent.WaitForContainerHealthy(ctx, pgID, 10)
 
 
 		if err != nil {
 		if err != nil {
 			return nil, "", err
 			return nil, "", err
@@ -144,13 +145,13 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		Env:           opts.Env,
 		Env:           opts.Env,
 	}
 	}
 
 
-	id, err = agent.StartPorterContainer(startOpts)
+	id, err = agent.StartPorterContainer(ctx, startOpts)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
 	}
 	}
 
 
-	err = agent.WaitForContainerHealthy(id, 10)
+	err = agent.WaitForContainerHealthy(ctx, id, 10)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err
@@ -174,20 +175,20 @@ type PorterServerStartOpts struct {
 
 
 // StartPorterContainer pulls a specific Porter image and starts a container
 // StartPorterContainer pulls a specific Porter image and starts a container
 // using the Docker engine. It returns the container ID
 // using the Docker engine. It returns the container ID
-func (a *Agent) StartPorterContainer(opts PorterServerStartOpts) (string, error) {
-	id, err := a.upsertPorterContainer(opts)
+func (a *Agent) StartPorterContainer(ctx context.Context, opts PorterServerStartOpts) (string, error) {
+	id, err := a.upsertPorterContainer(ctx, opts)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	err = a.startPorterContainer(id)
+	err = a.startPorterContainer(ctx, id)
 
 
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	// attach container to network
 	// attach container to network
-	err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
+	err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
 
 
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
@@ -200,20 +201,20 @@ func (a *Agent) StartPorterContainer(opts PorterServerStartOpts) (string, error)
 // if spec has changed, remove and recreate container
 // if spec has changed, remove and recreate container
 // if container does not exist, create the container
 // if container does not exist, create the container
 // otherwise, return stopped container
 // otherwise, return stopped container
-func (a *Agent) upsertPorterContainer(opts PorterServerStartOpts) (id string, err error) {
-	containers, err := a.getContainersCreatedByStart()
+func (a *Agent) upsertPorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
+	containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
 
 
 	// remove the matching container
 	// remove the matching container
 	for _, container := range containers {
 	for _, container := range containers {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 			timeout, _ := time.ParseDuration("15s")
 			timeout, _ := time.ParseDuration("15s")
 
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 			if err != nil {
 				return "", a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 				return "", a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 			}
 			}
 
 
-			err = a.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+			err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
 
 
 			if err != nil {
 			if err != nil {
 				return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
 				return "", a.handleDockerClientErr(err, "Could not remove container "+container.ID)
@@ -221,12 +222,12 @@ func (a *Agent) upsertPorterContainer(opts PorterServerStartOpts) (id string, er
 		}
 		}
 	}
 	}
 
 
-	return a.pullAndCreatePorterContainer(opts)
+	return a.pullAndCreatePorterContainer(ctx, opts)
 }
 }
 
 
 // create the container and return its id
 // create the container and return its id
-func (a *Agent) pullAndCreatePorterContainer(opts PorterServerStartOpts) (id string, err error) {
-	a.PullImage(opts.Image)
+func (a *Agent) pullAndCreatePorterContainer(ctx context.Context, opts PorterServerStartOpts) (id string, err error) {
+	_ = a.PullImage(ctx, opts.Image)
 
 
 	// format the port array for binding to host machine
 	// format the port array for binding to host machine
 	ports := []string{fmt.Sprintf("127.0.0.1:%d:%d/tcp", opts.HostPort, opts.ContainerPort)}
 	ports := []string{fmt.Sprintf("127.0.0.1:%d:%d/tcp", opts.HostPort, opts.ContainerPort)}
@@ -240,7 +241,7 @@ func (a *Agent) pullAndCreatePorterContainer(opts PorterServerStartOpts) (id str
 	labels[a.label] = "true"
 	labels[a.label] = "true"
 
 
 	// create the container with a label specifying this was created via the CLI
 	// create the container with a label specifying this was created via the CLI
-	resp, err := a.ContainerCreate(a.ctx, &container.Config{
+	resp, err := a.ContainerCreate(ctx, &container.Config{
 		Image:   opts.Image,
 		Image:   opts.Image,
 		Cmd:     opts.StartCmd,
 		Cmd:     opts.StartCmd,
 		Tty:     false,
 		Tty:     false,
@@ -265,8 +266,8 @@ func (a *Agent) pullAndCreatePorterContainer(opts PorterServerStartOpts) (id str
 }
 }
 
 
 // start the container
 // start the container
-func (a *Agent) startPorterContainer(id string) error {
-	if err := a.ContainerStart(a.ctx, id, types.ContainerStartOptions{}); err != nil {
+func (a *Agent) startPorterContainer(ctx context.Context, id string) error {
+	if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
 		return a.handleDockerClientErr(err, "Could not start Porter container")
 		return a.handleDockerClientErr(err, "Could not start Porter container")
 	}
 	}
 
 
@@ -285,20 +286,20 @@ type PostgresOpts struct {
 
 
 // StartPostgresContainer pulls a specific Porter image and starts a container
 // StartPostgresContainer pulls a specific Porter image and starts a container
 // using the Docker engine
 // using the Docker engine
-func (a *Agent) StartPostgresContainer(opts PostgresOpts) (string, error) {
-	id, err := a.upsertPostgresContainer(opts)
+func (a *Agent) StartPostgresContainer(ctx context.Context, opts PostgresOpts) (string, error) {
+	id, err := a.upsertPostgresContainer(ctx, opts)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	err = a.startPostgresContainer(id)
+	err = a.startPostgresContainer(ctx, id)
 
 
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	// attach container to network
 	// attach container to network
-	err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
+	err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
 
 
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
@@ -311,15 +312,15 @@ func (a *Agent) StartPostgresContainer(opts PostgresOpts) (string, error) {
 // if it is running, stop it
 // if it is running, stop it
 // if it is stopped, return id
 // if it is stopped, return id
 // if it does not exist, create it and return it
 // if it does not exist, create it and return it
-func (a *Agent) upsertPostgresContainer(opts PostgresOpts) (id string, err error) {
-	containers, err := a.getContainersCreatedByStart()
+func (a *Agent) upsertPostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
+	containers, err := a.getContainersCreatedByStart(ctx) // nolint:ineffassign,staticcheck // linter complaining, do not want to change logic incase intentional
 
 
 	// stop the matching container and return it
 	// stop the matching container and return it
 	for _, container := range containers {
 	for _, container := range containers {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 			timeout, _ := time.ParseDuration("15s")
 			timeout, _ := time.ParseDuration("15s")
 
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 			if err != nil {
 				return "", a.handleDockerClientErr(err, "Could not stop postgres container "+container.ID)
 				return "", a.handleDockerClientErr(err, "Could not stop postgres container "+container.ID)
 			}
 			}
@@ -328,18 +329,18 @@ func (a *Agent) upsertPostgresContainer(opts PostgresOpts) (id string, err error
 		}
 		}
 	}
 	}
 
 
-	return a.pullAndCreatePostgresContainer(opts)
+	return a.pullAndCreatePostgresContainer(ctx, opts)
 }
 }
 
 
 // create the container and return it
 // create the container and return it
-func (a *Agent) pullAndCreatePostgresContainer(opts PostgresOpts) (id string, err error) {
-	a.PullImage(opts.Image)
+func (a *Agent) pullAndCreatePostgresContainer(ctx context.Context, opts PostgresOpts) (id string, err error) {
+	_ = a.PullImage(ctx, opts.Image) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 
 	labels := make(map[string]string)
 	labels := make(map[string]string)
 	labels[a.label] = "true"
 	labels[a.label] = "true"
 
 
 	// create the container with a label specifying this was created via the CLI
 	// create the container with a label specifying this was created via the CLI
-	resp, err := a.ContainerCreate(a.ctx, &container.Config{
+	resp, err := a.ContainerCreate(ctx, &container.Config{
 		Image:   opts.Image,
 		Image:   opts.Image,
 		Tty:     false,
 		Tty:     false,
 		Labels:  labels,
 		Labels:  labels,
@@ -365,8 +366,8 @@ func (a *Agent) pullAndCreatePostgresContainer(opts PostgresOpts) (id string, er
 }
 }
 
 
 // start the container in the background
 // start the container in the background
-func (a *Agent) startPostgresContainer(id string) error {
-	if err := a.ContainerStart(a.ctx, id, types.ContainerStartOptions{}); err != nil {
+func (a *Agent) startPostgresContainer(ctx context.Context, id string) error {
+	if err := a.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
 		return a.handleDockerClientErr(err, "Could not start Postgres container")
 		return a.handleDockerClientErr(err, "Could not start Postgres container")
 	}
 	}
 
 
@@ -375,8 +376,8 @@ func (a *Agent) startPostgresContainer(id string) error {
 
 
 // StopPorterContainers finds all containers that were started via the CLI and stops them
 // StopPorterContainers finds all containers that were started via the CLI and stops them
 // -- removes the container if remove is set to true
 // -- removes the container if remove is set to true
-func (a *Agent) StopPorterContainers(remove bool) error {
-	containers, err := a.getContainersCreatedByStart()
+func (a *Agent) StopPorterContainers(ctx context.Context, remove bool) error {
+	containers, err := a.getContainersCreatedByStart(ctx)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -385,13 +386,13 @@ func (a *Agent) StopPorterContainers(remove bool) error {
 	for _, container := range containers {
 	for _, container := range containers {
 		timeout, _ := time.ParseDuration("15s")
 		timeout, _ := time.ParseDuration("15s")
 
 
-		err := a.ContainerStop(a.ctx, container.ID, &timeout)
+		err := a.ContainerStop(ctx, container.ID, &timeout)
 		if err != nil {
 		if err != nil {
 			return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 			return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 		}
 		}
 
 
 		if remove {
 		if remove {
-			err = a.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+			err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
 
 
 			if err != nil {
 			if err != nil {
 				return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
 				return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
@@ -405,8 +406,8 @@ func (a *Agent) StopPorterContainers(remove bool) error {
 // StopPorterContainersWithProcessID finds all containers that were started via the CLI
 // StopPorterContainersWithProcessID finds all containers that were started via the CLI
 // and have a given process id and stops them -- removes the container if remove is set
 // and have a given process id and stops them -- removes the container if remove is set
 // to true
 // to true
-func (a *Agent) StopPorterContainersWithProcessID(processID string, remove bool) error {
-	containers, err := a.getContainersCreatedByStart()
+func (a *Agent) StopPorterContainersWithProcessID(ctx context.Context, processID string, remove bool) error {
+	containers, err := a.getContainersCreatedByStart(ctx)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -416,13 +417,13 @@ func (a *Agent) StopPorterContainersWithProcessID(processID string, remove bool)
 		if strings.Contains(container.Names[0], "_"+processID) {
 		if strings.Contains(container.Names[0], "_"+processID) {
 			timeout, _ := time.ParseDuration("15s")
 			timeout, _ := time.ParseDuration("15s")
 
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 			if err != nil {
 				return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 				return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 			}
 			}
 
 
 			if remove {
 			if remove {
-				err = a.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+				err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
 
 
 				if err != nil {
 				if err != nil {
 					return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
 					return a.handleDockerClientErr(err, "Could not remove container "+container.ID)
@@ -436,8 +437,8 @@ func (a *Agent) StopPorterContainersWithProcessID(processID string, remove bool)
 
 
 // getContainersCreatedByStart gets all containers that were created by the "porter start"
 // getContainersCreatedByStart gets all containers that were created by the "porter start"
 // command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
 // command by looking for the label "CreatedByPorterCLI" (or .label of the agent)
-func (a *Agent) getContainersCreatedByStart() ([]types.Container, error) {
-	containers, err := a.ContainerList(a.ctx, types.ContainerListOptions{
+func (a *Agent) getContainersCreatedByStart(ctx context.Context) ([]types.Container, error) {
+	containers, err := a.ContainerList(ctx, types.ContainerListOptions{
 		All: true,
 		All: true,
 	})
 	})
 	if err != nil {
 	if err != nil {

+ 14 - 6
cli/cmd/errors/error_handler.go

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/config"
 )
 )
 
 
+// SentryDSN is a global value for sentry's dsn. This should be removed
 var SentryDSN string = ""
 var SentryDSN string = ""
 
 
 type errorHandler interface {
 type errorHandler interface {
@@ -18,21 +19,25 @@ type errorHandler interface {
 
 
 type standardErrorHandler struct{}
 type standardErrorHandler struct{}
 
 
+// HandleError implements errorhandler for handling non-sentry errors
 func (h *standardErrorHandler) HandleError(err error) {
 func (h *standardErrorHandler) HandleError(err error) {
 	color.New(color.FgRed).Fprintf(os.Stderr, "error: %s\n", err.Error())
 	color.New(color.FgRed).Fprintf(os.Stderr, "error: %s\n", err.Error())
 }
 }
 
 
-type sentryErrorHandler struct{}
+type sentryErrorHandler struct {
+	cliConfig config.CLIConfig
+}
 
 
+// HandleError implements errorhandler for handling sentry errors
 func (h *sentryErrorHandler) HandleError(err error) {
 func (h *sentryErrorHandler) HandleError(err error) {
 	if SentryDSN != "" {
 	if SentryDSN != "" {
 		localHub := sentry.CurrentHub().Clone()
 		localHub := sentry.CurrentHub().Clone()
 
 
 		localHub.ConfigureScope(func(scope *sentry.Scope) {
 		localHub.ConfigureScope(func(scope *sentry.Scope) {
 			scope.SetTags(map[string]string{
 			scope.SetTags(map[string]string{
-				"host":    config.GetCLIConfig().Host,
-				"project": fmt.Sprintf("%d", config.GetCLIConfig().Project),
-				"cluster": fmt.Sprintf("%d", config.GetCLIConfig().Cluster),
+				"host":    h.cliConfig.Host,
+				"project": fmt.Sprintf("%d", h.cliConfig.Project),
+				"cluster": fmt.Sprintf("%d", h.cliConfig.Cluster),
 			})
 			})
 		})
 		})
 
 
@@ -43,9 +48,12 @@ func (h *sentryErrorHandler) HandleError(err error) {
 	color.New(color.FgRed).Fprintf(os.Stderr, "error: %s\n", err.Error())
 	color.New(color.FgRed).Fprintf(os.Stderr, "error: %s\n", err.Error())
 }
 }
 
 
-func GetErrorHandler() errorHandler {
+// GetErrorHandler returns an errorhandler.
+func GetErrorHandler(cliConf config.CLIConfig) errorHandler {
 	if SentryDSN != "" {
 	if SentryDSN != "" {
-		return &sentryErrorHandler{}
+		return &sentryErrorHandler{
+			cliConfig: cliConf,
+		}
 	}
 	}
 
 
 	return &standardErrorHandler{}
 	return &standardErrorHandler{}

+ 8 - 8
cli/cmd/github/release.go

@@ -44,8 +44,8 @@ type ZIPReleaseGetter struct {
 }
 }
 
 
 // GetLatestRelease downloads the latest .zip release from a given Github repository
 // GetLatestRelease downloads the latest .zip release from a given Github repository
-func (z *ZIPReleaseGetter) GetLatestRelease() error {
-	releaseURL, err := z.getLatestReleaseDownloadURL()
+func (z *ZIPReleaseGetter) GetLatestRelease(ctx context.Context) error {
+	releaseURL, err := z.getLatestReleaseDownloadURL(ctx)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -54,8 +54,8 @@ func (z *ZIPReleaseGetter) GetLatestRelease() error {
 }
 }
 
 
 // GetRelease downloads a specific .zip release from a given Github repository
 // GetRelease downloads a specific .zip release from a given Github repository
-func (z *ZIPReleaseGetter) GetRelease(releaseTag string) error {
-	releaseURL, err := z.getReleaseDownloadURL(releaseTag)
+func (z *ZIPReleaseGetter) GetRelease(ctx context.Context, releaseTag string) error {
+	releaseURL, err := z.getReleaseDownloadURL(ctx, releaseTag)
 
 
 	fmt.Printf("getting release %s\n", releaseURL)
 	fmt.Printf("getting release %s\n", releaseURL)
 
 
@@ -85,10 +85,10 @@ func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
 }
 }
 
 
 // retrieves the download url for the latest release of an asset
 // retrieves the download url for the latest release of an asset
-func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) {
+func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL(ctx context.Context) (string, error) {
 	client := github.NewClient(nil)
 	client := github.NewClient(nil)
 
 
-	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), z.EntityID, z.RepoName)
+	rel, _, err := client.Repositories.GetLatestRelease(ctx, z.EntityID, z.RepoName)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -110,10 +110,10 @@ func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) {
 	return releaseURL, nil
 	return releaseURL, nil
 }
 }
 
 
-func (z *ZIPReleaseGetter) getReleaseDownloadURL(releaseTag string) (string, error) {
+func (z *ZIPReleaseGetter) getReleaseDownloadURL(ctx context.Context, releaseTag string) (string, error) {
 	client := github.NewClient(nil)
 	client := github.NewClient(nil)
 
 
-	rel, _, err := client.Repositories.GetReleaseByTag(context.Background(), z.EntityID, z.RepoName, releaseTag)
+	rel, _, err := client.Repositories.GetReleaseByTag(ctx, z.EntityID, z.RepoName, releaseTag)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("release %s does not exist", releaseTag)
 		return "", fmt.Errorf("release %s does not exist", releaseTag)
 	}
 	}

+ 0 - 57
cli/cmd/helm.go

@@ -1,57 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/spf13/cobra"
-)
-
-var helmCmd = &cobra.Command{
-	Use:   "helm",
-	Short: "Use helm to interact with a Porter cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, runHelm)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(helmCmd)
-}
-
-func runHelm(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	_, err := exec.LookPath("helm")
-	if err != nil {
-		return fmt.Errorf("error finding helm: %w", err)
-	}
-
-	tmpFile, err := downloadTempKubeconfig(client)
-	if err != nil {
-		return err
-	}
-
-	defer func() {
-		os.Remove(tmpFile)
-	}()
-
-	os.Setenv("KUBECONFIG", tmpFile)
-
-	cmd := exec.Command("helm", args...)
-
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-
-	err = cmd.Run()
-
-	if err != nil {
-		return fmt.Errorf("error running helm: %w", err)
-	}
-
-	return nil
-}

+ 0 - 194
cli/cmd/list.go

@@ -1,194 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"text/tabwriter"
-
-	"github.com/fatih/color"
-	api "github.com/porter-dev/porter/api/client"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/spf13/cobra"
-	"github.com/stefanmcshane/helm/pkg/release"
-)
-
-var allNamespaces bool
-
-// listCmd represents the "porter list" base command and "porter list all" subcommand
-var listCmd = &cobra.Command{
-	Use:   "list",
-	Short: "List applications, addons or jobs.",
-	Run: func(cmd *cobra.Command, args []string) {
-		if len(args) == 0 || (args[0] == "all") {
-			err := checkLoginAndRun(args, listAll)
-			if err != nil {
-				os.Exit(1)
-			}
-		} else {
-			color.New(color.FgRed).Fprintf(os.Stderr, "invalid command: %s\n", args[0])
-		}
-	},
-}
-
-var listAppsCmd = &cobra.Command{
-	Use:     "apps",
-	Aliases: []string{"applications", "app", "application"},
-	Short:   "Lists applications in a specific namespace, or across all namespaces",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listApps)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var listJobsCmd = &cobra.Command{
-	Use:     "jobs",
-	Aliases: []string{"job"},
-	Short:   "Lists jobs in a specific namespace, or across all namespaces",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listJobs)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-var listAddonsCmd = &cobra.Command{
-	Use:     "addons",
-	Aliases: []string{"addon"},
-	Short:   "Lists addons in a specific namespace, or across all namespaces",
-	Run: func(cmd *cobra.Command, args []string) {
-		err := checkLoginAndRun(args, listAddons)
-		if err != nil {
-			os.Exit(1)
-		}
-	},
-}
-
-func init() {
-	listCmd.PersistentFlags().StringVar(
-		&namespace,
-		"namespace",
-		"default",
-		"the namespace of the release",
-	)
-
-	listCmd.PersistentFlags().BoolVar(
-		&allNamespaces,
-		"all-namespaces",
-		false,
-		"list resources for all namespaces",
-	)
-
-	listCmd.AddCommand(listAppsCmd)
-	listCmd.AddCommand(listJobsCmd)
-	listCmd.AddCommand(listAddonsCmd)
-
-	rootCmd.AddCommand(listCmd)
-}
-
-func listAll(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	err := writeReleases(client, "all")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func listApps(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	err := writeReleases(client, "application")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func listJobs(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	err := writeReleases(client, "job")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func listAddons(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	err := writeReleases(client, "addon")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func writeReleases(client *api.Client, kind string) error {
-	var namespaces []string
-	var releases []*release.Release
-
-	if allNamespaces {
-		resp, err := client.GetK8sNamespaces(context.Background(), cliConf.Project, cliConf.Cluster)
-		if err != nil {
-			return err
-		}
-
-		namespaceResp := *resp
-
-		for _, ns := range namespaceResp {
-			namespaces = append(namespaces, ns.Name)
-		}
-	} else {
-		namespaces = append(namespaces, namespace)
-	}
-
-	for _, ns := range namespaces {
-		resp, err := client.ListReleases(context.Background(), cliConf.Project, cliConf.Cluster, ns,
-			&types.ListReleasesRequest{
-				ReleaseListFilter: &types.ReleaseListFilter{
-					Limit: 50,
-					Skip:  0,
-					StatusFilter: []string{
-						"deployed",
-						"uninstalled",
-						"pending",
-						"pending-install",
-						"pending-upgrade",
-						"pending-rollback",
-						"failed",
-					},
-				},
-			},
-		)
-		if err != nil {
-			return err
-		}
-
-		releases = append(releases, resp...)
-	}
-
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 3, 8, 2, '\t', tabwriter.AlignRight)
-
-	fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "NAME", "NAMESPACE", "STATUS", "KIND")
-
-	for _, rel := range releases {
-		chartName := rel.Chart.Name()
-
-		if kind == "all" {
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
-		} else if kind == "application" && (chartName == "web" || chartName == "worker") {
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
-		} else if kind == "job" && chartName == "job" {
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
-		} else if kind == "addon" && chartName != "web" && chartName != "worker" && chartName != "job" {
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", rel.Name, rel.Namespace, rel.Info.Status, chartName)
-		}
-	}
-
-	w.Flush()
-
-	return nil
-}

+ 0 - 31
cli/cmd/open.go

@@ -1,31 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/porter-dev/porter/cli/cmd/config"
-	"github.com/porter-dev/porter/cli/cmd/utils"
-
-	"github.com/spf13/cobra"
-)
-
-var openCmd = &cobra.Command{
-	Use:   "open",
-	Short: "Opens the browser at the currently set Porter instance",
-	Run: func(cmd *cobra.Command, args []string) {
-		client := config.GetAPIClient()
-
-		user, err := client.AuthCheck(context.Background())
-
-		if err == nil {
-			utils.OpenBrowser(fmt.Sprintf("%s/login?email=%s", cliConf.Host, user.Email))
-		} else {
-			utils.OpenBrowser(fmt.Sprintf("%s/register", cliConf.Host))
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(openCmd)
-}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio