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

Merge branch 'beta.3.provisioning-delete' of https://github.com/porter-dev/porter into beta.3.integration-frontend

jusrhee 5 лет назад
Родитель
Сommit
f8f176de7f

+ 17 - 3
.github/workflows/gcr.yaml

@@ -1,10 +1,10 @@
 name: Build, Push to GCR.
 name: Build, Push to GCR.
 on:
 on:
   push:
   push:
-    branches:
+    branches: 
     - staging
     - staging
 jobs:
 jobs:
-  build-push-test:
+  login-build-push:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
     - name: Set up Cloud SDK
     - name: Set up Cloud SDK
@@ -17,7 +17,21 @@ jobs:
       run: gcloud auth configure-docker
       run: gcloud auth configure-docker
     - name: Checkout
     - name: Checkout
       uses: actions/checkout@v2.3.4
       uses: actions/checkout@v2.3.4
+    - name: Write Dashboard Environment Variables
+      run: |
+        cat >./dashboard/.env <<EOL
+        NODE_ENV=development
+        API_SERVER=localhost:8080
+        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
     - name: Build
       run: |
       run: |
-        docker build . -t gcr.io/porter-dev-273614/porter-prov:latest -f ./docker/Dockerfile
+        DOCKER_BUILDKIT=1 docker build . -t gcr.io/porter-dev-273614/porter-prov:latest -f ./docker/Dockerfile
+    - name: Push
+      run: |
         docker push gcr.io/porter-dev-273614/porter-prov:latest
         docker push gcr.io/porter-dev-273614/porter-prov:latest

+ 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

+ 5 - 0
dashboard/package-lock.json

@@ -981,6 +981,11 @@
       "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
       "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
       "dev": true
       "dev": true
     },
     },
+    "ansi-parser": {
+      "version": "3.2.10",
+      "resolved": "https://registry.npmjs.org/ansi-parser/-/ansi-parser-3.2.10.tgz",
+      "integrity": "sha512-CGKGIbd678lm15IXJXI1cTyOVAnMQw0jES+klW/yIc+GzYccsYanLMhczPIIj2hE64B79g75QfiuWrEWd6nJdg=="
+    },
     "ansi-regex": {
     "ansi-regex": {
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",

+ 2 - 2
dashboard/package.json

@@ -12,6 +12,7 @@
     "ace-builds": "^1.4.12",
     "ace-builds": "^1.4.12",
     "axios": "^0.20.0",
     "axios": "^0.20.0",
     "dotenv": "^8.2.0",
     "dotenv": "^8.2.0",
+    "ini": ">=1.3.6",
     "js-base64": "^3.6.0",
     "js-base64": "^3.6.0",
     "js-yaml": "^3.14.0",
     "js-yaml": "^3.14.0",
     "lodash": "^4.17.20",
     "lodash": "^4.17.20",
@@ -24,8 +25,7 @@
     "react-dom": "^16.13.1",
     "react-dom": "^16.13.1",
     "react-modal": "^3.11.2",
     "react-modal": "^3.11.2",
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
-    "styled-components": "^5.2.0",
-    "ini": ">=1.3.6"
+    "styled-components": "^5.2.0"
   },
   },
   "scripts": {
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "test": "echo \"Error: no test specified\" && exit 1",

+ 17 - 11
dashboard/src/main/home/Home.tsx

@@ -28,7 +28,7 @@ type StateType = {
   forceSidebar: boolean,
   forceSidebar: boolean,
   showWelcome: boolean,
   showWelcome: boolean,
   currentView: string,
   currentView: string,
-  viewData: any,
+  viewData: any[],
 
 
   // Track last project id for refreshing clusters on project change
   // Track last project id for refreshing clusters on project change
   prevProjectId: number | null,
   prevProjectId: number | null,
@@ -59,16 +59,22 @@ export default class Home extends Component<PropsType, StateType> {
             if (err) {
             if (err) {
               console.log(err);
               console.log(err);
             } else if (res.data) {
             } else if (res.data) {
-              
+
+              let viewData = [] as any[]
               // TODO: separately handle non meta-provisioning case
               // TODO: separately handle non meta-provisioning case
               res.data.forEach((el: InfraType) => {
               res.data.forEach((el: InfraType) => {
                 if (el.status === 'creating') {
                 if (el.status === 'creating') {
-                  this.setState({ currentView: 'provisioner', viewData: {
+                  viewData.push({
                     infra_id: el.id,
                     infra_id: el.id,
                     kind: el.kind,
                     kind: el.kind,
-                  }});
+                  })
                 }
                 }
               });
               });
+
+              if (viewData.length > 0) {
+                this.setState({ currentView: 'provisioner', viewData});
+              }
+
             }
             }
           });
           });
         } else if (res.data.length === 0) {
         } else if (res.data.length === 0) {
@@ -108,7 +114,7 @@ export default class Home extends Component<PropsType, StateType> {
               setCurrentModal('ClusterConfigModal', { currentTab: 'select' });
               setCurrentModal('ClusterConfigModal', { currentTab: 'select' });
             }}>Select Clusters</A> tab.<br /><br />
             }}>Select Clusters</A> tab.<br /><br />
             3. For additional information, please refer to our <A>docs</A>.<br /><br /><br />
             3. For additional information, please refer to our <A>docs</A>.<br /><br /><br />
