Просмотр исходного кода

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

Feroze Mohideen 2 лет назад
Родитель
Сommit
8292039ae0
100 измененных файлов с 5356 добавлено и 3347 удалено
  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:
     - codeanalysis
 
-# enable exported entity commenting lint rule
 issues:
-  exclude:
-    - EXC0012
+  new-from-rev: origin/master # default: HEAD, this will only show linting changes in the current change
   exclude-use-default: false
 linters-settings:
   revive:
     rules:
       - name: exported
         severity: error
-  # gocyclo:
-  #   min-complexity: 15
+  gocyclo:
+    min-complexity: 40 # should drop to 15 max
   gomoddirectives:
     replace-local: false
   gosec:
     excludes:
-    - G307
+    - G307 # exclude duplicated errcheck checks
 
 linters:
   # 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:
           go-version-file: go.mod
           cache: false
-          go-version: '1.20.5'
-      - name: Run Go vet
-        run: go vet ./${{ matrix.folder }}/...
       - name: Run Go tests
         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:
     name: Running smoke test npm build
     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
 
 [![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.
 

+ 5 - 1
Taskfile.yaml

@@ -17,4 +17,8 @@ tasks:
       ignore_error: false
       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=[
     "api",
     "build",
-    "cli",
     "ee",
     "internal",
     "pkg",

+ 54 - 17
api/client/api.go

@@ -1,8 +1,10 @@
 package client
 
 import (
+	"context"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -25,9 +27,61 @@ type Client struct {
 	CookieFilePath string
 	Token          string
 
+	// cfToken is a cloudflare token for accessing the API
 	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
 func NewClient(baseURL string, cookieFileName string) *Client {
 	home := homedir.HomeDir()
@@ -55,23 +109,6 @@ func NewClient(baseURL string, cookieFileName string) *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 {
 	vals := make(map[string][]string)
 	err := schema.NewEncoder().Encode(data, vals)

+ 145 - 0
api/client/porter_app.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/porter-dev/porter/api/server/handlers/porter_app"
+
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -127,3 +129,146 @@ func (c *Client) CreateOrUpdatePorterAppEvent(
 
 	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
 
 import (
-	"encoding/base64"
 	"net/http"
 
 	"connectrpc.com/connect"
 
-	"github.com/google/uuid"
 	helpers "github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"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")
 	defer span.End()
 
-	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 	var apiContract porterv1.Contract
@@ -50,59 +47,6 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		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{
 		Id: int32(user.ID),
 	}

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

@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"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/handlers"
@@ -31,31 +32,39 @@ func NewGetPodMetricsHandler(
 }
 
 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{}
 
 	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
 	}
 
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-
 	agent, err := c.GetAgent(r, cluster, "")
 	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
 	}
 
 	// get prometheus service
 	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
-
 	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
 	}
 
 	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, &request.QueryOpts)
 	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
 	}
 

+ 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/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 	"gopkg.in/yaml.v2"
 )
@@ -37,6 +38,9 @@ func NewGithubGetPorterYamlHandler(
 func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-porter-yaml")
 	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
 	request := &types.GetPorterYamlRequest{}
 	ok := c.DecodeAndValidate(w, r, request)
 	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))
 		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
-	if parsed.Version != nil {
+	if !project.ValidateApplyV2 && parsed.Version != nil {
 		version := *parsed.Version
 		if version != "v1stack" {
 			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()
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
 	defer span.End()
@@ -302,7 +301,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 		}
 
-		if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+		if features.AreAgentDeployEventsEnabled(k8sAgent) {
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
 		} else {
@@ -491,7 +490,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 		}
 
-		if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+		if features.AreAgentDeployEventsEnabled(k8sAgent) {
 			serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 			_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 		} 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]
 	}
 
+	// 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 ""
 }

+ 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/requestutils"
 	"github.com/porter-dev/porter/api/types"
+	utils "github.com/porter-dev/porter/api/utils/porter_app"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
@@ -25,6 +26,7 @@ func NewGetPorterAppHandler(
 ) *GetPorterAppHandler {
 	return &GetPorterAppHandler{
 		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()
 
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
 	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
 	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
 	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
 
 	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	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
 	}
 
-	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
 	}
 
+	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, "")
 	if err != nil {
 		_ = 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
 	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
 	} else {
 		// 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 {
 					if pod.GetCreationTimestamp().Time.After(request.StartRange) && pod.GetCreationTimestamp().Time.Before(request.EndRange) {
 						if latestPod == nil || pod.GetCreationTimestamp().Time.After(latestPod.GetCreationTimestamp().Time) {
-							latestPod = &pod
+							copyPod := pod
+							latestPod = &copyPod
 						}
 					}
 				}
 			}
 			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))
 				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
 }
 

+ 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")
 	defer span.End()
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
-	user, _ := ctx.Value(types.UserScope).(*models.User)
 
 	request := &types.RollbackPorterAppRequest{}
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -154,7 +153,7 @@ func (c *RollbackPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	if features.AreAgentDeployEventsEnabled(user.Email, k8sAgent) {
+	if features.AreAgentDeployEventsEnabled(k8sAgent) {
 		serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
 		_, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, latestHelmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
 	} 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,
 		HelmValuesEnabled:      false,
 		MultiCluster:           false,
+		EnableReprovision:      false,
 	}
 
 	var err error

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

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

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

@@ -1,8 +1,11 @@
 package project_integration
 
 import (
+	"encoding/base64"
 	"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/shared"
 	"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/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
 type CreateGCPHandler struct {
@@ -27,8 +31,11 @@ func NewCreateGCPHandler(
 }
 
 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{}
 
@@ -36,6 +43,43 @@ func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		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, 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
 	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.AttributeKV{Key: "repo-name", Value: repoName},
 		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{
 			Token:     ecrResponse.Msg.Token,
-			ExpiresAt: &expiry,
+			ExpiresAt: expiry,
 		}
 
 		c.WriteResult(w, r, resp)
@@ -82,7 +82,7 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 	for _, reg := range regs {
 		if reg.AWSIntegrationID != 0 {
@@ -123,8 +123,11 @@ func (c *RegistryGetECRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 					return
 				}
 
+				if output == nil || output.AuthorizationData == nil || len(output.AuthorizationData) == 0 {
+					continue
+				}
 				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 expiresAt *time.Time
+	var expiresAt time.Time
 
 	for _, reg := range regs {
 		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
-			expiresAt = &oauthTok.Expiry
+			expiresAt = oauthTok.Expiry
 			break
 		}
 	}
@@ -238,8 +241,43 @@ func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		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 expiresAt *time.Time
+	var expiresAt time.Time
 
 	for _, reg := range regs {
 		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))
 			}
 
+			if oauthTok == nil {
+				continue
+			}
 			token = oauthTok.AccessToken
-			expiresAt = &oauthTok.Expiry
+			expiresAt = oauthTok.Expiry
 			break
 		}
 	}
@@ -302,7 +343,7 @@ func (c *RegistryGetDOCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 	for _, reg := range regs {
 		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
-			expiresAt = expiry
+			expiresAt = *expiry
 			break
 		}
 	}
@@ -361,7 +402,7 @@ func (c *RegistryGetDockerhubTokenHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}
 
 	var token string
-	var expiresAt *time.Time
+	var expiresAt time.Time
 
 	for _, reg := range regs {
 		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)
 			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 expiresAt *time.Time
+	var expiresAt time.Time
 
 	var matchingReg *models.Registry
 	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)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
-		expiresAt = &timeExpires
+		expiresAt = timeExpires
 	}
 
 	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)))
 		// we'll just set an arbitrary 30-day expiry time (this is not enforced)
 		timeExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
-		expiresAt = &timeExpires
+		expiresAt = timeExpires
 	}
 
 	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/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+	"github.com/stefanmcshane/helm/pkg/release"
 	"gorm.io/gorm"
 	"k8s.io/apimachinery/pkg/util/validation"
 )
@@ -34,9 +36,16 @@ func NewUpdateCanonicalNameHandler(
 }
 
 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)
-	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{}
 
@@ -44,13 +53,15 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		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 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
 		}
 
+		err = telemetry.Error(ctx, span, err, "unable to get release resource")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -58,7 +69,8 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	if release.CanonicalName != request.CanonicalName {
 		if request.CanonicalName != "" {
 			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
 			}
 		}
@@ -68,6 +80,7 @@ func (c *UpdateCanonicalNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 		release, err = c.Repo().Release().UpdateRelease(release)
 
 		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error updating chart")
 			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}

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

