Sfoglia il codice sorgente

Merge pull request #171 from porter-dev/beta.3.provisioning-delete

Beta.3.provisioning delete
abelanger5 5 anni fa
parent
commit
04775207f4

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

@@ -0,0 +1,367 @@
+on:
+  push:
+    tags:
+    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+
+name: Create release w/ binaries and docker image
+
+jobs:
+  docker-build-push:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Get tag name
+      id: tag_name
+      run: |
+        tag=${GITHUB_TAG/refs\/tags\//}
+        echo ::set-output name=tag::$tag
+      env:
+        GITHUB_TAG: ${{ github.ref }}
+    - name: Checkout
+      uses: actions/checkout@v2.3.4
+    - name: Setup docker
+      uses: docker/login-action@v1
+      with:
+        username: ${{ secrets.DOCKERHUB_USERNAME }}
+        password: ${{ secrets.DOCKERHUB_TOKEN }}
+    - name: Write Dashboard Environment Variables
+      run: |
+        cat >./dashboard/.env <<EOL
+        NODE_ENV=production
+        API_SERVER=dashboard.getporter.dev
+        FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
+        DISCORD_KEY=${{secrets.DISCORD_KEY}}
+        DISCORD_CID=${{secrets.DISCORD_CID}}
+        FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+        EOL
+
+        cat ./dashboard/.env
+    - name: Build
+      run: |
+        DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter-prov:${{steps.tag_name.outputs.tag}} -f ./docker/Dockerfile
+    - name: Push
+      run: |
+        docker push gcr.io/porter-dev-273614/porter-prov:${{steps.tag_name.outputs.tag}}
+  build:
+    name: Build binaries
+    runs-on: ubuntu-latest
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.15
+      - name: Build Linux binaries
+        run: |
+          go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=${{steps.tag_name.outputs.tag}}'" -a -tags cli -o ./porter ./cli &
+          go build -ldflags="-w -s -X 'main.Version=${{steps.tag_name.outputs.tag}}'" -a -o ./docker-credential-porter ./cmd/docker-credential-porter/ &
+          go build -ldflags="-w -s" -a -o ./portersvr ./cmd/app/ &
+          wait
+        env:
+          GOOS: linux
+          GOARCH: amd64
+      # Note: we have to zip all binaries before uploading them as artifacts --
+      # without this step, the binaries will be uploaded but the file metadata will
+      # be listed as plaintext after downloading the artifact in a later step
+      # 
+      # TODO: investigate
+      - name: Zip Linux binaries
+        run: |
+          mkdir -p ./release/linux
+          zip --junk-paths ./release/linux/porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip ./porter
+          zip --junk-paths ./release/linux/portersvr_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip ./portersvr
+          zip --junk-paths ./release/linux/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip ./docker-credential-porter
+      - name: Build Darwin binaries
+        run: |
+          go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=${{steps.tag_name.outputs.tag}}'" -a -tags cli -o ./porter ./cli &
+          go build -ldflags="-w -s -X 'main.Version=${{steps.tag_name.outputs.tag}}'" -a -o ./docker-credential-porter ./cmd/docker-credential-porter/ &
+          go build -ldflags="-w -s" -a -o ./portersvr ./cmd/app/ &
+          wait
+        env:
+          GOOS: darwin
+          GOARCH: amd64
+      - name: Zip Darwin binaries
+        run: |
+          mkdir -p ./release/darwin
+          zip --junk-paths ./release/darwin/UNSIGNED_porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip ./porter
+          zip --junk-paths ./release/darwin/UNSIGNED_portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip ./portersvr
+          zip --junk-paths ./release/darwin/UNSIGNED_docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip ./docker-credential-porter
+      - name: Build Windows binaries
+        run: |
+          go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=${{steps.tag_name.outputs.tag}}'" -a -tags cli -o ./porter.exe ./cli &
+          go build -ldflags="-w -s -X 'main.Version=${{steps.tag_name.outputs.tag}}'" -a -o ./docker-credential-porter.exe ./cmd/docker-credential-porter/ &
+          go build -ldflags="-w -s" -a -o ./portersvr.exe ./cmd/app/ &
+          wait
+        env:
+          GOOS: windows
+          GOARCH: amd64
+      - name: Zip Windows binaries
+        run: |
+          mkdir -p ./release/windows
+          zip --junk-paths ./release/windows/porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip ./porter.exe
+          zip --junk-paths ./release/windows/portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip ./portersvr.exe
+          zip --junk-paths ./release/windows/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip ./docker-credential-porter.exe
+      - name: Build and zip static folder
+        run: |
+          mkdir -p ./release/static
+          cd dashboard
+          npm i
+          npm run build
+          cd ..
+          zip --junk-paths ./release/static/static_${{steps.tag_name.outputs.tag}}.zip ./dashboard/build/*
+        env:
+          NODE_ENV: production
+          API_SERVER: ${{ secrets.API_SERVER }}
+          FULLSTORY_ORG_ID: ${{ secrets.FULLSTORY_ORG_ID }}
+          DISCORD_KEY: ${{ secrets.DISCORD_KEY }}
+          DISCORD_CID: ${{ secrets.DISCORD_CID }}
+          FEEDBACK_ENDPOINT: ${{ secrets.FEEDBACK_ENDPOINT }}
+      - name: Upload binaries
+        uses: actions/upload-artifact@v2
+        with:
+          path: ./release
+          name: binaries
+          retention-days: 1
+  notarize:
+    name: Notarize Darwin binaries
+    runs-on: macos-latest
+    needs: build
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Download binaries
+        uses: actions/download-artifact@v2
+        with:
+          name: binaries
+          path: release/
+      - name: Unzip Darwin binaries
+        run: |
+          unzip ./release/darwin/UNSIGNED_porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          unzip ./release/darwin/UNSIGNED_portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          unzip ./release/darwin/UNSIGNED_docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+      - name: Import Code-Signing Certificates
+        uses: Apple-Actions/import-codesign-certs@v1
+        with:
+          # The certificates in a PKCS12 file encoded as a base64 string
+          p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
+          # The password used to import the PKCS12 file.
+          p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
+      - name: Install gon via HomeBrew for code signing and app notarization
+        run: |
+          brew tap mitchellh/gon
+          brew install mitchellh/gon/gon  
+      - name: Create a porter.gon.json file
+        run: |
+          echo "
+          {
+              \"source\": [\"./porter\"],
+              \"bundle_id\": \"cli.porter\",
+              \"apple_id\": {
+                  \"password\":  \"@env:AC_PASSWORD\"
+              },
+              \"sign\": {
+                  \"application_identity\": \"${{ secrets.AC_APPLICATION_IDENTITY }}\"
+              },
+              \"zip\": {
+                  \"output_path\": \"./release/darwin/porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip\"
+              }
+          }
+          " > ./porter.gon.json
+      - name: Create a portersvr.gon.json file
+        run: |
+          echo "
+          {
+              \"source\": [\"./portersvr\"],
+              \"bundle_id\": \"cli.portersvr\",
+              \"apple_id\": {
+                  \"password\":  \"@env:AC_PASSWORD\"
+              },
+              \"sign\": {
+                  \"application_identity\": \"${{ secrets.AC_APPLICATION_IDENTITY }}\"
+              },
+              \"zip\": {
+                  \"output_path\": \"./release/darwin/portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip\"
+              }
+          }
+          " > ./portersvr.gon.json
+      - name: Create a docker-credential-porter.gon.json file
+        run: |
+          echo "
+          {
+              \"source\": [\"./docker-credential-porter\"],
+              \"bundle_id\": \"cli.docker-credential-porter\",
+              \"apple_id\": {
+                  \"password\":  \"@env:AC_PASSWORD\"
+              },
+              \"sign\": {
+                  \"application_identity\": \"${{ secrets.AC_APPLICATION_IDENTITY }}\"
+              },
+              \"zip\": {
+                  \"output_path\": \"./release/darwin/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip\"
+              }
+          }
+          " > ./docker-credential-porter.gon.json
+      - name: Sign the mac binaries with Gon
+        env:
+          AC_USERNAME: ${{ secrets.AC_USERNAME }}
+          AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
+        run: |
+          gon ./porter.gon.json &
+          gon ./portersvr.gon.json &
+          gon ./docker-credential-porter.gon.json &
+          wait
+      - name: Upload binaries
+        uses: actions/upload-artifact@v2
+        with:
+          path: ./release
+          name: binaries
+          retention-days: 1
+  release:
+    name: Zip binaries, create release and upload assets
+    runs-on: ubuntu-latest
+    needs: notarize
+    steps:
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Download binaries
+        uses: actions/download-artifact@v2
+        with:
+          name: binaries
+          path: release/
+      - name: Create Release
+        id: create_release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: ${{ github.ref }}
+          release_name: Release ${{ github.ref }}
+          draft: false
+          prerelease: true
+      - name: Upload Linux CLI Release Asset
+        id: upload-linux-cli-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/linux/porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_name: porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Linux Server Release Asset
+        id: upload-linux-server-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/linux/portersvr_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Linux Docker Credential Release Asset
+        id: upload-linux-docker-cred-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/linux/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Darwin CLI Release Asset
+        id: upload-darwin-cli-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/darwin/porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_name: porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Darwin Server Release Asset
+        id: upload-darwin-server-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/darwin/portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Darwin Docker Credential Release Asset
+        id: upload-darwin-docker-cred-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/darwin/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Windows CLI Release Asset
+        id: upload-windows-cli-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/windows/porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_name: porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Windows Server Release Asset
+        id: upload-windows-server-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/windows/portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Windows Docker Credential Release Asset
+        id: upload-windows-docker-cred-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/windows/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Static Release Asset
+        id: upload-static-release-asset 
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/static/static_${{steps.tag_name.outputs.tag}}.zip
+          asset_name: static_${{steps.tag_name.outputs.tag}}.zip
+          asset_content_type: application/zip

+ 17 - 0
internal/forms/infra.go

@@ -1,9 +1,12 @@
 package forms
 
 import (
+	cmdutils "github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/internal/models"
 )
 
+const randCharset string = "abcdefghijklmnopqrstuvwxyz1234567890"
+
 // CreateECRInfra represents the accepted values for creating an
 // ECR infra via the provisioning container
 type CreateECRInfra struct {
@@ -17,6 +20,7 @@ func (ce *CreateECRInfra) ToAWSInfra() (*models.AWSInfra, error) {
 	return &models.AWSInfra{
 		Kind:             models.AWSInfraECR,
 		ProjectID:        ce.ProjectID,
+		Suffix:           cmdutils.StringWithCharset(6, randCharset),
 		Status:           models.StatusCreating,
 		AWSIntegrationID: ce.AWSIntegrationID,
 	}, nil
@@ -35,7 +39,20 @@ func (ce *CreateEKSInfra) ToAWSInfra() (*models.AWSInfra, error) {
 	return &models.AWSInfra{
 		Kind:             models.AWSInfraEKS,
 		ProjectID:        ce.ProjectID,
+		Suffix:           cmdutils.StringWithCharset(6, randCharset),
 		Status:           models.StatusCreating,
 		AWSIntegrationID: ce.AWSIntegrationID,
 	}, nil
 }
+
+// DestroyECRInfra represents the accepted values for destroying an
+// ECR infra via the provisioning container
+type DestroyECRInfra struct {
+	ECRName string `json:"ecr_name" form:"required"`
+}
+
+// DestroyEKSInfra represents the accepted values for destroying an
+// EKS infra via the provisioning container
+type DestroyEKSInfra struct {
+	EKSName string `json:"eks_name" form:"required"`
+}

+ 31 - 3
internal/forms/registry.go

@@ -1,6 +1,7 @@
 package forms
 
 import (
+	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 )
@@ -16,13 +17,40 @@ type CreateRegistry struct {
 }
 
 // ToRegistry converts the form to a gorm registry model
-func (cr *CreateRegistry) ToRegistry() (*models.Registry, error) {
-	return &models.Registry{
+func (cr *CreateRegistry) ToRegistry(repo repository.Repository) (*models.Registry, error) {
+	registry := &models.Registry{
 		Name:             cr.Name,
 		ProjectID:        cr.ProjectID,
+		URL:              cr.URL,
 		GCPIntegrationID: cr.GCPIntegrationID,
 		AWSIntegrationID: cr.AWSIntegrationID,
-	}, nil
+	}
+
+	if registry.URL == "" && registry.AWSIntegrationID != 0 {
+		awsInt, err := repo.AWSIntegration.ReadAWSIntegration(registry.AWSIntegrationID)
+
+		if err != nil {
+			return nil, err
+		}
+
+		sess, err := awsInt.GetSession()
+
+		if err != nil {
+			return nil, err
+		}
+
+		ecrSvc := ecr.New(sess)
+
+		output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+
+		if err != nil {
+			return nil, err
+		}
+
+		registry.URL = *output.AuthorizationData[0].ProxyEndpoint
+	}
+
+	return registry, nil
 }
 
 // UpdateRegistryForm represents the accepted values for updating a

+ 15 - 9
internal/kubernetes/agent.go

@@ -239,12 +239,14 @@ func (a *Agent) ProvisionECR(
 	awsConf *integrations.AWSIntegration,
 	ecrName string,
 	awsInfra *models.AWSInfra,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 	id := awsInfra.GetID()
 	prov := &provisioner.Conf{
-		ID:   id,
-		Name: fmt.Sprintf("prov-%s", id),
-		Kind: provisioner.ECR,
+		ID:        id,
+		Name:      fmt.Sprintf("prov-%s-%s", id, string(operation)),
+		Kind:      provisioner.ECR,
+		Operation: operation,
 		AWS: &aws.Conf{
 			AWSRegion:          awsConf.AWSRegion,
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
@@ -264,12 +266,14 @@ func (a *Agent) ProvisionEKS(
 	awsConf *integrations.AWSIntegration,
 	eksName string,
 	awsInfra *models.AWSInfra,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 	id := awsInfra.GetID()
 	prov := &provisioner.Conf{
-		ID:   id,
-		Name: fmt.Sprintf("prov-%s", id),
-		Kind: provisioner.EKS,
+		ID:        id,
+		Name:      fmt.Sprintf("prov-%s-%s", id, string(operation)),
+		Kind:      provisioner.EKS,
+		Operation: provisioner.Apply,
 		AWS: &aws.Conf{
 			AWSRegion:          awsConf.AWSRegion,
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
@@ -286,11 +290,13 @@ func (a *Agent) ProvisionEKS(
 // ProvisionTest spawns a new provisioning pod that tests provisioning
 func (a *Agent) ProvisionTest(
 	projectID uint,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 	prov := &provisioner.Conf{
-		ID:   fmt.Sprintf("%s-%d", "testing", projectID),
-		Name: fmt.Sprintf("prov-%s-%d", "testing", projectID),
-		Kind: provisioner.Test,
+		ID:        fmt.Sprintf("%s-%d", "testing", projectID),
+		Name:      fmt.Sprintf("prov-%s-%d-%s", "testing", projectID, string(operation)),
+		Operation: provisioner.Apply,
+		Kind:      provisioner.Test,
 	}
 
 	return a.provision(prov)

+ 38 - 1
internal/kubernetes/provisioner/global_stream.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"regexp"
 
+	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/repository"
 
 	redis "github.com/go-redis/redis/v8"
@@ -141,7 +142,29 @@ func GlobalStreamListener(
 						json.Unmarshal([]byte(dataString), reg)
 					}
 
-					reg, err := repo.Registry.CreateRegistry(reg)
+					awsInt, err := repo.AWSIntegration.ReadAWSIntegration(reg.AWSIntegrationID)
+
+					if err != nil {
+						continue
+					}
+
+					sess, err := awsInt.GetSession()
+
+					if err != nil {
+						continue
+					}
+
+					ecrSvc := ecr.New(sess)
+
+					output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+
+					if err != nil {
+						continue
+					}
+
+					reg.URL = *output.AuthorizationData[0].ProxyEndpoint
+
+					reg, err = repo.Registry.CreateRegistry(reg)
 
 					if err != nil {
 						continue
@@ -191,6 +214,20 @@ func GlobalStreamListener(
 
 				infra, err = repo.AWSInfra.UpdateAWSInfra(infra)
 
+				if err != nil {
+					continue
+				}
+			} else if fmt.Sprintf("%v", msg.Values["status"]) == "destroyed" {
+				infra, err := repo.AWSInfra.ReadAWSInfra(infraID)
+
+				if err != nil {
+					continue
+				}
+
+				infra.Status = models.StatusDestroyed
+
+				infra, err = repo.AWSInfra.UpdateAWSInfra(infra)
+
 				if err != nil {
 					continue
 				}

+ 17 - 3
internal/kubernetes/provisioner/provisioner.go

@@ -30,6 +30,7 @@ type Conf struct {
 	ID        string
 	Redis     *config.RedisConf
 	Postgres  *PostgresConf
+	Operation ProvisionerOperation
 
 	// provider-specific configurations
 	AWS *aws.Conf
@@ -43,9 +44,22 @@ type PostgresConf struct {
 	Port string
 }
 
+type ProvisionerOperation string
+
+const (
+	Apply   ProvisionerOperation = "apply"
+	Destroy ProvisionerOperation = "destroy"
+)
+
 // GetProvisionerJobTemplate returns the manifest that should be applied to
 // create a provisioning job
 func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
+	operation := string(conf.Operation)
+
+	if conf.Operation == "" {
+		operation = string(Apply)
+	}
+
 	env := make([]v1.EnvVar, 0)
 
 	env = conf.attachDefaultEnv(env)
@@ -60,13 +74,13 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 	args := make([]string, 0)
 
 	if conf.Kind == Test {
-		args = []string{"test", "hello"}
+		args = []string{operation, "test", "hello"}
 	} else if conf.Kind == ECR {
-		args = []string{"apply", "ecr"}
+		args = []string{operation, "ecr"}
 		env = conf.AWS.AttachAWSEnv(env)
 		env = conf.ECR.AttachECREnv(env)
 	} else if conf.Kind == EKS {
-		args = []string{"apply", "eks"}
+		args = []string{operation, "eks"}
 		env = conf.AWS.AttachAWSEnv(env)
 		env = conf.EKS.AttachEKSEnv(env)
 	}

+ 9 - 4
internal/models/infra.go

@@ -13,9 +13,11 @@ type InfraStatus string
 
 // The allowed statuses
 const (
-	StatusCreating InfraStatus = "creating"
-	StatusCreated  InfraStatus = "created"
-	StatusError    InfraStatus = "error"
+	StatusCreating   InfraStatus = "creating"
+	StatusCreated    InfraStatus = "created"
+	StatusError      InfraStatus = "error"
+	StatusDestroying InfraStatus = "destroying"
+	StatusDestroyed  InfraStatus = "destroyed"
 )
 
 // AWSInfraKind is the kind that aws infra can be
@@ -35,6 +37,9 @@ type AWSInfra struct {
 	// The type of infra that was provisioned
 	Kind AWSInfraKind `json:"kind"`
 
+	// A random 6-byte suffix to ensure workspace/stream ids are unique
+	Suffix string
+
 	// The project that this infra belongs to
 	ProjectID uint `json:"project_id"`
 
@@ -71,7 +76,7 @@ func (ai *AWSInfra) Externalize() *AWSInfraExternal {
 
 // GetID returns the unique id for this infra
 func (ai *AWSInfra) GetID() string {
-	return fmt.Sprintf("%s-%d-%d", ai.Kind, ai.ProjectID, ai.ID)
+	return fmt.Sprintf("%s-%d-%d-%s", ai.Kind, ai.ProjectID, ai.ID, ai.Suffix)
 }
 
 // ParseWorkspaceID returns the (kind, projectID, infraID)

+ 157 - 6
server/api/provision_handler.go

@@ -2,7 +2,6 @@ package api
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"strconv"
 
@@ -11,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
+	"github.com/porter-dev/porter/internal/models"
 
 	"github.com/porter-dev/porter/internal/adapter"
 )
@@ -33,7 +33,7 @@ func (app *App) HandleProvisionTest(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	_, err = agent.ProvisionTest(uint(projID))
+	_, err = agent.ProvisionTest(uint(projID), provisioner.Apply)
 
 	if err != nil {
 		app.handleErrorInternal(err, w)
@@ -104,6 +104,7 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 		awsInt,
 		form.ECRName,
 		infra,
+		provisioner.Apply,
 	)
 
 	if err != nil {
@@ -123,6 +124,75 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 	}
 }
 
+// HandleDestroyAWSECRInfra destroys ecr infra
+func (app *App) HandleDestroyAWSECRInfra(w http.ResponseWriter, r *http.Request) {
+	// get path parameters
+	infraID, err := strconv.ParseUint(chi.URLParam(r, "infra_id"), 10, 64)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// read infra to get id
+	infra, err := app.Repo.AWSInfra.ReadAWSInfra(uint(infraID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(infra.AWSIntegrationID)
+
+	form := &forms.DestroyECRInfra{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// launch provisioning destruction pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// mark infra for deletion
+	infra.Status = models.StatusDestroying
+	infra, err = app.Repo.AWSInfra.UpdateAWSInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionECR(
+		infra.ProjectID,
+		awsInt,
+		form.ECRName,
+		infra,
+		provisioner.Destroy,
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("AWS ECR infra marked for destruction: %d", infra.ID)
+
+	w.WriteHeader(http.StatusOK)
+}
+
 // HandleProvisionAWSEKSInfra provisions a new aws EKS instance for a project
 func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
@@ -184,6 +254,7 @@ func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Reques
 		awsInt,
 		form.EKSName,
 		infra,
+		provisioner.Apply,
 	)
 
 	if err != nil {
@@ -203,14 +274,94 @@ func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Reques
 	}
 }
 
+// HandleDestroyAWSEKSInfra destroys eks infra
+func (app *App) HandleDestroyAWSEKSInfra(w http.ResponseWriter, r *http.Request) {
+	// get path parameters
+	infraID, err := strconv.ParseUint(chi.URLParam(r, "infra_id"), 10, 64)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// read infra to get id
+	infra, err := app.Repo.AWSInfra.ReadAWSInfra(uint(infraID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(infra.AWSIntegrationID)
+
+	form := &forms.DestroyEKSInfra{}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// launch provisioning destruction pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// mark infra for deletion
+	infra.Status = models.StatusDestroying
+	infra, err = app.Repo.AWSInfra.UpdateAWSInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionEKS(
+		infra.ProjectID,
+		awsInt,
+		form.EKSName,
+		infra,
+		provisioner.Destroy,
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("AWS EKS infra marked for destruction: %d", infra.ID)
+
+	w.WriteHeader(http.StatusOK)
+}
+
 // HandleGetProvisioningLogs returns real-time logs of the provisioning process via websockets
 func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
-	kind := chi.URLParam(r, "kind")
-	projectID := chi.URLParam(r, "project_id")
-	infraID := chi.URLParam(r, "infra_id")
+	infraID, err := strconv.ParseUint(chi.URLParam(r, "infra_id"), 10, 64)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// read infra to get id
+	infra, err := app.Repo.AWSInfra.ReadAWSInfra(uint(infraID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
 
-	streamName := fmt.Sprintf("%s-%s-%s", kind, projectID, infraID)
+	streamName := infra.GetID()
 
 	upgrader.CheckOrigin = func(r *http.Request) bool { return true }
 

+ 1 - 29
server/api/registry_handler.go

@@ -41,41 +41,13 @@ func (app *App) HandleCreateRegistry(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// convert the form to a registry
-	registry, err := form.ToRegistry()
+	registry, err := form.ToRegistry(*app.Repo)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 
-	// if the registry is ECR and URL is not set, get the registry url
-	if registry.URL == "" && registry.AWSIntegrationID != 0 {
-		awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(registry.AWSIntegrationID)
-
-		if err != nil {
-			app.handleErrorDataRead(err, w)
-			return
-		}
-
-		sess, err := awsInt.GetSession()
-
-		if err != nil {
-			app.handleErrorDataRead(err, w)
-			return
-		}
-
-		ecrSvc := ecr.New(sess)
-
-		output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
-
-		if err != nil {
-			app.handleErrorDataRead(err, w)
-			return
-		}
-
-		registry.URL = *output.AuthorizationData[0].ProxyEndpoint
-	}
-
 	// handle write to the database
 	registry, err = app.Repo.Registry.CreateRegistry(registry)
 

+ 98 - 0
server/router/middleware/auth.go

@@ -78,6 +78,10 @@ type bodyGitRepoID struct {
 	GitRepoID uint64 `json:"git_repo_id"`
 }
 
+type bodyInfraID struct {
+	InfraID uint64 `json:"infra_id"`
+}
+
 // DoesUserIDMatch checks the id URL parameter and verifies that it matches
 // the one stored in the session
 func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handler {
@@ -314,6 +318,55 @@ func (auth *Auth) DoesUserHaveGitRepoAccess(
 	})
 }
 
+// DoesUserHaveInfraAccess looks for a project_id parameter and an
+// infra_id parameter, and verifies that the infra belongs
+// to the project
+func (auth *Auth) DoesUserHaveInfraAccess(
+	next http.Handler,
+	projLoc IDLocation,
+	infraLoc IDLocation,
+) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		infraID, err := findGitRepoIDInRequest(r, infraLoc)
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
+
+		projID, err := findProjIDInRequest(r, projLoc)
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
+
+		infras, err := auth.repo.AWSInfra.ListAWSInfrasByProjectID(uint(projID))
+
+		if err != nil {
+			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+			return
+		}
+
+		doesExist := false
+
+		for _, infra := range infras {
+			if infra.ID == uint(infraID) {
+				doesExist = true
+				break
+			}
+		}
+
+		if doesExist {
+			next.ServeHTTP(w, r)
+			return
+		}
+
+		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+		return
+	})
+}
+
 // Helpers
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 	session, _ := auth.store.Get(r, auth.cookieName)
@@ -565,3 +618,48 @@ func findGitRepoIDInRequest(r *http.Request, gitRepoLoc IDLocation) (uint64, err
 
 	return grID, nil
 }
+
+func findInfraIDInRequest(r *http.Request, infraLoc IDLocation) (uint64, error) {
+	var infraID uint64
+	var err error
+
+	if infraLoc == URLParam {
+		infraID, err = strconv.ParseUint(chi.URLParam(r, "infra_id"), 0, 64)
+
+		if err != nil {
+			return 0, err
+		}
+	} else if infraLoc == BodyParam {
+		form := &bodyInfraID{}
+		body, err := ioutil.ReadAll(r.Body)
+
+		if err != nil {
+			return 0, err
+		}
+
+		err = json.Unmarshal(body, form)
+
+		if err != nil {
+			return 0, err
+		}
+
+		infraID = form.InfraID
+
+		// need to create a new stream for the body
+		r.Body = ioutil.NopCloser(bytes.NewReader(body))
+	} else {
+		vals, err := url.ParseQuery(r.URL.RawQuery)
+
+		if err != nil {
+			return 0, err
+		}
+
+		if regStrArr, ok := vals["infra_id"]; ok && len(regStrArr) == 1 {
+			infraID, err = strconv.ParseUint(regStrArr[0], 10, 64)
+		} else {
+			return 0, errors.New("infra id not found")
+		}
+	}
+
+	return infraID, nil
+}

+ 47 - 1
server/router/router.go

@@ -223,7 +223,53 @@ func New(a *api.App) *chi.Mux {
 			"GET",
 			"/projects/{project_id}/provision/{kind}/{infra_id}/logs",
 			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleGetProvisioningLogs, l),
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleGetProvisioningLogs, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/provision/{kind}/{infra_id}/logs",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleGetProvisioningLogs, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/infra/{infra_id}/ecr/destroy",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleDestroyAWSECRInfra, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/projects/{project_id}/infra/{infra_id}/eks/destroy",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveInfraAccess(
+					requestlog.NewHandler(a.HandleDestroyAWSEKSInfra, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
 				mw.URLParam,
 				mw.ReadAccess,
 			),