-            
+
             * Make sure all fields are explicitly declared (e.g., certs and keys).
             * Make sure all fields are explicitly declared (e.g., certs and keys).
           </Placeholder>
           </Placeholder>
         </DashboardWrapper>
         </DashboardWrapper>
@@ -154,8 +160,8 @@ export default class Home extends Component<PropsType, StateType> {
     }
     }
 
 
     return (
     return (
-      <Templates 
-        setCurrentView={(x: string) => this.setState({ currentView: x })} 
+      <Templates
+        setCurrentView={(x: string) => this.setState({ currentView: x })}
       />
       />
     );
     );
   }
   }
@@ -171,11 +177,11 @@ export default class Home extends Component<PropsType, StateType> {
   renderSidebar = () => {
   renderSidebar = () => {
     if (this.context.projects.length > 0) {
     if (this.context.projects.length > 0) {
 
 
-      // Force sidebar closed on first provision 
+      // Force sidebar closed on first provision
       if (this.state.currentView === 'provisioner' && this.state.forceSidebar) {
       if (this.state.currentView === 'provisioner' && this.state.forceSidebar) {
         this.setState({ forceSidebar: false });
         this.setState({ forceSidebar: false });
       }
       }
-      
+
       return (
       return (
         <Sidebar
         <Sidebar
           forceSidebar={this.state.forceSidebar}
           forceSidebar={this.state.forceSidebar}
@@ -227,8 +233,8 @@ export default class Home extends Component<PropsType, StateType> {
         {this.renderSidebar()}
         {this.renderSidebar()}
 
 
         <ViewWrapper>
         <ViewWrapper>
-          <Navbar 
-            logOut={this.props.logOut} 
+          <Navbar
+            logOut={this.props.logOut}
             currentView={this.state.currentView} // For form feedback
             currentView={this.state.currentView} // For form feedback
           />
           />
           {this.renderContents()}
           {this.renderContents()}

+ 3 - 1
dashboard/src/main/home/new-project/NewProject.tsx

@@ -223,11 +223,13 @@ export default class NewProject extends Component<PropsType, StateType> {
 
 
   provisionEKS = (proj: ProjectType, ecr: any) => {
   provisionEKS = (proj: ProjectType, ecr: any) => {
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
     let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+    let clusterName = `${proj.name}-cluster`
 
 
     api.createAWSIntegration('<token>', {
     api.createAWSIntegration('<token>', {
       aws_region: awsRegion,
       aws_region: awsRegion,
       aws_access_key_id: awsAccessId,
       aws_access_key_id: awsAccessId,
       aws_secret_access_key: awsSecretKey,
       aws_secret_access_key: awsSecretKey,
+      aws_cluster_id: clusterName,
     }, { id: proj.id }, (err: any, res: any) => {
     }, { id: proj.id }, (err: any, res: any) => {
       if (err) {
       if (err) {
         console.log(err)
         console.log(err)
@@ -236,7 +238,7 @@ export default class NewProject extends Component<PropsType, StateType> {
 
 
       api.provisionEKS('<token>', {
       api.provisionEKS('<token>', {
         aws_integration_id: res.data.id,
         aws_integration_id: res.data.id,
-        eks_name: `${proj.name}-cluster`,
+        eks_name: clusterName,
       }, { id: proj.id}, (err: any, eks: any) => {
       }, { id: proj.id}, (err: any, eks: any) => {
         if (err) {
         if (err) {
           console.log(err)
           console.log(err)

+ 63 - 15
dashboard/src/main/home/new-project/Provisioner.tsx

@@ -3,11 +3,13 @@ import styled from 'styled-components';
 
 
 import api from '../../../shared/api';
 import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
 import { Context } from '../../../shared/Context';
+import ansiparse from '../../../shared/ansiparser'
 import { integrationList } from '../../../shared/common';
 import { integrationList } from '../../../shared/common';
 import loading from '../../../assets/loading.gif';
 import loading from '../../../assets/loading.gif';
 
 
 import Helper from '../../../components/values-form/Helper';
 import Helper from '../../../components/values-form/Helper';
 import { eventNames } from 'process';
 import { eventNames } from 'process';
+import { inflateRaw, inflateRawSync } from 'zlib';
 
 
 type PropsType = {
 type PropsType = {
   viewData: any,
   viewData: any,
@@ -33,11 +35,23 @@ export default class Provisioner extends Component<PropsType, StateType> {
     this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight
     this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight
   }
   }
 
 
+  isJSON = (str: string) => {
+    try {
+        JSON.parse(str);
+    } catch (e) {
+        return false;
+    }
+    return true;
+  }
+
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+    let viewData = this.props.viewData || []
 
 
-    let websockets = this.props.viewData.forEach((infra: any) => {
+    console.log("viewData", viewData)
+
+    let websockets = viewData.map((infra: any) => {
       let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
       let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
       
       
       ws.onopen = () => {
       ws.onopen = () => {
@@ -46,32 +60,63 @@ export default class Provisioner extends Component<PropsType, StateType> {
 
 
       ws.onmessage = (evt: MessageEvent) => {
       ws.onmessage = (evt: MessageEvent) => {
         let event = JSON.parse(evt.data)
         let event = JSON.parse(evt.data)
-        let data = event.map((msg: any) => { return `${infra.kind}: ${msg["Values"]["data"]}` })
+        console.log(event)
+        let validEvents = [] as any[]
         let err = null
         let err = null
-
-        // check for error
-        event.forEach((e: any) => {
-          err = e["Values"]["kind"] == "error" ? e["Values"]["data"] : null
-        })
-
+        
+        for (var i = 0; i < event.length; i++) {
+          let msg = event[i]
+          if (msg["Values"] && msg["Values"]["data"] && this.isJSON(msg["Values"]["data"])) { 
+            let d = JSON.parse(msg["Values"]["data"])
+  
+            if (d["kind"] == "error") {
+              err = d["log"]
+              break;
+            }
+  
+            // add only valid events
+            if (d["log"] != null && d["created_resources"] != null && d["total_resources"] != null) {
+              validEvents.push(d)
+            }
+          }
+        }
+  
         if (err) {
         if (err) {
-          this.setState({ logs: [err] })
+          let e = ansiparse(err).map((el: any) => {
+            return el.text
+          })
+          console.log("error", e)
+          this.setState({ logs: e })
+          return;
+        }
+  
+        if (validEvents.length == 0) {
+          return;
         }
         }
         
         
-        if (!this.state.maxStep[infra.kind]) {
+        if (!this.state.maxStep[infra.kind] || !this.state.maxStep[infra.kind]["total_resources"]) {
           this.setState({
           this.setState({
             maxStep: {
             maxStep: {
               ...this.state.maxStep,
               ...this.state.maxStep,
-              [infra.kind] : event[event.length]["Values"]["created_resources"]
+              [infra.kind] : validEvents[validEvents.length - 1]["total_resources"]
             }
             }
           })
           })
         }
         }
+        
+        let logs = [] as any[]
+        validEvents.forEach((e: any) => {
+          logs.push(...ansiparse(e["log"]))
+        })
 
 
+        logs = logs.map((log: any) => {
+          return log.text
+        })
+  
         this.setState({ 
         this.setState({ 
-          logs: [...this.state.logs, ...data], 
+          logs: [...this.state.logs, ...logs], 
           currentStep: {
           currentStep: {
             ...this.state.currentStep,
             ...this.state.currentStep,
-            [infra.kind] : event[event.length]["Values"]["created_resources"]
+            [infra.kind] : validEvents[validEvents.length - 1]["created_resources"]
           },
           },
         }, () => {
         }, () => {
           this.scrollToBottom()
           this.scrollToBottom()
@@ -93,7 +138,9 @@ export default class Provisioner extends Component<PropsType, StateType> {
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    this.state.websockets?.forEach((ws) => {
+    if (!this.state.websockets) { return; }
+
+    this.state.websockets.forEach((ws) => {
       ws.close()
       ws.close()
     })
     })
   }
   }
@@ -111,6 +158,7 @@ export default class Provisioner extends Component<PropsType, StateType> {
     let currentStep = 0;
     let currentStep = 0;
 
 
     for (let key in this.state.maxStep) {
     for (let key in this.state.maxStep) {
+      console.log(key)
       maxStep += this.state.maxStep[key]
       maxStep += this.state.maxStep[key]
     }
     }
 
 
@@ -133,7 +181,7 @@ export default class Provisioner extends Component<PropsType, StateType> {
         </Helper>
         </Helper>
 
 
         <LoadingBar>
         <LoadingBar>
-          <Loaded progress={((currentStep / maxStep) * 100).toString() + '%'} />
+          <Loaded progress={((currentStep / (maxStep == 0 ? 1 : maxStep)) * 100).toString() + '%'} />
         </LoadingBar>
         </LoadingBar>
 
 
         <LogStream ref={this.scrollRef}>
         <LogStream ref={this.scrollRef}>

+ 134 - 0
dashboard/src/shared/ansiparser.tsx

@@ -0,0 +1,134 @@
+/* eslint-disable no-plusplus, no-continue */
+const foregroundColors = {
+    '30': 'black',
+    '31': 'red',
+    '32': 'green',
+    '33': 'yellow',
+    '34': 'blue',
+    '35': 'magenta',
+    '36': 'cyan',
+    '37': 'white',
+    '90': 'grey',
+  } as Record<string, string>;
+
+  const backgroundColors = {
+    '40': 'black',
+    '41': 'red',
+    '42': 'green',
+    '43': 'yellow',
+    '44': 'blue',
+    '45': 'magenta',
+    '46': 'cyan',
+    '47': 'white',
+  } as Record<string, string>;
+
+  const styles = {
+    '1': 'bold',
+    '3': 'italic',
+    '4': 'underline',
+  } as Record<string, string>;
+
+  const eraseChar = (matchingText: any, result: any) => {
+    if (matchingText.length) {
+      return [matchingText.substr(0, matchingText.length - 1), result];
+    } else if (result.length) {
+      const index = result.length - 1;
+      const { text } = result[index];
+      const newResult =
+        text.length === 1
+          ? result.slice(0, result.length - 1)
+          : result.map((item: any, i: number) =>
+              index === i
+                ? { ...item, text: text.substr(0, text.length - 1) }
+                : item
+            );
+  
+      return [matchingText, newResult];
+    }
+  
+    return [matchingText, result];
+  };
+  
+  const ansiparse = (str: string) => {
+    let matchingControl = null;
+    let matchingData = null;
+    let matchingText = '';
+    let ansiState = [] as any[];
+    let result = [] as any[];
+    let state = {} as any;
+  
+    for (let i = 0; i < str.length; i++) {
+      if (matchingControl !== null) {
+        if (matchingControl === '\x1b' && str[i] === '[') {
+          if (matchingText) {
+            state.text = matchingText;
+            result.push(state);
+            state = {};
+            matchingText = '';
+          }
+  
+          matchingControl = null;
+          matchingData = '';
+        } else {
+          matchingText += matchingControl + str[i];
+          matchingControl = null;
+        }
+  
+        continue;
+      } else if (matchingData !== null) {
+        if (str[i] === ';') {
+          ansiState.push(matchingData);
+          matchingData = '';
+        } else if (str[i] === 'm') {
+          ansiState.push(matchingData);
+          matchingData = null;
+          matchingText = '';
+  
+          for (let a = 0; a < ansiState.length; a++) {
+            const ansiCode = ansiState[a];
+  
+            if (foregroundColors[ansiCode]) {
+              state.foreground = foregroundColors[ansiCode];
+            } else if (backgroundColors[ansiCode]) {
+              state.background = backgroundColors[ansiCode];
+            } else if (ansiCode === 39) {
+              delete state.foreground;
+            } else if (ansiCode === 49) {
+              delete state.background;
+            } else if (styles[ansiCode]) {
+              state[styles[ansiCode]] = true;
+            } else if (ansiCode === 22) {
+              state.bold = false;
+            } else if (ansiCode === 23) {
+              state.italic = false;
+            } else if (ansiCode === 24) {
+              state.underline = false;
+            }
+          }
+  
+          ansiState = [];
+        } else {
+          matchingData += str[i];
+        }
+  
+        continue;
+      }
+  
+      if (str[i] === '\x1b') {
+        matchingControl = str[i];
+      } else if (str[i] === '\u0008') {
+        [matchingText, result] = eraseChar(matchingText, result);
+      } else {
+        matchingText += str[i];
+      }
+    }
+  
+    if (matchingText) {
+      state.text = matchingText + (matchingControl || '');
+      result.push(state);
+    }
+  
+    return result;
+  };
+  
+  export default ansiparse;

+ 17 - 0
internal/forms/infra.go

@@ -1,9 +1,12 @@
 package forms
 package forms
 
 
 import (
 import (
+	cmdutils "github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 )
 )
 
 
+const randCharset string = "abcdefghijklmnopqrstuvwxyz1234567890"
+
 // CreateECRInfra represents the accepted values for creating an
 // CreateECRInfra represents the accepted values for creating an
 // ECR infra via the provisioning container
 // ECR infra via the provisioning container
 type CreateECRInfra struct {
 type CreateECRInfra struct {
@@ -17,6 +20,7 @@ func (ce *CreateECRInfra) ToAWSInfra() (*models.AWSInfra, error) {
 	return &models.AWSInfra{
 	return &models.AWSInfra{
 		Kind:             models.AWSInfraECR,
 		Kind:             models.AWSInfraECR,
 		ProjectID:        ce.ProjectID,
 		ProjectID:        ce.ProjectID,
+		Suffix:           cmdutils.StringWithCharset(6, randCharset),
 		Status:           models.StatusCreating,
 		Status:           models.StatusCreating,
 		AWSIntegrationID: ce.AWSIntegrationID,
 		AWSIntegrationID: ce.AWSIntegrationID,
 	}, nil
 	}, nil
@@ -35,7 +39,20 @@ func (ce *CreateEKSInfra) ToAWSInfra() (*models.AWSInfra, error) {
 	return &models.AWSInfra{
 	return &models.AWSInfra{
 		Kind:             models.AWSInfraEKS,
 		Kind:             models.AWSInfraEKS,
 		ProjectID:        ce.ProjectID,
 		ProjectID:        ce.ProjectID,
+		Suffix:           cmdutils.StringWithCharset(6, randCharset),
 		Status:           models.StatusCreating,
 		Status:           models.StatusCreating,
 		AWSIntegrationID: ce.AWSIntegrationID,
 		AWSIntegrationID: ce.AWSIntegrationID,
 	}, nil
 	}, 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
 package forms
 
 
 import (
 import (
+	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 )
 )
@@ -16,13 +17,40 @@ type CreateRegistry struct {
 }
 }
 
 
 // ToRegistry converts the form to a gorm registry model
 // 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,
 		Name:             cr.Name,
 		ProjectID:        cr.ProjectID,
 		ProjectID:        cr.ProjectID,
+		URL:              cr.URL,
 		GCPIntegrationID: cr.GCPIntegrationID,
 		GCPIntegrationID: cr.GCPIntegrationID,
 		AWSIntegrationID: cr.AWSIntegrationID,
 		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
 // 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,
 	awsConf *integrations.AWSIntegration,
 	ecrName string,
 	ecrName string,
 	awsInfra *models.AWSInfra,
 	awsInfra *models.AWSInfra,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 ) (*batchv1.Job, error) {
 	id := awsInfra.GetID()
 	id := awsInfra.GetID()
 	prov := &provisioner.Conf{
 	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{
 		AWS: &aws.Conf{
 			AWSRegion:          awsConf.AWSRegion,
 			AWSRegion:          awsConf.AWSRegion,
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
@@ -264,12 +266,14 @@ func (a *Agent) ProvisionEKS(
 	awsConf *integrations.AWSIntegration,
 	awsConf *integrations.AWSIntegration,
 	eksName string,
 	eksName string,
 	awsInfra *models.AWSInfra,
 	awsInfra *models.AWSInfra,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 ) (*batchv1.Job, error) {
 	id := awsInfra.GetID()
 	id := awsInfra.GetID()
 	prov := &provisioner.Conf{
 	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{
 		AWS: &aws.Conf{
 			AWSRegion:          awsConf.AWSRegion,
 			AWSRegion:          awsConf.AWSRegion,
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
 			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
@@ -286,11 +290,13 @@ func (a *Agent) ProvisionEKS(
 // ProvisionTest spawns a new provisioning pod that tests provisioning
 // ProvisionTest spawns a new provisioning pod that tests provisioning
 func (a *Agent) ProvisionTest(
 func (a *Agent) ProvisionTest(
 	projectID uint,
 	projectID uint,
+	operation provisioner.ProvisionerOperation,
 ) (*batchv1.Job, error) {
 ) (*batchv1.Job, error) {
 	prov := &provisioner.Conf{
 	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)
 	return a.provision(prov)

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

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"fmt"
 	"regexp"
 	"regexp"
 
 
+	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 
 
 	redis "github.com/go-redis/redis/v8"
 	redis "github.com/go-redis/redis/v8"
@@ -141,7 +142,29 @@ func GlobalStreamListener(
 						json.Unmarshal([]byte(dataString), reg)
 						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 {
 					if err != nil {
 						continue
 						continue
@@ -191,6 +214,20 @@ func GlobalStreamListener(
 
 
 				infra, err = repo.AWSInfra.UpdateAWSInfra(infra)
 				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 {
 				if err != nil {
 					continue
 					continue
 				}
 				}

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

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

+ 9 - 4
internal/models/infra.go

@@ -13,9 +13,11 @@ type InfraStatus string
 
 
 // The allowed statuses
 // The allowed statuses
 const (
 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
 // AWSInfraKind is the kind that aws infra can be
@@ -35,6 +37,9 @@ type AWSInfra struct {
 	// The type of infra that was provisioned
 	// The type of infra that was provisioned
 	Kind AWSInfraKind `json:"kind"`
 	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
 	// The project that this infra belongs to
 	ProjectID uint `json:"project_id"`
 	ProjectID uint `json:"project_id"`
 
 
@@ -71,7 +76,7 @@ func (ai *AWSInfra) Externalize() *AWSInfraExternal {
 
 
 // GetID returns the unique id for this infra
 // GetID returns the unique id for this infra
 func (ai *AWSInfra) GetID() string {
 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)
 // ParseWorkspaceID returns the (kind, projectID, infraID)

Разница между файлами не показана из-за своего большого размера
+ 6 - 0
prov.yaml


+ 157 - 6
server/api/provision_handler.go

@@ -2,7 +2,6 @@ package api
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
 
 
@@ -11,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
+	"github.com/porter-dev/porter/internal/models"
 
 
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/adapter"
 )
 )
@@ -33,7 +33,7 @@ func (app *App) HandleProvisionTest(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	_, err = agent.ProvisionTest(uint(projID))
+	_, err = agent.ProvisionTest(uint(projID), provisioner.Apply)
 
 
 	if err != nil {
 	if err != nil {
 		app.handleErrorInternal(err, w)
 		app.handleErrorInternal(err, w)
@@ -104,6 +104,7 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 		awsInt,
 		awsInt,
 		form.ECRName,
 		form.ECRName,
 		infra,
 		infra,
+		provisioner.Apply,
 	)
 	)
 
 
 	if err != nil {
 	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
 // HandleProvisionAWSEKSInfra provisions a new aws EKS instance for a project
 func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	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,
 		awsInt,
 		form.EKSName,
 		form.EKSName,
 		infra,
 		infra,
+		provisioner.Apply,
 	)
 	)
 
 
 	if err != nil {
 	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
 // HandleGetProvisioningLogs returns real-time logs of the provisioning process via websockets
 func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
 	// 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 }
 	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
 	// convert the form to a registry
-	registry, err := form.ToRegistry()
+	registry, err := form.ToRegistry(*app.Repo)
 
 
 	if err != nil {
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 		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
 	// handle write to the database
 	registry, err = app.Repo.Registry.CreateRegistry(registry)
 	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"`
 	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
 // DoesUserIDMatch checks the id URL parameter and verifies that it matches
 // the one stored in the session
 // the one stored in the session
 func (auth *Auth) DoesUserIDMatch(next http.Handler, loc IDLocation) http.Handler {
 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
 // Helpers
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 	session, _ := auth.store.Get(r, auth.cookieName)
 	session, _ := auth.store.Get(r, auth.cookieName)
@@ -565,3 +618,48 @@ func findGitRepoIDInRequest(r *http.Request, gitRepoLoc IDLocation) (uint64, err
 
 
 	return grID, nil
 	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",
 			"GET",
 			"/projects/{project_id}/provision/{kind}/{infra_id}/logs",
 			"/projects/{project_id}/provision/{kind}/{infra_id}/logs",
 			auth.DoesUserHaveProjectAccess(
 			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.URLParam,
 				mw.ReadAccess,
 				mw.ReadAccess,
 			),
 			),

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