@@ -625,6 +625,43 @@ func getGitInstallationRoutes(
 		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 ->
 	// gitinstallation.NewGithubGetProcfileHandler
 	getProcfileEndpoint := factory.NewAPIEndpoint(

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

@@ -454,6 +454,35 @@ func getPorterAppRoutes(
 		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
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
@@ -542,5 +571,179 @@ func getPorterAppRoutes(
 		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
 }

+ 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])
 	if parsedMajor < major {
 		return false
+	} else if parsedMajor > major {
+		return true
 	}
 	if parsedMinor < minor {
 		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
-// 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"`
 }
 
+// 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 {
 	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 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"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	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 {
@@ -26,6 +29,9 @@ type FeatureFlags struct {
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,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 {

+ 4 - 0
api/types/project_integration.go

@@ -138,6 +138,10 @@ type CreateGCPRequest struct {
 
 type CreateGCPResponse struct {
 	*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 {

+ 2 - 2
api/types/registry.go

@@ -172,8 +172,8 @@ type UpdateRegistryRequest 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 {

+ 3 - 0
api/types/template.go

@@ -33,6 +33,9 @@ type PorterTemplateSimple struct {
 
 	// The repo URL for the template
 	RepoURL string `json:"repo_url,omitempty"`
+
+	//
+	Tags []string `json:"tags,omitempty"`
 }
 
 // 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 (
 	"context"
@@ -12,6 +12,7 @@ import (
 	"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"
 	batchv1 "k8s.io/api/batch/v1"
@@ -42,55 +43,67 @@ var (
 	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(
 		&appExistingPod,
 		"existing_pod",
@@ -137,20 +150,9 @@ func init() {
 		"",
 		"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:]
 
 	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])
 
 	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 {
 			return fmt.Errorf("Unable to run command: %w", err)
 		}
 		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 != "" &&
@@ -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 {
 		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{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 	if err != nil {
 		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 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{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err := config.setSharedConfig()
+	err := config.setSharedConfig(ctx)
 	if err != nil {
 		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")
 
 	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 {
 			return err
 		}
 
 		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...)
 			} else {
 				return err
 			}
 		}
 	} else {
-		if pods, err := appGetEphemeralPods(appNamespace, config.Clientset); err == nil {
+		if pods, err := appGetEphemeralPods(ctx, appNamespace, config.Clientset); err == nil {
 			podNames = append(podNames, pods...)
 		} else {
 			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)
 
 		err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
-			context.Background(), podName, metav1.DeleteOptions{},
+			ctx, podName, metav1.DeleteOptions{},
 		)
 		if err != nil {
 			return err
@@ -335,11 +339,11 @@ func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []s
 	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
 
 	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 {
 		return nil, err
@@ -353,17 +357,18 @@ func appGetEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]s
 }
 
 type AppPorterRunSharedConfig struct {
-	Client     *api.Client
+	Client     api.Client
 	RestConf   *rest.Config
 	Clientset  *kubernetes.Clientset
 	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 {
 		return err
 	}
@@ -411,11 +416,11 @@ type appPodSimple struct {
 	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 {
 		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 {
 		return err
 	}
 
-	newPod, err := appCreateEphemeralPodFromExisting(config, existing, container, args)
+	newPod, err := appCreateEphemeralPodFromExisting(ctx, config, existing, container, args)
 	if err != nil {
 		return err
 	}
 	podName := newPod.ObjectMeta.Name
 
 	// 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)
-	if err = appWaitForPod(config, newPod); err != nil {
+	if err = appWaitForPod(ctx, config, newPod); err != nil {
 		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 {
 		return err
 	}
@@ -511,7 +516,7 @@ func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, c
 	// refresh pod info for latest status
 	newPod, err = config.Clientset.CoreV1().
 		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.
 	// 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) {
 		color.New(color.FgGreen).Println("complete!")
 		var writtenBytes int64
-		writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
+		writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 		if appVerbose || writtenBytes == 0 {
 			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
 	}
@@ -564,44 +569,44 @@ func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, c
 		})
 	}); err != nil {
 		// 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 {
 		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
 }
 
-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,
 	// starting with the service account, then role and then a role binding
 
-	err := appCheckForServiceAccount(config)
+	err := appCheckForServiceAccount(ctx, config)
 	if err != nil {
 		return err
 	}
 
-	err = appCheckForClusterRole(config)
+	err = appCheckForClusterRole(ctx, config)
 	if err != nil {
 		return err
 	}
 
-	err = appCheckForRoleBinding(config)
+	err = appCheckForRoleBinding(ctx, config)
 	if err != nil {
 		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 {
 		return err
 	}
 
 	for _, namespace := range namespaces.Items {
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		if err != nil {
 			return err
@@ -617,7 +622,7 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 			for _, cronJob := range cronJobs.Items {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
-						context.Background(), cronJob.Name, metav1.DeleteOptions{},
+						ctx, cronJob.Name, metav1.DeleteOptions{},
 					)
 					if err != nil {
 						return err
@@ -656,7 +661,7 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
-		context.Background(), cronJob, metav1.CreateOptions{},
+		ctx, cronJob, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -665,15 +670,15 @@ func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
 	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 {
 		return err
 	}
 
 	for _, namespace := range namespaces.Items {
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		if err != nil {
 			return err
@@ -689,7 +694,7 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 			for _, svcAccount := range serviceAccounts.Items {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
-						context.Background(), svcAccount.Name, metav1.DeleteOptions{},
+						ctx, svcAccount.Name, metav1.DeleteOptions{},
 					)
 					if err != nil {
 						return err
@@ -705,7 +710,7 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
-		context.Background(), serviceAccount, metav1.CreateOptions{},
+		ctx, serviceAccount, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -714,9 +719,9 @@ func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
 	return nil
 }
 
-func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
+func appCheckForClusterRole(ctx context.Context, config *AppPorterRunSharedConfig) error {
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	if err != nil {
 		return err
@@ -746,7 +751,7 @@ func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
-		context.Background(), role, metav1.CreateOptions{},
+		ctx, role, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -755,9 +760,9 @@ func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
 	return nil
 }
 
-func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
+func appCheckForRoleBinding(ctx context.Context, config *AppPorterRunSharedConfig) error {
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	if err != nil {
 		return err
@@ -788,7 +793,7 @@ func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
-		context.Background(), binding, metav1.CreateOptions{},
+		ctx, binding, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -797,7 +802,7 @@ func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
 	return nil
 }
 
-func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
+func appWaitForPod(ctx context.Context, config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 	var (
 		w   watch.Interface
 		err error
@@ -810,7 +815,7 @@ func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		w, err = config.Clientset.CoreV1().
 			Pods(pod.Namespace).
-			Watch(context.Background(), metav1.ListOptions{FieldSelector: selector})
+			Watch(ctx, metav1.ListOptions{FieldSelector: selector})
 
 		if err == nil {
 			break
@@ -828,7 +833,7 @@ func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
 			// creating the listener.
 			pod, err = config.Clientset.CoreV1().
 				Pods(pod.Namespace).
-				Get(context.Background(), pod.Name, metav1.GetOptions{})
+				Get(ctx, pod.Name, metav1.GetOptions{})
 			if appIsPodReady(pod) || appIsPodExited(pod) {
 				return nil
 			}
@@ -861,23 +866,23 @@ func appIsPodExited(pod *v1.Pod) bool {
 	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 {
 		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:")
 
 	var writtenBytes int64
-	writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
+	writtenBytes, _ = appPipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 	if appVerbose || writtenBytes == 0 {
 		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
 }
 
-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{
 		Container: container,
 		Follow:    follow,
@@ -886,7 +891,7 @@ func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, c
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 	podLogs, err := req.Stream(
-		context.Background(),
+		ctx,
 	)
 	if err != nil {
 		return 0, err
@@ -897,13 +902,13 @@ func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, c
 	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
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 	// creates the clientset
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
-		context.TODO(),
+		ctx,
 		metav1.ListOptions{
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 		},
@@ -919,20 +924,20 @@ func appPipeEventsToStdout(config *AppPorterRunSharedConfig, namespace, name, co
 	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(
-		context.Background(),
+		ctx,
 		name,
 		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
-	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(
-		context.Background(),
+		ctx,
 		name,
 		metav1.DeleteOptions{},
 	)
@@ -947,6 +952,7 @@ func appDeletePod(config *AppPorterRunSharedConfig, name, namespace string) erro
 }
 
 func appCreateEphemeralPodFromExisting(
+	ctx context.Context,
 	config *AppPorterRunSharedConfig,
 	existing *v1.Pod,
 	container string,
@@ -1032,18 +1038,18 @@ func appCreateEphemeralPodFromExisting(
 
 	// create the pod and return it
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
-		context.Background(),
+		ctx,
 		newPod,
 		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])
 	if appTag == "" {
 		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 {
 		return fmt.Errorf("Unable to find application %s", args[0])
 	}
@@ -1056,8 +1062,8 @@ func appUpdateTag(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 		Tag:        appTag,
 	}
 	createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
-		ClusterID:       cliConf.Cluster,
-		ProjectID:       cliConf.Project,
+		ClusterID:       cliConfig.Cluster,
+		ProjectID:       cliConfig.Project,
 		ImageInfo:       imageInfo,
 		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)
 
 	_, err = client.CreatePorterApp(
-		context.Background(),
-		cliConf.Project,
-		cliConf.Cluster,
+		ctx,
+		cliConfig.Project,
+		cliConfig.Cluster,
 		args[0],
 		createUpdatePorterAppRequest,
 	)

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

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"context"
@@ -13,6 +13,8 @@ import (
 	"strings"
 	"time"
 
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/cli/cli/git"
 	"github.com/fatih/color"
 	"github.com/mitchellh/mapstructure"
@@ -21,9 +23,10 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"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"
 	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"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
@@ -36,12 +39,13 @@ import (
 	"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
 
 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_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.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
 	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 {
 		stackName := os.Getenv("PORTER_STACK_NAME")
 		if stackName == "" {
@@ -133,7 +146,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 	if previewVersion.Version == "v2beta1" {
 		ns := os.Getenv("PORTER_NAMESPACE")
 
-		applier, err := previewV2Beta1.NewApplier(client, fileBytes, ns)
+		applier, err := previewV2Beta1.NewApplier(client, cliConfig, fileBytes, ns)
 		if err != nil {
 			return err
 		}
@@ -158,7 +171,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		}
 	} else if previewVersion.Version == "v1stack" || previewVersion.Version == "" {
 
-		parsed, err := stack.ValidateAndMarshal(fileBytes)
+		parsed, err := porter_app.ValidateAndMarshal(fileBytes)
 		if err != nil {
 			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 {
 			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 {
 					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")
 			}
 
-			var services map[string]*stack.Service
+			var services map[string]*porter_app.Service
 			if parsed.Apps != nil {
 				services = parsed.Apps
 			}
@@ -201,7 +214,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 				services = parsed.Services
 			}
 
-			app := &stack.Application{
+			app := &porter_app.Application{
 				Env:      parsed.Env,
 				Services: services,
 				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)
 			}
 
-			resources, err := stack.CreateApplicationDeploy(client, worker, app, appName, cliConf)
+			resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, appName, cliConfig)
 			if err != nil {
 				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
 		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},
-		{"env-group", preview.NewEnvGroupDriver},
+		{"env-group", preview.NewEnvGroupDriver(ctx, client, cliConfig)},
 		{"os-env", preview.NewOSEnvDriver},
 	}
 	for _, driver := range drivers {
@@ -259,7 +272,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 			return
 		}
 
-		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
+		deploymentHook, err := NewDeploymentHook(cliConfig, client, resGroup, deplNamespace)
 		if err != nil {
 			err = fmt.Errorf("error creating deployment hook: %w", err)
 			return err
@@ -279,7 +292,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		return err
 	}
 
-	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
+	cloneEnvGroupHook := NewCloneEnvGroupHook(client, cliConfig, resGroup)
 	err = worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
 	if err != nil {
 		err = fmt.Errorf("error registering clone env group hook: %w", err)
@@ -349,47 +362,55 @@ func hasDeploymentHookEnvVars() bool {
 	return true
 }
 
+// DeployDriver contains all information needed for deploying with switchboard
 type DeployDriver struct {
 	source      *previewInt.Source
 	target      *previewInt.Target
 	output      map[string]interface{}
 	lookupTable *map[string]drivers.Driver
 	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 {
 	return true
 }
 
+// Apply extends switchboard
 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,
 		d.target.Project,
 		d.target.Cluster,
@@ -404,14 +425,14 @@ func (d *DeployDriver) Apply(resource *switchboardModels.Resource) (*switchboard
 	}
 
 	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
-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)
 	if err != nil {
 		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 {
 		err := client.DeployAddon(
-			context.Background(),
+			ctx,
 			d.target.Project,
 			d.target.Cluster,
 			d.target.Namespace,
@@ -443,7 +464,7 @@ func (d *DeployDriver) applyAddon(resource *switchboardModels.Resource, client *
 		}
 
 		err = client.UpgradeRelease(
-			context.Background(),
+			ctx,
 			d.target.Project,
 			d.target.Cluster,
 			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 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 {
 		return nil, fmt.Errorf("nil resource")
 	}
@@ -526,20 +547,20 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 
 	if appConfig.Build.UseCache {
 		// 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 {
 			return nil, err
 		}
 	}
 
 	if shouldCreate {
-		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
+		resource, err = d.createApplication(ctx, resource, client, sharedOpts, appConfig)
 
 		if err != nil {
 			return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
 		}
 	} else if !appConfig.OnlyCreate {
-		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
+		resource, err = d.updateApplication(ctx, resource, client, sharedOpts, appConfig)
 
 		if err != nil {
 			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)
 	}
 
-	if err = d.assignOutput(resource, client); err != nil {
+	if err = d.assignOutput(ctx, resource, client); err != nil {
 		return nil, err
 	}
 
@@ -574,7 +595,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 			predeployEventResponseID = eventResponse.ID
 		}
 
-		err = wait.WaitForJob(client, &wait.WaitOpts{
+		err = wait.WaitForJob(ctx, client, &wait.WaitOpts{
 			ProjectID: d.target.Project,
 			ClusterID: d.target.Cluster,
 			Namespace: d.target.Namespace,
@@ -602,7 +623,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 
 			if appConfig.OnlyCreate {
 				deleteJobErr := client.DeleteRelease(
-					context.Background(),
+					ctx,
 					d.target.Project,
 					d.target.Cluster,
 					d.target.Namespace,
@@ -641,7 +662,7 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 	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
 	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 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
 
 	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 {
 		// if useCache is set, create the image repository first
 		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 {
 				return nil, err
 			}
 
 			err = client.CreateRepository(
-				context.Background(),
+				ctx,
 				sharedOpts.ProjectID,
 				regID,
 				&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 {
@@ -713,14 +734,14 @@ func (d *DeployDriver) createApplication(resource *switchboardModels.Resource, c
 	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)
 
 	if len(appConf.Build.Env) > 0 {
 		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,
 		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 appConf.Build.Method != "registry" {
-		buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+		buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 			UseNewConfig: true,
 			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 {
 			return nil, err
 		}
 
 		if !appConf.Build.UseCache {
-			err = updateAgent.Push()
+			err = updateAgent.Push(ctx)
 
 			if err != nil {
 				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 {
 		return nil, err
 	}
@@ -792,9 +813,9 @@ func (d *DeployDriver) updateApplication(resource *switchboardModels.Resource, c
 	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(
-		context.Background(),
+		ctx,
 		d.target.Project,
 		d.target.Cluster,
 		d.target.Namespace,
@@ -809,6 +830,7 @@ func (d *DeployDriver) assignOutput(resource *switchboardModels.Resource, client
 	return nil
 }
 
+// Output extends switchboard
 func (d *DeployDriver) Output() (map[string]interface{}, error) {
 	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 {
-	client                                                                    *api.Client
+	client                                                                    api.Client
 	resourceGroup                                                             *switchboardTypes.ResourceGroup
 	gitInstallationID, projectID, clusterID, prID, actionID, envID            uint
 	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{
 		client:        client,
 		resourceGroup: resourceGroup,
 		namespace:     namespace,
+		cliConfig:     cliConfig,
 	}
 
 	ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID")
@@ -877,13 +903,13 @@ func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.Resou
 
 	res.prID = uint(prID)
 
-	res.projectID = cliConf.Project
+	res.projectID = cliConfig.Project
 
 	if res.projectID == 0 {
 		return nil, fmt.Errorf("project id must be set")
 	}
 
-	res.clusterID = cliConf.Cluster
+	res.clusterID = cliConfig.Cluster
 
 	if res.clusterID == 0 {
 		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
 }
 
+// PreApply extends switchboard
 func (t *DeploymentHook) PreApply() error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	if isSystemNamespace(t.namespace) {
 		color.New(color.FgYellow).Printf("attempting to deploy to system namespace '%s'\n", t.namespace)
 	}
 
 	envList, err := t.client.ListEnvironments(
-		context.Background(), t.projectID, t.clusterID,
+		ctx, t.projectID, t.clusterID,
 	)
 	if err != nil {
 		return err
@@ -956,7 +985,7 @@ func (t *DeploymentHook) PreApply() error {
 	}
 
 	nsList, err := t.client.GetK8sNamespaces(
-		context.Background(), t.projectID, t.clusterID,
+		ctx, t.projectID, t.clusterID,
 	)
 	if err != nil {
 		return fmt.Errorf("error fetching namespaces: %w", err)
@@ -986,7 +1015,7 @@ func (t *DeploymentHook) PreApply() error {
 		}
 
 		// 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") {
 			// ignore the error if the namespace already exists
@@ -1000,7 +1029,7 @@ func (t *DeploymentHook) PreApply() error {
 
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
@@ -1008,7 +1037,7 @@ func (t *DeploymentHook) PreApply() error {
 		)
 	} else {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
@@ -1039,7 +1068,7 @@ func (t *DeploymentHook) PreApply() error {
 		}
 
 		_, err = t.client.CreateDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, createReq,
 		)
 	} else if err == nil {
@@ -1059,12 +1088,13 @@ func (t *DeploymentHook) PreApply() error {
 			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
 }
 
+// DataQueries extends switchboard
 func (t *DeploymentHook) DataQueries() map[string]interface{} {
 	res := make(map[string]interface{})
 
@@ -1125,7 +1155,10 @@ func (t *DeploymentHook) DataQueries() map[string]interface{} {
 	return res
 }
 
+// PostApply extends switchboard
 func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	subdomains := make([]string, 0)
 
 	for _, data := range populatedData {
@@ -1153,8 +1186,8 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	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 != "" {
 			req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
@@ -1165,17 +1198,20 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	// 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
 }
 
+// OnError extends switchboard
 func (t *DeploymentHook) OnError(error) {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	var deplErr error
 
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
@@ -1183,7 +1219,7 @@ func (t *DeploymentHook) OnError(error) {
 		)
 	} else {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
@@ -1210,16 +1246,19 @@ func (t *DeploymentHook) OnError(error) {
 		}
 
 		// 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) {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	var deplErr error
 
 	if t.isBranchDeploy() {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				Branch: t.branchFrom,
@@ -1227,7 +1266,7 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		)
 	} else {
 		_, deplErr = t.client.GetDeployment(
-			context.Background(),
+			ctx,
 			t.projectID, t.clusterID, t.envID,
 			&types.GetDeploymentRequest{
 				PRNumber: t.prID,
@@ -1252,8 +1291,8 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		for _, res := range t.resourceGroup.Resources {
 			if _, ok := allErrors[res.Name]; !ok {
 				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
-		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 {
-	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{
-		client:   client,
-		resGroup: resourceGroup,
+		client:    client,
+		cliConfig: cliConfig,
+		resGroup:  resourceGroup,
 	}
 }
 
 func (t *CloneEnvGroupHook) PreApply() error {
+	ctx := context.TODO() // switchboard blocks changing this for now
+
 	for _, res := range t.resGroup.Resources {
 		if res.Driver == "env-group" {
 			continue
@@ -1293,7 +1338,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 		}
 
 		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 {
 				return err
 			}
@@ -1304,7 +1349,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 				}
 
 				_, err := t.client.GetEnvGroup(
-					context.Background(),
+					ctx,
 					target.Project,
 					target.Cluster,
 					target.Namespace,
@@ -1326,7 +1371,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 							group.Name, group.Namespace, target.Namespace)
 
 					_, err = t.client.CloneEnvGroup(
-						context.Background(), target.Project, target.Cluster, group.Namespace,
+						ctx, target.Project, target.Cluster, group.Namespace,
 						&types.CloneEnvGroupRequest{
 							SourceName:      group.Name,
 							TargetNamespace: target.Namespace,
@@ -1358,10 +1403,10 @@ func (t *CloneEnvGroupHook) OnError(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
 	// 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 != "" {
 		return target.AppName
@@ -1370,10 +1415,10 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 	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
 	// 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 != "" {
 		return source.Name
@@ -1392,7 +1437,8 @@ func isSystemNamespace(namespace string) bool {
 
 type ErrorEmitterHook struct{}
 
-func NewErrorEmitterHook(*api.Client, *switchboardTypes.ResourceGroup) *ErrorEmitterHook {
+// NewErrorEmitterHook handles switchboard errors
+func NewErrorEmitterHook(api.Client, *switchboardTypes.ResourceGroup) *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 (
 	"context"
@@ -11,70 +11,67 @@ import (
 	"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"
 )
 
-// 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(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)
+
+	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 {
 		return err
 	}
@@ -101,7 +98,7 @@ func listClusters(user *types.GetAuthenticatedUserResponse, client *api.Client,
 	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(
 		fmt.Sprintf(
 			`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
 		}
 
-		err = client.DeleteProjectCluster(context.Background(), cliConf.Project, uint(id))
+		err = client.DeleteProjectCluster(ctx, cliConf.Project, uint(id))
 
 		if err != nil {
 			return err
@@ -131,7 +128,7 @@ func deleteCluster(user *types.GetAuthenticatedUserResponse, client *api.Client,
 	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
 
 	// 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
 	namespaceList, err := client.GetK8sNamespaces(
-		context.Background(),
+		ctx,
 		pID,
 		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 (
 	"context"
@@ -8,6 +8,8 @@ import (
 	"path/filepath"
 	"strings"
 
+	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"
@@ -20,13 +22,21 @@ import (
 	"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
 
 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
 `,
-		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(
 		&name,
@@ -177,18 +175,30 @@ func init() {
 	)
 
 	createCmd.PersistentFlags().MarkDeprecated("force-build", "--force-build is deprecated")
+	return createCmd
 }
 
 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
 	if _, exists := supportedKinds[args[0]]; !exists {
 		return fmt.Errorf("%s is not a supported type: specify web, job, or worker", args[0])
 	}
 
-	var err error
-
 	fullPath, err := filepath.Abs(localPath)
 	if err != nil {
 		return err
@@ -251,13 +261,13 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 	if source == "local" {
 		if useCache {
-			regID, imageURL, err := createAgent.GetImageRepoURL(name, namespace)
+			regID, imageURL, err := createAgent.GetImageRepoURL(ctx, name, namespace)
 			if err != nil {
 				return err
 			}
 
 			err = client.CreateRepository(
-				context.Background(),
+				ctx,
 				cliConf.Project,
 				regID,
 				&types.CreateRegistryRepositoryRequest{
@@ -269,21 +279,21 @@ func createFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 				return err
 			}
 
-			err = config.SetDockerConfig(createAgent.Client)
+			err = config.SetDockerConfig(ctx, createAgent.Client, project.ID)
 
 			if err != nil {
 				return err
 			}
 		}
 
-		subdomain, err := createAgent.CreateFromDocker(valuesObj, "default", nil)
+		subdomain, err := createAgent.CreateFromDocker(ctx, valuesObj, "default", nil)
 
 		return handleSubdomainCreate(subdomain, err)
 	} 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)
 }
@@ -302,7 +312,7 @@ func handleSubdomainCreate(subdomain string, err error) error {
 	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)
 	if err != nil {
 		return err
@@ -328,10 +338,12 @@ func createFromGithub(createAgent *deploy.CreateAgent, overrideValues map[string
 		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)
 }

+ 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 (
 	"context"
@@ -6,6 +6,9 @@ import (
 	"os"
 	"time"
 
+	"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"
@@ -16,23 +19,21 @@ import (
 	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)
 
 	bluegreenCmd.PersistentFlags().StringVar(
@@ -57,11 +58,25 @@ func init() {
 		"",
 		"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
-	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 {
 		return err
 	}
@@ -74,11 +89,11 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	currActiveImage := deploy.GetCurrActiveBlueGreenImage(webRelease.Config)
 
 	sharedConf := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err = sharedConf.setSharedConfig()
-
+	err = sharedConf.setSharedConfig(ctx)
 	if err != nil {
 		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) {
 		// refresh the client every 10 minutes
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
-			err = sharedConf.setSharedConfig()
+			err = sharedConf.setSharedConfig(ctx)
 
 			if err != nil {
 				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(
-			context.Background(),
+			ctx,
 			metav1.ListOptions{
 				LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 			},
@@ -129,13 +144,13 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 					// push the deployment
 					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 {
 						return err
 					}
 
 					if currActiveImage == "" {
-						err = deployAgent.UpdateImageAndValues(map[string]interface{}{
+						err = deployAgent.UpdateImageAndValues(ctx, map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 								"enabled":                  true,
 								"disablePrimaryDeployment": true,
@@ -144,7 +159,7 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 							},
 						})
 					} else {
-						err = deployAgent.UpdateImageAndValues(map[string]interface{}{
+						err = deployAgent.UpdateImageAndValues(ctx, map[string]interface{}{
 							"bluegreen": map[string]interface{}{
 								"enabled":                  true,
 								"disablePrimaryDeployment": true,
@@ -182,19 +197,21 @@ func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	// wait 30 seconds before removing old deployment
 	time.Sleep(30 * time.Second)
 
-	deployAgent, err := updateGetAgent(client)
+	deployAgent, err := updateGetAgent(ctx, client, cliConfig)
 	if err != nil {
 		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
 }

+ 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 (
 	"context"
 	"errors"
+	"fmt"
 	"os"
 	"strings"
 
@@ -18,10 +19,17 @@ var (
 	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 {
 		red := color.New(color.FgRed)
 
@@ -39,7 +47,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 		return err
 	}
 
-	err = runner(user, client, args)
+	err = runner(ctx, user, client, cliConf, args)
 
 	if err != nil {
 		red := color.New(color.FgRed)
@@ -54,7 +62,7 @@ func checkLoginAndRun(args []string, runner func(user *types.GetAuthenticatedUse
 			return nil
 		}
 
-		cliErrors.GetErrorHandler().HandleError(err)
+		cliErrors.GetErrorHandler(cliConf).HandleError(err)
 
 		return err
 	}

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

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"context"
@@ -6,6 +6,9 @@ import (
 	"fmt"
 	"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"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
@@ -13,36 +16,34 @@ import (
 	"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(
 		&namespace,
 		"namespace",
@@ -59,7 +60,7 @@ func init() {
 
 	getCmd.AddCommand(getValuesCmd)
 
-	rootCmd.AddCommand(getCmd)
+	return getCmd
 }
 
 type getReleaseInfo struct {
@@ -70,8 +71,21 @@ type getReleaseInfo struct {
 	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 {
 		return err
 	}
@@ -109,8 +123,21 @@ func get(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []strin
 	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 {
 		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 (
 	"context"
 	"fmt"
 	"os"
 
+	"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"
@@ -13,14 +16,17 @@ import (
 	"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
 
 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
 `,
-		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
 
 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
 `,
-		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
 
 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
 `,
-		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(waitCmd)
 	jobCmd.AddCommand(runJobCmd)
@@ -172,13 +174,27 @@ func init() {
 	)
 
 	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)
 
 	return client.UpdateBatchImage(
-		context.TODO(),
+		ctx,
 		cliConf.Project,
 		cliConf.Cluster,
 		namespace,
@@ -190,8 +206,21 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 }
 
 // waits for a job with a given name/namespace
-func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
-	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,
 		ClusterID: cliConf.Cluster,
 		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)
 
 	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 {
 		return fmt.Errorf("error running job: %w", err)
 	}
 
-	err = waitForJob(authRes, client, args)
+	err = waitForJob(ctx, authRes, client, cliConf, args)
 
 	if err != nil {
 		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 (
 	"context"
@@ -8,31 +8,31 @@ import (
 
 	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"
 )
 
-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")
 	if err != nil {
 		return fmt.Errorf("error finding kubectl: %w", err)
 	}
 
-	tmpFile, err := downloadTempKubeconfig(client)
+	tmpFile, err := downloadTempKubeconfig(ctx, client, cliConf)
 	if err != nil {
 		return err
 	}
@@ -57,7 +57,7 @@ func runKubectl(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	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")
 	if err != nil {
 		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()
 
-	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 {
 		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 (
+	"context"
 	"fmt"
 	"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/utils"
 	"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
 
-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(
 		&namespace,
@@ -43,10 +41,11 @@ func init() {
 		false,
 		"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 {
 		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{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 	if err != nil {
 		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
 }

+ 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 (
 	"context"
@@ -11,78 +11,75 @@ import (
 	"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"
 )
 
-// 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)
 
@@ -94,14 +91,16 @@ func init() {
 
 	registryCmd.AddCommand(registryImageCmd)
 	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
 
 	// get the list of namespaces
 	resp, err := client.ListRegistries(
-		context.Background(),
+		ctx,
 		pID,
 	)
 	if err != nil {
@@ -130,7 +129,7 @@ func listRegistries(user *types.GetAuthenticatedUserResponse, client *api.Client
 	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(
 		fmt.Sprintf(
 			`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
 		}
 
-		err = client.DeleteProjectRegistry(context.Background(), cliConf.Project, uint(id))
+		err = client.DeleteProjectRegistry(ctx, cliConf.Project, uint(id))
 
 		if err != nil {
 			return err
@@ -160,13 +159,13 @@ func deleteRegistry(user *types.GetAuthenticatedUserResponse, client *api.Client
 	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
 	rID := cliConf.Registry
 
 	// get the list of namespaces
 	resp, err := client.ListRegistryRepositories(
-		context.Background(),
+		ctx,
 		pID,
 		rID,
 	)
@@ -190,14 +189,14 @@ func listRepos(user *types.GetAuthenticatedUserResponse, client *api.Client, arg
 	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
 	rID := cliConf.Registry
 	repoName := args[0]
 
 	// get the list of namespaces
 	resp, err := client.ListImages(
-		context.Background(),
+		ctx,
 		pID,
 		rID,
 		repoName,

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

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"context"
@@ -11,37 +11,24 @@ import (
 	"github.com/Masterminds/semver/v3"
 	"github.com/fatih/color"
 	"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"
 )
 
-// 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()
 
 // 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.
-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)
-		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
 		defer cancel()
 		release, _, err := ghClient.Repositories.GetLatestRelease(ctx, "porter-dev", "porter")
 		if err == nil {
 			release.GetURL()
 			// 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 {
 				latestRelease, err := semver.NewVersion(strings.TrimPrefix(release.GetTagName(), "v"))
 				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 {
 		color.New(color.FgRed).Println(err)
 		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 (
 	"context"
@@ -12,6 +12,7 @@ import (
 	"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"
 	batchv1 "k8s.io/api/batch/v1"
@@ -41,35 +42,31 @@ var (
 	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(
 		&namespace,
@@ -126,9 +123,10 @@ func init() {
 	)
 
 	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:]
 
 	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 {
 		release, err := client.GetRelease(
-			context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0],
+			ctx, cliConf.Project, cliConf.Cluster, namespace, args[0],
 		)
 		if err != nil {
 			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 {
 		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{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConf,
 	}
 
-	err = config.setSharedConfig()
+	err = config.setSharedConfig(ctx)
 
 	if err != nil {
 		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 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{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err := config.setSharedConfig()
+	err := config.setSharedConfig(ctx)
 	if err != nil {
 		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")
 
 	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 {
 			return err
 		}
 
 		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...)
 			} else {
 				return err
 			}
 		}
 	} else {
-		if pods, err := getEphemeralPods(namespace, config.Clientset); err == nil {
+		if pods, err := getEphemeralPods(ctx, namespace, config.Clientset); err == nil {
 			podNames = append(podNames, pods...)
 		} else {
 			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)
 
 		err = config.Clientset.CoreV1().Pods(namespace).Delete(
-			context.Background(), podName, metav1.DeleteOptions{},
+			ctx, podName, metav1.DeleteOptions{},
 		)
 		if err != nil {
 			return err
@@ -314,11 +314,11 @@ func cleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []stri
 	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
 
 	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 {
 		return nil, err
@@ -332,17 +332,18 @@ func getEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]stri
 }
 
 type PorterRunSharedConfig struct {
-	Client     *api.Client
+	Client     api.Client
 	RestConf   *rest.Config
 	Clientset  *kubernetes.Clientset
 	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 {
 		return err
 	}
@@ -390,11 +391,11 @@ type podSimple struct {
 	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
 	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 {
 		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 {
 		return err
 	}
 
-	newPod, err := createEphemeralPodFromExisting(config, existing, container, args)
+	newPod, err := createEphemeralPodFromExisting(ctx, config, existing, container, args)
 	if err != nil {
 		return err
 	}
 	podName := newPod.ObjectMeta.Name
 
 	// 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)
-	if err = waitForPod(config, newPod); err != nil {
+	if err = waitForPod(ctx, config, newPod); err != nil {
 		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 {
 		return err
 	}
@@ -490,7 +491,7 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 	// refresh pod info for latest status
 	newPod, err = config.Clientset.CoreV1().
 		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.
 	// 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) {
 		color.New(color.FgGreen).Println("complete!")
 		var writtenBytes int64
-		writtenBytes, _ = pipePodLogsToStdout(config, namespace, podName, container, false)
+		writtenBytes, _ = pipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 		if verbose || writtenBytes == 0 {
 			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
 	}
@@ -543,44 +544,44 @@ func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, contain
 		})
 	}); err != nil {
 		// 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 {
 		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
 }
 
-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,
 	// starting with the service account, then role and then a role binding
 
-	err := checkForServiceAccount(config)
+	err := checkForServiceAccount(ctx, config)
 	if err != nil {
 		return err
 	}
 
-	err = checkForClusterRole(config)
+	err = checkForClusterRole(ctx, config)
 	if err != nil {
 		return err
 	}
 
-	err = checkForRoleBinding(config)
+	err = checkForRoleBinding(ctx, config)
 	if err != nil {
 		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 {
 		return err
 	}
 
 	for _, namespace := range namespaces.Items {
 		cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		if err != nil {
 			return err
@@ -596,7 +597,7 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 			for _, cronJob := range cronJobs.Items {
 				if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
 					err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
-						context.Background(), cronJob.Name, metav1.DeleteOptions{},
+						ctx, cronJob.Name, metav1.DeleteOptions{},
 					)
 					if err != nil {
 						return err
@@ -635,7 +636,7 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.BatchV1().CronJobs("default").Create(
-		context.Background(), cronJob, metav1.CreateOptions{},
+		ctx, cronJob, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -644,15 +645,15 @@ func checkForPodDeletionCronJob(config *PorterRunSharedConfig) error {
 	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 {
 		return err
 	}
 
 	for _, namespace := range namespaces.Items {
 		serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
-			context.Background(), metav1.ListOptions{},
+			ctx, metav1.ListOptions{},
 		)
 		if err != nil {
 			return err
@@ -668,7 +669,7 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 			for _, svcAccount := range serviceAccounts.Items {
 				if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
 					err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
-						context.Background(), svcAccount.Name, metav1.DeleteOptions{},
+						ctx, svcAccount.Name, metav1.DeleteOptions{},
 					)
 					if err != nil {
 						return err
@@ -684,7 +685,7 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
-		context.Background(), serviceAccount, metav1.CreateOptions{},
+		ctx, serviceAccount, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -693,9 +694,9 @@ func checkForServiceAccount(config *PorterRunSharedConfig) error {
 	return nil
 }
 
-func checkForClusterRole(config *PorterRunSharedConfig) error {
+func checkForClusterRole(ctx context.Context, config *PorterRunSharedConfig) error {
 	roles, err := config.Clientset.RbacV1().ClusterRoles().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	if err != nil {
 		return err
@@ -725,7 +726,7 @@ func checkForClusterRole(config *PorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoles().Create(
-		context.Background(), role, metav1.CreateOptions{},
+		ctx, role, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -734,9 +735,9 @@ func checkForClusterRole(config *PorterRunSharedConfig) error {
 	return nil
 }
 
-func checkForRoleBinding(config *PorterRunSharedConfig) error {
+func checkForRoleBinding(ctx context.Context, config *PorterRunSharedConfig) error {
 	bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
-		context.Background(), metav1.ListOptions{},
+		ctx, metav1.ListOptions{},
 	)
 	if err != nil {
 		return err
@@ -767,7 +768,7 @@ func checkForRoleBinding(config *PorterRunSharedConfig) error {
 		},
 	}
 	_, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
-		context.Background(), binding, metav1.CreateOptions{},
+		ctx, binding, metav1.CreateOptions{},
 	)
 	if err != nil {
 		return err
@@ -776,7 +777,7 @@ func checkForRoleBinding(config *PorterRunSharedConfig) error {
 	return nil
 }
 
-func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
+func waitForPod(ctx context.Context, config *PorterRunSharedConfig, pod *v1.Pod) error {
 	var (
 		w   watch.Interface
 		err error
@@ -789,7 +790,7 @@ func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
 		selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
 		w, err = config.Clientset.CoreV1().
 			Pods(pod.Namespace).
-			Watch(context.Background(), metav1.ListOptions{FieldSelector: selector})
+			Watch(ctx, metav1.ListOptions{FieldSelector: selector})
 
 		if err == nil {
 			break
@@ -807,7 +808,7 @@ func waitForPod(config *PorterRunSharedConfig, pod *v1.Pod) error {
 			// creating the listener.
 			pod, err = config.Clientset.CoreV1().
 				Pods(pod.Namespace).
-				Get(context.Background(), pod.Name, metav1.GetOptions{})
+				Get(ctx, pod.Name, metav1.GetOptions{})
 			if isPodReady(pod) || isPodExited(pod) {
 				return nil
 			}
@@ -840,23 +841,23 @@ func isPodExited(pod *v1.Pod) bool {
 	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 {
 		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:")
 
 	var writtenBytes int64
-	writtenBytes, _ = pipePodLogsToStdout(config, namespace, podName, container, false)
+	writtenBytes, _ = pipePodLogsToStdout(ctx, config, namespace, podName, container, false)
 
 	if verbose || writtenBytes == 0 {
 		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
 }
 
-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{
 		Container: container,
 		Follow:    follow,
@@ -865,7 +866,7 @@ func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, contain
 	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 	podLogs, err := req.Stream(
-		context.Background(),
+		ctx,
 	)
 	if err != nil {
 		return 0, err
@@ -876,13 +877,13 @@ func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, contain
 	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
-	config.setSharedConfig()
+	config.setSharedConfig(ctx) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
 	// creates the clientset
 	resp, err := config.Clientset.CoreV1().Events(namespace).List(
-		context.TODO(),
+		ctx,
 		metav1.ListOptions{
 			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
 		},
@@ -898,20 +899,20 @@ func pipeEventsToStdout(config *PorterRunSharedConfig, namespace, name, containe
 	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(
-		context.Background(),
+		ctx,
 		name,
 		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
-	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(
-		context.Background(),
+		ctx,
 		name,
 		metav1.DeleteOptions{},
 	)
@@ -926,6 +927,7 @@ func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
 }
 
 func createEphemeralPodFromExisting(
+	ctx context.Context,
 	config *PorterRunSharedConfig,
 	existing *v1.Pod,
 	container string,
@@ -1011,7 +1013,7 @@ func createEphemeralPodFromExisting(
 
 	// create the pod and return it
 	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
-		context.Background(),
+		ctx,
 		newPod,
 		metav1.CreateOptions{},
 	)

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

@@ -1,6 +1,7 @@
-package cmd
+package commands
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"os/exec"
@@ -24,68 +25,72 @@ type startOps struct {
 
 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 {
-					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(stopCmd)
@@ -112,9 +117,12 @@ func init() {
 		8080,
 		"the host port to run the server on",
 	)
+	return serverCmd
 }
 
 func startDocker(
+	ctx context.Context,
+	cliConf config.CLIConfig,
 	imageTag string,
 	db string,
 	port int,
@@ -141,7 +149,7 @@ func startDocker(
 		Env:            env,
 	}
 
-	_, _, err := docker.StartPorter(startOpts)
+	_, _, err := docker.StartPorter(ctx, startOpts)
 	if err != nil {
 		return err
 	}
@@ -154,6 +162,8 @@ func startDocker(
 }
 
 func startLocal(
+	ctx context.Context,
+	cliConf config.CLIConfig,
 	db string,
 	port int,
 ) error {
@@ -169,7 +179,7 @@ func startLocal(
 	staticFilePath := filepath.Join(home, ".porter", "static")
 
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
-		err := downloadMatchingRelease(porterDir)
+		err := downloadMatchingRelease(ctx, porterDir)
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			os.Exit(1)
@@ -184,7 +194,7 @@ func startLocal(
 	err := cmdVersionPorter.Run()
 
 	if err != nil || writer.Version != config.Version {
-		err := downloadMatchingRelease(porterDir)
+		err := downloadMatchingRelease(ctx, porterDir)
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download server binary:", err.Error())
 			os.Exit(1)
@@ -223,13 +233,13 @@ func startLocal(
 	return nil
 }
 
-func stopDocker() error {
-	agent, err := docker.NewAgentFromEnv()
+func stopDocker(ctx context.Context) error {
+	agent, err := docker.NewAgentFromEnv(ctx)
 	if err != nil {
 		return err
 	}
 
-	err = agent.StopPorterContainersWithProcessID("main", false)
+	err = agent.StopPorterContainersWithProcessID(ctx, "main", false)
 
 	if err != nil {
 		return err
@@ -242,7 +252,7 @@ func stopDocker() error {
 	return nil
 }
 
-func downloadMatchingRelease(porterDir string) error {
+func downloadMatchingRelease(ctx context.Context, porterDir string) error {
 	z := &github.ZIPReleaseGetter{
 		AssetName:           "portersvr",
 		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 {
 		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 (
 	"context"
 	"fmt"
 	"os"
 
+	"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"
@@ -13,49 +16,45 @@ import (
 
 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)
 
@@ -98,9 +97,24 @@ func init() {
 
 	stackEnvGroupCmd.AddCommand(stackEnvGroupAddCmd)
 	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]
 
 	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")
 	}
 
-	listStacks, err := client.ListStacks(context.Background(), cliConf.Project, cliConf.Cluster, namespace)
+	listStacks, err := client.ListStacks(ctx, cliConf.Project, cliConf.Cluster, namespace)
 	if err != nil {
 		return err
 	}
@@ -152,7 +166,7 @@ func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	}
 
 	err = client.AddEnvGroupToStack(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, stackID,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, stackID,
 		&types.CreateStackEnvGroupRequest{
 			Name:               envGroupName,
 			Variables:          normalVariables,
@@ -170,7 +184,20 @@ func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 	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]
 
 	if len(envGroupName) == 0 {
@@ -179,7 +206,7 @@ func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 		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 {
 		return err
 	}
@@ -198,7 +225,7 @@ func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 		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)
 
 	if err != nil {

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

@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"context"
@@ -9,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+
 	"github.com/briandowns/spinner"
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
@@ -23,12 +25,33 @@ import (
 	"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
 
 Builds and updates a specified application given by the --app flag. For example:
@@ -61,25 +84,25 @@ specify it as follows:
 
   %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
 
 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
 `,
-		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
 
 Builds a new version of the application specified by the --app flag. Depending on the
@@ -133,24 +156,24 @@ for the application:
 
   %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
 
 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
 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
 
 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
 `,
-		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(
 		&app,
@@ -438,9 +439,24 @@ func init() {
 	updateCmd.AddCommand(updatePushCmd)
 	updateCmd.AddCommand(updateConfigCmd)
 	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)
 	if err != nil {
 		return err
@@ -459,25 +475,22 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 	color.New(color.FgGreen).Println("Deploying app:", app)
 
-	updateAgent, err := updateGetAgent(client)
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 		return err
 	}
 
-	err = updateBuildWithAgent(updateAgent)
-
+	err = updateBuildWithAgent(ctx, updateAgent)
 	if err != nil {
 		return err
 	}
 
-	err = updatePushWithAgent(updateAgent)
-
+	err = updatePushWithAgent(ctx, updateAgent)
 	if err != nil {
 		return err
 	}
 
-	err = updateUpgradeWithAgent(updateAgent)
-
+	err = updateUpgradeWithAgent(ctx, updateAgent)
 	if err != nil {
 		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
 		time.Sleep(10 * time.Second)
 
-		err := checkDeploymentStatus(client)
+		err := checkDeploymentStatus(ctx, client, cliConf)
 		if err != nil {
 			return err
 		}
@@ -495,13 +508,13 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	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 {
 		return err
 	}
 
-	buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+	buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 		UseNewConfig: false,
 	})
 	if err != nil {
@@ -510,7 +523,6 @@ func updateGetEnv(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 
 	// set the environment variables in the process
 	err = updateAgent.SetBuildEnv(buildEnv)
-
 	if err != nil {
 		return err
 	}
@@ -519,16 +531,29 @@ func updateGetEnv(_ *types.GetAuthenticatedUserResponse, client *api.Client, arg
 	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 {
 		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 len(args) == 0 {
 			return fmt.Errorf("please provide the docker image name")
@@ -536,7 +561,7 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 
 		image := args[0]
 
-		registries, err := client.ListRegistries(context.Background(), cliConf.Project)
+		registries, err := client.ListRegistries(ctx, cliConf.Project)
 		if err != nil {
 			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)
 		}
 
-		err = client.CreateRepository(context.Background(), cliConf.Project, regID,
+		err = client.CreateRepository(ctx, cliConf.Project, regID,
 			&types.CreateRegistryRepositoryRequest{
 				ImageRepoURI: strings.Split(image, ":")[0],
 			},
@@ -565,12 +590,12 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 			return err
 		}
 
-		agent, err := docker.NewAgentWithAuthGetter(client, cliConf.Project)
+		agent, err := docker.NewAgentWithAuthGetter(ctx, client, cliConf.Project)
 		if err != nil {
 			return err
 		}
 
-		err = agent.PushImage(image)
+		err = agent.PushImage(ctx, image)
 
 		if err != nil {
 			return err
@@ -579,21 +604,34 @@ func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 		return nil
 	}
 
-	updateAgent, err := updateGetAgent(client)
+	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 		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 {
 		return err
 	}
 
-	err = updateUpgradeWithAgent(updateAgent)
+	err = updateUpgradeWithAgent(ctx, updateAgent)
 
 	if err != nil {
 		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
 		time.Sleep(10 * time.Second)
 
-		err := checkDeploymentStatus(client)
+		err := checkDeploymentStatus(ctx, client, cliConf)
 		if err != nil {
 			return err
 		}
@@ -612,7 +650,7 @@ func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 	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 {
 		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.Start()
 
-	envGroupResp, err := client.GetEnvGroup(context.Background(), cliConf.Project, cliConf.Cluster, namespace,
+	envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
 		&types.GetEnvGroupRequest{
 			Name: name, Version: version,
 		},
@@ -696,7 +734,7 @@ func updateSetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client
 	s.Start()
 
 	_, err = client.CreateEnvGroup(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
 	)
 
 	s.Stop()
@@ -720,7 +758,7 @@ func validateVarValue(in string) (string, string, error) {
 	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 {
 		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.Start()
 
-	envGroupResp, err := client.GetEnvGroup(context.Background(), cliConf.Project, cliConf.Cluster, namespace,
+	envGroupResp, err := client.GetEnvGroup(ctx, cliConf.Project, cliConf.Cluster, namespace,
 		&types.GetEnvGroupRequest{
 			Name: name, Version: version,
 		},
@@ -757,7 +795,7 @@ func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 	s.Start()
 
 	_, err = client.CreateEnvGroup(
-		context.Background(), cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
+		ctx, cliConf.Project, cliConf.Cluster, namespace, newEnvGroup,
 	)
 
 	s.Stop()
@@ -772,7 +810,7 @@ func updateUnsetEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Clie
 }
 
 // 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
 
 	if method != "" {
@@ -789,7 +827,7 @@ func updateGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
 	}
 
 	// initialize the update agent
-	return deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
+	return deploy.NewDeployAgent(ctx, client, app, &deploy.DeployOpts{
 		SharedOpts: &deploy.SharedOpts{
 			ProjectID:       cliConf.Project,
 			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
 	color.New(color.FgGreen).Println("Building docker image for", app)
 
 	if stream {
-		updateAgent.StreamEvent(types.SubEvent{
+		_ = updateAgent.StreamEvent(ctx, types.SubEvent{
 			EventID: "build",
 			Name:    "Build",
 			Index:   100,
 			Status:  types.EventStatusInProgress,
 			Info:    "",
-		})
+		}) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 	}
 
 	if useCache {
-		err := config.SetDockerConfig(updateAgent.Client)
+		err := config.SetDockerConfig(ctx, updateAgent.Client, updateAgent.Opts.ProjectID)
 		if err != nil {
 			return err
 		}
@@ -832,14 +870,14 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 		return err
 	}
 
-	buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+	buildEnv, err := updateAgent.GetBuildEnv(ctx, &deploy.GetBuildEnvOpts{
 		UseNewConfig: true,
 		NewConfig:    valuesObj,
 	})
 	if err != nil {
 		if stream {
 			// 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",
 				Name:    "Build",
 				Index:   110,
@@ -855,7 +893,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 
 	if err != nil {
 		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",
 				Name:    "Build",
 				Index:   120,
@@ -866,9 +904,9 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 		return err
 	}
 
-	if err := updateAgent.Build(nil); err != nil {
+	if err := updateAgent.Build(ctx, nil); err != nil {
 		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",
 				Name:    "Build",
 				Index:   130,
@@ -880,7 +918,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 
 	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",
 			Name:    "Build",
 			Index:   140,
@@ -892,7 +930,7 @@ func updateBuildWithAgent(updateAgent *deploy.DeployAgent) error {
 	return nil
 }
 
-func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
+func updatePushWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
 	if useCache {
 		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)
 
 	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 {
-			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",
 				Name:    "Push",
 				Index:   210,
@@ -926,7 +965,7 @@ func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 
 	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",
 			Name:    "Push",
 			Index:   220,
@@ -938,12 +977,12 @@ func updatePushWithAgent(updateAgent *deploy.DeployAgent) error {
 	return nil
 }
 
-func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
+func updateUpgradeWithAgent(ctx context.Context, updateAgent *deploy.DeployAgent) error {
 	// push the deployment
 	color.New(color.FgGreen).Println("Upgrading configuration for", app)
 
 	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",
 			Name:    "Upgrade",
 			Index:   300,
@@ -962,7 +1001,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 
 	if err != nil {
 		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",
 				Name:    "Upgrade",
 				Index:   310,
@@ -975,6 +1014,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 
 	if len(updateAgent.Opts.AdditionalEnv) > 0 {
 		syncedEnv, err := deploy.GetSyncedEnv(
+			ctx,
 			updateAgent.Client,
 			updateAgent.Release.Config,
 			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 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",
 				Name:    "Upgrade",
 				Index:   320,
@@ -1034,7 +1074,7 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 	}
 
 	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",
 			Name:    "Upgrade",
 			Index:   330,
@@ -1048,14 +1088,15 @@ func updateUpgradeWithAgent(updateAgent *deploy.DeployAgent) error {
 	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")
 
 	sharedConf := &PorterRunSharedConfig{
-		Client: client,
+		Client:    client,
+		CLIConfig: cliConfig,
 	}
 
-	err := sharedConf.setSharedConfig()
+	err := sharedConf.setSharedConfig(ctx)
 	if err != nil {
 		return fmt.Errorf("could not retrieve kubernetes credentials: %w", err)
 	}
@@ -1065,7 +1106,7 @@ func checkDeploymentStatus(client *api.Client) error {
 	success := false
 
 	depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
-		context.Background(),
+		ctx,
 		metav1.ListOptions{
 			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(
-		context.Background(), metav1.ListOptions{
+		ctx, metav1.ListOptions{
 			LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
 		},
 	)
@@ -1120,7 +1161,7 @@ func checkDeploymentStatus(client *api.Client) error {
 			for _, ref := range pod.OwnerReferences {
 				if ref.Kind == "ReplicaSet" {
 					rs, err := sharedConf.Clientset.AppsV1().ReplicaSets(namespace).Get(
-						context.Background(),
+						ctx,
 						ref.Name,
 						metav1.GetOptions{},
 					)
@@ -1147,7 +1188,7 @@ func checkDeploymentStatus(client *api.Client) error {
 	for time.Now().Before(timeWait) {
 		// refresh the client every 10 minutes
 		if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
-			err = sharedConf.setSharedConfig()
+			err = sharedConf.setSharedConfig(ctx)
 
 			if err != nil {
 				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(
-			context.Background(),
+			ctx,
 			rsName,
 			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"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -18,9 +17,6 @@ import (
 
 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.
 // This config is used by viper: calling Set() function for any parameter will
 // update the corresponding field in the viper config file.
@@ -45,78 +41,31 @@ type CLIConfig struct {
 // 2. env
 // 3. config
 // 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.SetConfigType("yaml")
 	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(
 		&config.Driver,
 		"driver",
 		"local",
 		"driver to use (local or docker)",
 	)
+	err = viper.BindPFlags(utils.DriverFlagSet)
+	if err != nil {
+		return config, err
+	}
 
 	utils.DefaultFlagSet.StringVar(
 		&config.Host,
@@ -146,6 +95,11 @@ func initFlagSet() {
 		"token for Porter authentication",
 	)
 
+	err = viper.BindPFlags(utils.DefaultFlagSet)
+	if err != nil {
+		return config, err
+	}
+
 	utils.RegistryFlagSet.UintVar(
 		&config.Registry,
 		"registry",
@@ -153,32 +107,109 @@ func initFlagSet() {
 		"registry ID of connected Porter registry",
 	)
 
+	err = viper.BindPFlags(utils.RegistryFlagSet)
+	if err != nil {
+		return config, err
+	}
+
 	utils.HelmRepoFlagSet.UintVar(
 		&config.HelmRepo,
 		"helmrepo",
 		0,
 		"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 {
 	viper.Set("driver", 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
 	}
 
-	config.Driver = driver
+	c.Driver = driver
 
 	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)
 
-	config.Host = host
-	config.Project = 0
-	config.Cluster = 0
-	config.Token = ""
+	c.Host = host
+	c.Project = 0
+	c.Cluster = 0
+	c.Token = ""
 
 	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)
 
 	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")
 	}
 
@@ -232,16 +264,13 @@ func (c *CLIConfig) SetProject(projectID uint) error {
 		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)
 
-	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
+	if c.Kubeconfig != "" || viper.IsSet("kubeconfig") {
 		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 	}
 
@@ -262,7 +291,7 @@ func (c *CLIConfig) SetCluster(clusterID uint) error {
 		return err
 	}
 
-	config.Cluster = clusterID
+	c.Cluster = clusterID
 
 	return nil
 }
@@ -274,7 +303,7 @@ func (c *CLIConfig) SetToken(token string) error {
 		return err
 	}
 
-	config.Token = token
+	c.Token = token
 
 	return nil
 }
@@ -287,7 +316,7 @@ func (c *CLIConfig) SetRegistry(registryID uint) error {
 		return err
 	}
 
-	config.Registry = registryID
+	c.Registry = registryID
 
 	return nil
 }
@@ -300,7 +329,7 @@ func (c *CLIConfig) SetHelmRepo(helmRepoID uint) error {
 		return err
 	}
 
-	config.HelmRepo = helmRepoID
+	c.HelmRepo = helmRepoID
 
 	return nil
 }
@@ -323,21 +352,22 @@ func (c *CLIConfig) SetKubeconfig(kubeconfig string) error {
 		return err
 	}
 
-	config.Kubeconfig = kubeconfig
+	c.Kubeconfig = kubeconfig
 
 	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")
 	}
 
-	if GetCLIConfig().Project == 0 {
+	if c.Project == 0 {
 		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")
 	}
 

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

@@ -3,7 +3,6 @@ package config
 import (
 	"context"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net/url"
@@ -19,15 +18,14 @@ import (
 	"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
 	regToAdd := make([]string, 0)
 
 	// get the list of namespaces
 	resp, err := client.ListRegistries(
-		context.Background(),
+		ctx,
 		pID,
 	)
 	if err != nil {
@@ -77,14 +75,15 @@ func SetDockerConfig(client *api.Client) error {
 	}
 
 	// 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
 	if !commandExists("docker-credential-porter") {
-		err := downloadCredMatchingRelease()
+		err := downloadCredMatchingRelease(ctx)
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			os.Exit(1)
@@ -99,7 +98,7 @@ func SetDockerConfig(client *api.Client) error {
 	err = cmdVersionCred.Run()
 
 	if err != nil || writer.Version != Version {
-		err := downloadCredMatchingRelease()
+		err := downloadCredMatchingRelease(ctx)
 		if err != nil {
 			color.New(color.FgRed).Println("Failed to download credential helper binary:", err.Error())
 			os.Exit(1)
@@ -110,11 +109,11 @@ func SetDockerConfig(client *api.Client) error {
 		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 {
 		configFile.CredentialHelpers = make(map[string]string)
@@ -138,7 +137,7 @@ func SetDockerConfig(client *api.Client) error {
 
 			if !isAuthenticated {
 				// 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 {
 					return err
 				}
@@ -176,7 +175,7 @@ func commandExists(cmd string) bool {
 	return err == nil
 }
 
-func downloadCredMatchingRelease() error {
+func downloadCredMatchingRelease(ctx context.Context) error {
 	// download the porter cred helper
 	z := &github.ZIPReleaseGetter{
 		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(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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
 	integration, err := client.CreateBasicAuthIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateBasicRequest{
 			Username: username,
@@ -60,7 +61,7 @@ func Dockerhub(
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateRegistryRequest{
 			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
 func DOCR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateRegistryRequest{
 			Name:            regName,
@@ -76,7 +77,7 @@ Registry URL: `))
 	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
 
 	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
 func ECR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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)
 		if err != nil {
 			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)
 
 		integration, err := client.CreateAWSIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateAWSRequest{
 				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)
 
-		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(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 	region string,
 ) (uint, error) {
@@ -102,7 +104,7 @@ func ecrManual(
 
 	// create the aws integration
 	integration, err := client.CreateAWSIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateAWSRequest{
 			AWSAccessKeyID:     accessKeyID,
@@ -116,10 +118,10 @@ func ecrManual(
 
 	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
 	// query for registry 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(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateRegistryRequest{
 			Name:             regName,

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

@@ -15,7 +15,8 @@ import (
 
 // GAR creates a GAR integration
 func GAR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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
 		integration, err := client.CreateGCPIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateGCPRequest{
 				GCPKeyData: string(bytes),
@@ -81,7 +82,7 @@ Artifact registry region: `)
 		}
 
 		reg, err := client.CreateRegistry(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateRegistryRequest{
 				Name:             regName,

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

@@ -15,7 +15,8 @@ import (
 
 // GCR creates a GCR integration
 func GCR(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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
 		integration, err := client.CreateGCPIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateGCPRequest{
 				GCPKeyData: string(bytes),
@@ -65,7 +66,7 @@ Registry URL: `))
 		}
 
 		reg, err := client.CreateRegistry(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateRegistryRequest{
 				Name:             regName,

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

@@ -13,7 +13,8 @@ import (
 )
 
 func HelmRepo(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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 != "" {
 		// create the basic auth integration
 		integration, err := client.CreateBasicAuthIntegration(
-			context.Background(),
+			ctx,
 			projectID,
 			&types.CreateBasicRequest{
 				Username: username,
@@ -70,7 +71,7 @@ Password:`)
 	}
 
 	reg, err := client.CreateHelmRepo(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateUpdateHelmRepoRequest{
 			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 and resolving actions that must be performed.
 func Kubeconfig(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	kubeconfigPath string,
 	contexts []string,
 	projectID uint,
@@ -41,7 +42,7 @@ func Kubeconfig(
 
 	// send kubeconfig to client
 	resp, err := client.CreateProjectCandidates(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateClusterCandidateRequest{
 			Kubeconfig: string(rawBytes),
@@ -166,6 +167,7 @@ func Kubeconfig(
 					}
 				case types.GCPKeyData:
 					err := resolveGCPKeyAction(
+						ctx,
 						cc.Server,
 						cc.Name,
 						allResolver,
@@ -189,7 +191,7 @@ func Kubeconfig(
 			}
 
 			resp, err := client.CreateProjectCluster(
-				context.Background(),
+				ctx,
 				projectID,
 				cc.ID,
 				allResolver,
@@ -203,7 +205,7 @@ func Kubeconfig(
 			cluster = &clExt
 		} else {
 			resp, err := client.GetProjectCluster(
-				context.Background(),
+				ctx,
 				projectID,
 				cc.CreatedClusterID,
 			)
@@ -306,6 +308,7 @@ func resolveTokenDataAction(
 
 // resolves a gcp key data action
 func resolveGCPKeyAction(
+	ctx context.Context,
 	endpoint string,
 	clusterName string,
 	resolver *types.ClusterResolverAll,
@@ -325,7 +328,7 @@ Would you like to proceed? %s `,
 	}
 
 	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
-		agent, err := gcpLocal.NewDefaultAgent()
+		agent, err := gcpLocal.NewDefaultAgent(ctx)
 		if err != nil {
 			color.New(color.FgRed).Fprintf(os.Stderr, "Automatic creation failed, manual input required. Error was: %v\n", err)
 			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
 func Registry(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID uint,
 ) (uint, error) {
 	// 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
 	integration, err := client.CreateBasicAuthIntegration(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateBasicRequest{
 			Username: username,
@@ -55,7 +56,7 @@ Username: `))
 	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
 
 	reg, err := client.CreateRegistry(
-		context.Background(),
+		ctx,
 		projectID,
 		&types.CreateRegistryRequest{
 			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
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -16,7 +17,7 @@ import (
 type BuildAgent struct {
 	*SharedOpts
 
-	APIClient   *api.Client
+	APIClient   api.Client
 	ImageRepo   string
 	Env         map[string]string
 	ImageExists bool
@@ -24,6 +25,7 @@ type BuildAgent struct {
 
 // BuildDocker uses the local Docker daemon to build the image
 func (b *BuildAgent) BuildDocker(
+	ctx context.Context,
 	dockerAgent *docker.Agent,
 	basePath,
 	buildCtx,
@@ -52,15 +54,17 @@ func (b *BuildAgent) BuildDocker(
 	}
 
 	return dockerAgent.BuildLocal(
+		ctx,
 		opts,
 	)
 }
 
 // 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
 	if b.ImageExists {
 		err := dockerAgent.TagImage(
+			ctx,
 			fmt.Sprintf("%s:%s", b.ImageRepo, prevTag),
 			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
-	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

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

@@ -15,7 +15,7 @@ import (
 
 // CreateAgent handles the creation of a new application on Porter
 type CreateAgent struct {
-	Client     *api.Client
+	Client     api.Client
 	CreateOpts *CreateOpts
 }
 
@@ -43,6 +43,7 @@ type GithubOpts struct {
 // 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.
 func (c *CreateAgent) CreateFromGithub(
+	ctx context.Context,
 	ghOpts *GithubOpts,
 	overrideValues map[string]interface{},
 ) (string, error) {
@@ -50,7 +51,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 	// get all linked github repos and find matching repo
 	resp, err := c.Client.ListGitInstallationIDs(
-		context.Background(),
+		ctx,
 		c.CreateOpts.ProjectID,
 	)
 	if err != nil {
@@ -64,7 +65,7 @@ func (c *CreateAgent) CreateFromGithub(
 	for _, gitInstallationID := range gitInstallations {
 		// for each git repo, search for a matching username/owner
 		resp, err := c.Client.ListGitRepos(
-			context.Background(),
+			ctx,
 			c.CreateOpts.ProjectID,
 			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)
 	}
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 		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 {
 		return "", err
 	}
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 		return "", err
 	}
 
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ClusterID,
 		opts.Namespace,
@@ -152,6 +153,7 @@ func (c *CreateAgent) CreateFromGithub(
 
 // CreateFromRegistry deploys a new application from an existing Docker repository + tag.
 func (c *CreateAgent) CreateFromRegistry(
+	ctx context.Context,
 	image string,
 	overrideValues map[string]interface{},
 ) (string, error) {
@@ -168,7 +170,7 @@ func (c *CreateAgent) CreateFromRegistry(
 
 	opts := c.CreateOpts
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 		return "", err
 	}
@@ -178,13 +180,13 @@ func (c *CreateAgent) CreateFromRegistry(
 		"tag":        imageSpl[1],
 	}
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 		return "", err
 	}
 
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ClusterID,
 		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
 // container image, and then deploys it onto Porter.
 func (c *CreateAgent) CreateFromDocker(
+	ctx context.Context,
 	overrideValues map[string]interface{},
 	imageTag string,
 	extraBuildConfig *types.BuildConfig,
@@ -241,12 +244,12 @@ func (c *CreateAgent) CreateFromDocker(
 	}
 
 	// 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 {
 		return "", err
 	}
 
-	latestVersion, mergedValues, err := c.GetMergedValues(overrideValues)
+	latestVersion, mergedValues, err := c.GetMergedValues(ctx, overrideValues)
 	if err != nil {
 		return "", err
 	}
@@ -257,12 +260,12 @@ func (c *CreateAgent) CreateFromDocker(
 	}
 
 	// create docker agent
-	agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
+	agent, err := docker.NewAgentWithAuthGetter(ctx, c.Client, opts.ProjectID)
 	if err != nil {
 		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 {
 		env = make(map[string]string)
 	}
@@ -307,9 +310,9 @@ func (c *CreateAgent) CreateFromDocker(
 			return "", err
 		}
 
-		err = buildAgent.BuildDocker(agent, basePath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
+		err = buildAgent.BuildDocker(ctx, agent, basePath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
 	} else {
-		err = buildAgent.BuildPack(agent, opts.LocalPath, imageTag, "", extraBuildConfig)
+		err = buildAgent.BuildPack(ctx, agent, opts.LocalPath, imageTag, "", extraBuildConfig)
 	}
 
 	if err != nil {
@@ -319,7 +322,7 @@ func (c *CreateAgent) CreateFromDocker(
 	if !opts.SharedOpts.UseCache {
 		// create repository
 		err = c.Client.CreateRepository(
-			context.Background(),
+			ctx,
 			opts.ProjectID,
 			regID,
 			&types.CreateRegistryRepositoryRequest{
@@ -331,20 +334,20 @@ func (c *CreateAgent) CreateFromDocker(
 			return "", err
 		}
 
-		err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, imageTag))
+		err = agent.PushImage(ctx, fmt.Sprintf("%s:%s", imageURL, imageTag))
 
 		if err != nil {
 			return "", err
 		}
 	}
 
-	subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
+	subdomain, err := c.CreateSubdomainIfRequired(ctx, mergedValues)
 	if err != nil {
 		return "", err
 	}
 
 	err = c.Client.DeployTemplate(
-		context.Background(),
+		ctx,
 		opts.ProjectID,
 		opts.ClusterID,
 		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
 // registry linked to Porter, and then generates a new name of the form:
 // `{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 the list of namespaces
 	resp, err := c.Client.ListRegistries(
-		context.Background(),
+		ctx,
 		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
 // 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(
-		context.Background(),
+		ctx,
 		c.CreateOpts.ProjectID,
 		&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
 // 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(
-		context.Background(),
+		ctx,
 		projectID,
 		templateName,
 		templateVersion,
@@ -481,20 +484,21 @@ func (c *CreateAgent) GetLatestTemplateDefaultValues(projectID uint, templateNam
 	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
-	latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
+	latestVersion, err := c.GetLatestTemplateVersion(ctx, c.CreateOpts.Kind)
 	if err != nil {
 		return "", nil, err
 	}
 
 	// 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 {
 		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)
 
 	if err != nil {
@@ -507,7 +511,8 @@ func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (st
 	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 := ""
 
 	// check for automatic subdomain creation if web kind
@@ -543,7 +548,7 @@ func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interfac
 					} else {
 						// in the case of ingress enabled but no custom domain, create subdomain
 						dnsRecord, err := c.Client.CreateDNSRecord(
-							context.Background(),
+							ctx,
 							c.CreateOpts.ProjectID,
 							c.CreateOpts.ClusterID,
 							c.CreateOpts.Namespace,

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

@@ -32,7 +32,7 @@ const (
 type DeployAgent struct {
 	App string
 
-	Client         *client.Client
+	Client         client.Client
 	Opts           *DeployOpts
 	Release        *types.GetReleaseResponse
 	agent          *docker.Agent
@@ -53,7 +53,7 @@ type DeployOpts struct {
 
 // NewDeployAgent creates a new DeployAgent given a Porter API client, application
 // 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{
 		App:    app,
 		Opts:   opts,
@@ -75,7 +75,7 @@ func NewDeployAgent(client *client.Client, app string, opts *DeployOpts) (*Deplo
 	))
 
 	// get docker agent
-	agent, err := docker.NewAgentWithAuthGetter(client, opts.ProjectID)
+	agent, err := docker.NewAgentWithAuthGetter(ctx, client, opts.ProjectID)
 	if err != nil {
 		return nil, err
 	}
@@ -134,10 +134,10 @@ func NewDeployAgent(client *client.Client, app string, opts *DeployOpts) (*Deplo
 
 	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.imageExists = deployAgent.agent.CheckIfImageExists(deployAgent.imageRepo, deployAgent.tag)
+	deployAgent.imageExists = deployAgent.agent.CheckIfImageExists(ctx, deployAgent.imageRepo, deployAgent.tag)
 
 	return deployAgent, err
 }
@@ -154,7 +154,7 @@ type GetBuildEnvOpts struct {
 //  2. container.env.build 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
-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
 
 	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 {
 		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
 // 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
 	currImageSection := d.Release.Config["image"].(map[string]interface{})
 	currentTag := currImageSection["tag"].(string)
@@ -263,7 +263,7 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 		}
 
 		zipResp, err := d.Client.GetRepoZIPDownloadURL(
-			context.Background(),
+			ctx,
 			d.Opts.ProjectID,
 			int64(d.Release.GitActionConfig.GitRepoID),
 			"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 err != nil && err != docker.PullImageErrNotFound {
@@ -311,6 +311,7 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 
 	if d.Opts.Method == DeployBuildTypeDocker {
 		return buildAgent.BuildDocker(
+			ctx,
 			d.agent,
 			basePath,
 			buildCtx,
@@ -326,21 +327,21 @@ func (d *DeployAgent) Build(overrideBuildConfig *types.BuildConfig) error {
 		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
-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
 // configuration passed in via overrrideValues. If overrideValues is nil, it just
 // reuses the configuration set for the application. If overrideValues is not nil,
 // 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
-	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 {
 		return err
 	}
@@ -396,7 +397,7 @@ func (d *DeployAgent) UpdateImageAndValues(overrideValues map[string]interface{}
 	}
 
 	return d.Client.UpgradeRelease(
-		context.Background(),
+		ctx,
 		d.Opts.ProjectID,
 		d.Opts.ClusterID,
 		d.Release.Namespace,
@@ -421,7 +422,8 @@ type SyncedEnvSectionKey struct {
 // GetEnvForRelease gets the env vars for a standard Porter template config. These env
 // vars are found at `container.env.normal` and `container.env.synced`.
 func GetEnvForRelease(
-	client *client.Client,
+	ctx context.Context,
+	client client.Client,
 	config map[string]interface{},
 	projID, clusterID uint,
 	namespace string,
@@ -440,7 +442,7 @@ func GetEnvForRelease(
 
 	// next, get the env vars specified by "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 {
 		return nil, fmt.Errorf("error while fetching container.env.synced variables: %w", err)
 	}
@@ -453,7 +455,7 @@ func GetEnvForRelease(
 }
 
 func GetNormalEnv(
-	client *client.Client,
+	client client.Client,
 	config map[string]interface{},
 	projID, clusterID uint,
 	namespace string,
@@ -487,7 +489,8 @@ func GetNormalEnv(
 }
 
 func GetSyncedEnv(
-	client *client.Client,
+	ctx context.Context,
+	client client.Client,
 	config map[string]interface{},
 	projID, clusterID uint,
 	namespace string,
@@ -590,7 +593,7 @@ func GetSyncedEnv(
 
 		for _, syncedEG := range syncedArr {
 			// 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{
 					Name: syncedEG.Name,
 				},
@@ -638,7 +641,7 @@ func (d *DeployAgent) getReleaseImage() (string, error) {
 	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
 	imageConfig, err := GetNestedMap(d.Release.Config, "image")
 	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))
 
-	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) {
@@ -706,9 +709,10 @@ func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
 	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(
-		context.Background(),
+		ctx,
 		d.Opts.ProjectID, d.Opts.ClusterID,
 		d.Release.Namespace, d.Release.Name,
 		&types.UpdateReleaseStepsRequest{

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

@@ -23,7 +23,8 @@ type SharedOpts struct {
 }
 
 func coalesceEnvGroups(
-	client *api.Client,
+	ctx context.Context,
+	client api.Client,
 	projectID, clusterID uint,
 	namespace string,
 	envGroups []types.EnvGroupMeta,
@@ -35,7 +36,7 @@ func coalesceEnvGroups(
 		}
 
 		envGroup, err := client.GetEnvGroup(
-			context.Background(),
+			ctx,
 			projectID,
 			clusterID,
 			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
-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
-	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 {
 		return err
 	}
@@ -47,7 +48,7 @@ func WaitForJob(client *api.Client, opts *WaitOpts) error {
 
 	for time.Now().Before(timeWait) {
 		// 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 {
 			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 {
 	*client.Client
 	authGetter *AuthGetter
-	ctx        context.Context
 	label      string
 }
 
 // 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
 // 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 {
 		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
 // configured options. The equivalent of:
 //
 // 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[a.label] = "true"
 
@@ -66,7 +65,7 @@ func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
 		Labels: labels,
 	}
 
-	vol, err := a.VolumeCreate(a.ctx, opts)
+	vol, err := a.VolumeCreate(ctx, opts)
 	if err != nil {
 		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
-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
 // 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.
-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 {
 		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)
 // 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[a.label] = "true"
 
@@ -110,7 +109,7 @@ func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
 		Attachable: true,
 	}
 
-	net, err := a.NetworkCreate(a.ctx, name, opts)
+	net, err := a.NetworkCreate(ctx, name, opts)
 	if err != nil {
 		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
-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
-	net, err := a.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
+	net, err := a.NetworkInspect(ctx, networkID, types.NetworkInspectOptions{})
 	if err != nil {
 		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
@@ -167,13 +167,13 @@ func getRegistryRepositoryPair(imageRepo string) ([]string, error) {
 }
 
 // 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 {
 		return false
 	}
 
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	ctx, cancel := context.WithTimeout(ctx, time.Second*5)
 	defer cancel()
 
 	if strings.Contains(imageRepo, "gcr.io") {
@@ -248,12 +248,12 @@ func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
 	}
 
 	image := imageRepo + ":" + imageTag
-	encodedRegistryAuth, err := a.getEncodedRegistryAuth(image)
+	encodedRegistryAuth, err := a.getEncodedRegistryAuth(ctx, image)
 	if err != nil {
 		return false
 	}
 
-	_, err = a.DistributionInspect(context.Background(), image, encodedRegistryAuth)
+	_, err = a.DistributionInspect(ctx, image, encodedRegistryAuth)
 
 	if err == nil {
 		return true
@@ -266,14 +266,14 @@ func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
 }
 
 // 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 {
 		return err
 	}
 
 	// pull the specified image
-	out, err := a.ImagePull(a.ctx, image, opts)
+	out, err := a.ImagePull(ctx, image, opts)
 	if err != nil {
 		if client.IsErrNotFound(err) ||
 			(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
-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 {
 		return err
 	}
 
 	out, err := a.ImagePush(
-		context.Background(),
+		ctx,
 		image,
 		opts,
 	)
@@ -323,13 +323,13 @@ func (a *Agent) PushImage(image string) error {
 	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
 	if a.authGetter == nil {
 		return types.ImagePullOptions{}, nil
 	}
 
-	authConfigEncoded, err := a.getEncodedRegistryAuth(image)
+	authConfigEncoded, err := a.getEncodedRegistryAuth(ctx, image)
 	if err != nil {
 		return types.ImagePullOptions{}, err
 	}
@@ -340,13 +340,13 @@ func (a *Agent) getPullOptions(image string) (types.ImagePullOptions, error) {
 	}, nil
 }
 
-func (a *Agent) getContainerRegistryToken(image string) (string, error) {
+func (a *Agent) getContainerRegistryToken(ctx context.Context, image string) (string, error) {
 	serverURL, err := GetServerURLFromTag(image)
 	if err != nil {
 		return "", err
 	}
 
-	_, secret, err := a.authGetter.GetCredentials(serverURL)
+	_, secret, err := a.authGetter.GetCredentials(ctx, serverURL)
 	if err != nil {
 		return "", err
 	}
@@ -354,14 +354,14 @@ func (a *Agent) getContainerRegistryToken(image string) (string, error) {
 	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
 	serverURL, err := GetServerURLFromTag(image)
 	if err != nil {
 		return "", err
 	}
 
-	user, secret, err := a.authGetter.GetCredentials(serverURL)
+	user, secret, err := a.authGetter.GetCredentials(ctx, serverURL)
 	if err != nil {
 		return "", err
 	}
@@ -385,8 +385,8 @@ func (a *Agent) getEncodedRegistryAuth(image string) (string, error) {
 	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
 }
@@ -430,9 +430,9 @@ func GetServerURLFromTag(image string) (string, error) {
 }
 
 // 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
-	statusCh, errCh := a.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
+	statusCh, errCh := a.ContainerWait(ctx, id, container.WaitConditionNotRunning)
 
 	select {
 	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
 // is the maximum number of failures in a row, while timeout is the length of time between
 // checks.
-func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
+func (a *Agent) WaitForContainerHealthy(ctx context.Context, id string, streak int) error {
 	for {
-		cont, err := a.ContainerInspect(a.ctx, id)
+		cont, err := a.ContainerInspect(ctx, id)
 		if err != nil {
 			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
 type AuthGetter struct {
-	Client    *api.Client
+	Client    api.Client
 	Cache     CredentialsCache
 	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") {
-		return a.GetGCRCredentials(serverURL, a.ProjectID)
+		return a.GetGCRCredentials(ctx, serverURL, a.ProjectID)
 	} 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") {
-		return a.GetDOCRCredentials(serverURL, a.ProjectID)
+		return a.GetDOCRCredentials(ctx, serverURL, a.ProjectID)
 	} 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") {
-		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 {
 		return "", "", err
 	}
@@ -78,7 +80,7 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 	} else {
 		// 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,
 		})
 		if err != nil {
@@ -91,7 +93,7 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}
@@ -99,7 +101,8 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 	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 {
 		return "", "", err
 	}
@@ -123,7 +126,7 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 	} else {
 		// 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,
 		})
 		if err != nil {
@@ -136,7 +139,7 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}
@@ -144,7 +147,8 @@ func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user stri
 	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)
 
 	var token string
@@ -154,7 +158,7 @@ func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user str
 	} else {
 
 		// 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,
 		})
 		if err != nil {
@@ -163,7 +167,7 @@ func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user str
 
 		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
 			a.Cache.Set(serverURL, &AuthEntry{
 				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)?`)
 
-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
 	matches := ecrPattern.FindStringSubmatch(serverURL)
 
@@ -201,7 +206,7 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 	} else {
 		// 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],
 			AccountID: matches[1],
 		})
@@ -215,7 +220,7 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}
@@ -223,7 +228,8 @@ func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user stri
 	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)
 	var token string
 
@@ -231,7 +237,7 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 		token = cachedEntry.AuthorizationToken
 	} else {
 		// get a token from the server
-		tokenResp, err := a.Client.GetDockerhubAuthorizationToken(context.Background(), projID)
+		tokenResp, err := a.Client.GetDockerhubAuthorizationToken(ctx, projID)
 		if err != nil {
 			return "", "", err
 		}
@@ -242,7 +248,7 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}
@@ -250,7 +256,8 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 	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)
 	var token string
 
@@ -258,7 +265,7 @@ func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user stri
 		token = cachedEntry.AuthorizationToken
 	} else {
 		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 {
 			return "", "", err
 		}
@@ -269,7 +276,7 @@ func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user stri
 		a.Cache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          *tokenResp.ExpiresAt,
+			ExpiresAt:          tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}

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

@@ -33,7 +33,7 @@ type BuildOpts struct {
 }
 
 // BuildLocal
-func (a *Agent) BuildLocal(opts *BuildOpts) (err error) {
+func (a *Agent) BuildLocal(ctx context.Context, opts *BuildOpts) (err error) {
 	dockerfilePath := opts.DockerfilePath
 
 	// attempt to read dockerignore file and paths
@@ -84,7 +84,7 @@ func (a *Agent) BuildLocal(opts *BuildOpts) (err error) {
 	inlineCacheVal := "1"
 	buildArgs["BUILDKIT_INLINE_CACHE"] = &inlineCacheVal
 
-	out, err := a.ImageBuild(context.Background(), tar, types.ImageBuildOptions{
+	out, err := a.ImageBuild(ctx, tar, types.ImageBuildOptions{
 		Dockerfile: dockerfilePath,
 		BuildArgs:  buildArgs,
 		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
 // on the host
-func NewAgentFromEnv() (*Agent, error) {
-	ctx := context.Background()
+func NewAgentFromEnv(ctx context.Context) (*Agent, error) {
 	cli, err := client.NewClientWithOpts(
 		client.FromEnv,
 		client.WithAPIVersionNegotiation(),
@@ -23,13 +22,13 @@ func NewAgentFromEnv() (*Agent, error) {
 
 	return &Agent{
 		Client: cli,
-		ctx:    ctx,
 		label:  label,
 	}, 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 {
 		return nil, err
 	}

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

@@ -1,6 +1,7 @@
 package docker
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
@@ -33,8 +34,8 @@ type PorterStartOpts struct {
 
 // StartPorter creates a new Docker agent using the host environment, and creates a
 // 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 {
 		return nil, "", err
@@ -46,7 +47,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 	// the volumes passed to the Porter container
 	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 {
 		return nil, "", err
 	}
@@ -54,7 +55,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 	switch opts.DB {
 	case SQLite:
 		// 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 {
 			return nil, "", err
 		}
@@ -77,7 +78,7 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		}...)
 	case Postgres:
 		// 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 {
 			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 {
 			return nil, "", err
 		}
 
-		err = agent.WaitForContainerHealthy(pgID, 10)
+		err = agent.WaitForContainerHealthy(ctx, pgID, 10)
 
 		if err != nil {
 			return nil, "", err
@@ -144,13 +145,13 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		Env:           opts.Env,
 	}
 
-	id, err = agent.StartPorterContainer(startOpts)
+	id, err = agent.StartPorterContainer(ctx, startOpts)
 
 	if err != nil {
 		return nil, "", err
 	}
 
-	err = agent.WaitForContainerHealthy(id, 10)
+	err = agent.WaitForContainerHealthy(ctx, id, 10)
 
 	if err != nil {
 		return nil, "", err
@@ -174,20 +175,20 @@ type PorterServerStartOpts struct {
 
 // StartPorterContainer pulls a specific Porter image and starts a container
 // 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 {
 		return "", err
 	}
 
-	err = a.startPorterContainer(id)
+	err = a.startPorterContainer(ctx, id)
 
 	if err != nil {
 		return "", err
 	}
 
 	// attach container to network
-	err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
+	err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
 
 	if err != nil {
 		return "", err
@@ -200,20 +201,20 @@ func (a *Agent) StartPorterContainer(opts PorterServerStartOpts) (string, error)
 // if spec has changed, remove and recreate container
 // if container does not exist, create the 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
 	for _, container := range containers {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 			timeout, _ := time.ParseDuration("15s")
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 				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 {
 				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
-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
 	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"
 
 	// 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,
 		Cmd:     opts.StartCmd,
 		Tty:     false,
@@ -265,8 +266,8 @@ func (a *Agent) pullAndCreatePorterContainer(opts PorterServerStartOpts) (id str
 }
 
 // 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")
 	}
 
@@ -285,20 +286,20 @@ type PostgresOpts struct {
 
 // StartPostgresContainer pulls a specific Porter image and starts a container
 // 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 {
 		return "", err
 	}
 
-	err = a.startPostgresContainer(id)
+	err = a.startPostgresContainer(ctx, id)
 
 	if err != nil {
 		return "", err
 	}
 
 	// attach container to network
-	err = a.ConnectContainerToNetwork(opts.NetworkID, id, opts.Name)
+	err = a.ConnectContainerToNetwork(ctx, opts.NetworkID, id, opts.Name)
 
 	if err != nil {
 		return "", err
@@ -311,15 +312,15 @@ func (a *Agent) StartPostgresContainer(opts PostgresOpts) (string, error) {
 // if it is running, stop it
 // if it is stopped, return id
 // 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
 	for _, container := range containers {
 		if len(container.Names) > 0 && container.Names[0] == "/"+opts.Name {
 			timeout, _ := time.ParseDuration("15s")
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 				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
-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[a.label] = "true"
 
 	// 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,
 		Tty:     false,
 		Labels:  labels,
@@ -365,8 +366,8 @@ func (a *Agent) pullAndCreatePostgresContainer(opts PostgresOpts) (id string, er
 }
 
 // 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")
 	}
 
@@ -375,8 +376,8 @@ func (a *Agent) startPostgresContainer(id string) error {
 
 // StopPorterContainers finds all containers that were started via the CLI and stops them
 // -- 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 {
 		return err
 	}
@@ -385,13 +386,13 @@ func (a *Agent) StopPorterContainers(remove bool) error {
 	for _, container := range containers {
 		timeout, _ := time.ParseDuration("15s")
 
-		err := a.ContainerStop(a.ctx, container.ID, &timeout)
+		err := a.ContainerStop(ctx, container.ID, &timeout)
 		if err != nil {
 			return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 		}
 
 		if remove {
-			err = a.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+			err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
 
 			if err != nil {
 				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
 // and have a given process id and stops them -- removes the container if remove is set
 // 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 {
 		return err
 	}
@@ -416,13 +417,13 @@ func (a *Agent) StopPorterContainersWithProcessID(processID string, remove bool)
 		if strings.Contains(container.Names[0], "_"+processID) {
 			timeout, _ := time.ParseDuration("15s")
 
-			err := a.ContainerStop(a.ctx, container.ID, &timeout)
+			err := a.ContainerStop(ctx, container.ID, &timeout)
 			if err != nil {
 				return a.handleDockerClientErr(err, "Could not stop container "+container.ID)
 			}
 
 			if remove {
-				err = a.ContainerRemove(a.ctx, container.ID, types.ContainerRemoveOptions{})
+				err = a.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{})
 
 				if err != nil {
 					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"
 // 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,
 	})
 	if err != nil {

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

@@ -10,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 )
 
+// SentryDSN is a global value for sentry's dsn. This should be removed
 var SentryDSN string = ""
 
 type errorHandler interface {
@@ -18,21 +19,25 @@ type errorHandler interface {
 
 type standardErrorHandler struct{}
 
+// HandleError implements errorhandler for handling non-sentry errors
 func (h *standardErrorHandler) HandleError(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) {
 	if SentryDSN != "" {
 		localHub := sentry.CurrentHub().Clone()
 
 		localHub.ConfigureScope(func(scope *sentry.Scope) {
 			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())
 }
 
-func GetErrorHandler() errorHandler {
+// GetErrorHandler returns an errorhandler.
+func GetErrorHandler(cliConf config.CLIConfig) errorHandler {
 	if SentryDSN != "" {
-		return &sentryErrorHandler{}
+		return &sentryErrorHandler{
+			cliConfig: cliConf,
+		}
 	}
 
 	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
-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 {
 		return err
 	}
@@ -54,8 +54,8 @@ func (z *ZIPReleaseGetter) GetLatestRelease() error {
 }
 
 // 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)
 
@@ -85,10 +85,10 @@ func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
 }
 
 // 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)
 
-	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), z.EntityID, z.RepoName)
+	rel, _, err := client.Repositories.GetLatestRelease(ctx, z.EntityID, z.RepoName)
 	if err != nil {
 		return "", err
 	}
@@ -110,10 +110,10 @@ func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) {
 	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)
 
-	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 {
 		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)
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов