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

Merge pull request #196 from porter-dev/master

master into staging
abelanger5 5 лет назад
Родитель
Сommit
421d1475ae

+ 28 - 18
.github/workflows/release.yaml

@@ -37,10 +37,10 @@ jobs:
         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
+        DOCKER_BUILDKIT=1 docker build . -t porter1/porter:${{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}}
+        docker push porter1/porter:${{steps.tag_name.outputs.tag}}
   build:
     name: Build binaries
     runs-on: ubuntu-latest
@@ -58,6 +58,31 @@ jobs:
         uses: actions/setup-go@v2
         with:
           go-version: 1.15
+      - 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
+      - name: Build and zip static folder
+        run: |
+          mkdir -p ./release/static
+          cd dashboard
+          npm i --production=false
+          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: 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 &
@@ -108,21 +133,6 @@ jobs:
           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:
@@ -364,4 +374,4 @@ jobs:
           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
+          asset_content_type: application/zip

+ 1 - 1
README.md

@@ -69,7 +69,7 @@ For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/do
 
 2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in.
 
-3. Put in your credentials, then Porter will automatically provision a cluster and an image registry in your own cloud account.
+3. [Put in your credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws), then Porter will automatically provision a cluster and an image registry in your own cloud account.
 
 4. [Build and push your Docker image to the provisioned registry with the CLI](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
 

+ 8 - 1
cli/cmd/docker.go

@@ -7,6 +7,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/github"
@@ -56,8 +57,14 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 
 	for _, registry := range registries {
 		if registry.URL != "" {
+			rURL := registry.URL
+
+			if !strings.Contains(rURL, "http") {
+				rURL = "http://" + rURL
+			}
+
 			// strip the protocol
-			regURL, err := url.Parse(registry.URL)
+			regURL, err := url.Parse(rURL)
 
 			if err != nil {
 				continue

+ 4 - 0
cmd/docker-credential-porter/helper/helper.go

@@ -44,6 +44,8 @@ var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fi
 // Get retrieves credentials from the store.
 // It returns username and secret as strings.
 func (p *PorterHelper) Get(serverURL string) (user string, secret string, err error) {
+	p.init()
+
 	if strings.Contains(serverURL, "gcr.io") {
 		return p.getGCR(serverURL)
 	}
@@ -154,6 +156,8 @@ func (p *PorterHelper) getECR(serverURL string) (user string, secret string, err
 
 // List returns the stored serverURLs and their associated usernames.
 func (p *PorterHelper) List() (map[string]string, error) {
+	p.init()
+
 	credCache := BuildCredentialsCache("")
 	entries := credCache.List()
 

+ 1 - 6
dashboard/package-lock.json

@@ -981,11 +981,6 @@
       "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
       "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": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
@@ -1158,7 +1153,7 @@
       "dev": true
     },
     "axios": {
-      "version": "0.21.1",
+      "version": "0.20.0",
       "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
       "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
       "requires": {

+ 3 - 0
dashboard/src/main/Register.tsx

@@ -5,6 +5,7 @@ import logo from '../assets/logo.png';
 import api from '../shared/api';
 import { emailRegex } from '../shared/regex';
 import { Context } from '../shared/Context';
+import { handleSubmitFeedback } from '../shared/feedback'; 
 
 type PropsType = {
   authenticate: () => void
@@ -60,6 +61,8 @@ export default class Register extends Component<PropsType, StateType> {
         email: email,
         password: password
       }, {}, (err: any, res: any) => {
+        let msg = '📡 ' + email + ' registered for Porter.';
+        handleSubmitFeedback(msg);
         setUser(res?.data?.id, res?.data?.email)
         err ? setCurrentError(err.response.data.errors[0]) : authenticate();
       });

+ 3 - 0
dashboard/src/main/home/Home.tsx

@@ -5,6 +5,7 @@ import ReactModal from 'react-modal';
 import { Context } from '../../shared/Context';
 import api from '../../shared/api';
 import { InfraType } from '../../shared/types';
+import { handleSubmitFeedback } from '../../shared/feedback';
 
 import Sidebar from './sidebar/Sidebar';
 import Dashboard from './dashboard/Dashboard';
@@ -93,6 +94,8 @@ export default class Home extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
+    let msg = '👋 ' + this.context.user.email + ' logged in.';
+    handleSubmitFeedback(msg);
     this.getProjects();
   }
 

+ 41 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -430,6 +430,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
   componentDidMount() {
     let { currentCluster, currentProject } = this.context;
+    let { currentChart } = this.props;
 
     this.getChartData(this.props.currentChart);
     this.getControllers(this.props.currentChart)
@@ -438,6 +439,23 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       this.props.currentChart 
     );
 
+    api.getChartComponents('<token>', {
+      namespace: currentChart.namespace,
+      cluster_id: currentCluster.id,
+      storage: StorageType.Secret
+    }, {
+      id: currentProject.id,
+      name: currentChart.name,
+      revision: currentChart.version
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        this.setState({ components: res.data.Objects });
+        console.log(res.data.Objects)
+      }
+    });
+
     api.getIngress('<token>', { 
       cluster_id: currentCluster.id,
     }, {
@@ -449,9 +467,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         console.log(err);
         return
       }
-      if (res.data) {
-        this.setState({url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}` })
-      }
+      console.log(res.data)
+      this.setState({url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}` })
     })
   }
 
@@ -472,7 +489,19 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
   renderUrl = () => {
     if (this.state.url) {
-      return <Url href={this.state.url} target='_blank'>{this.state.url}</Url>;
+      return <Url href={this.state.url} target='_blank'> <i className="material-icons">link</i> {this.state.url}</Url>;
+    } else {
+      let serviceName = null as string
+      let serviceNamespace = null as string
+
+      this.state.components.forEach((c: any) => {
+        if (c.Kind == "Service") {
+          serviceName = c.Name
+          serviceNamespace = c.Namespace
+        }
+      })
+
+      return <Url><i className="material-icons">link</i>{`${serviceName}.${serviceNamespace}.namespace.svc.cluster.local`}</Url>
     }
   }
 
@@ -551,6 +580,14 @@ const Url = styled.a`
   font-size: 13px;
   margin-top: 15px;
   margin-bottom: -5px;
+  user-select: text;
+  display: flex;
+  align-items: center;
+
+  > i {
+    font-size: 15px;
+    margin-right: 10px;
+  }
 `;
 
 const TabButton = styled.div`

+ 16 - 1
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -104,6 +104,12 @@ export default class UpdateClusterModal extends Component<PropsType, StateType>
           onYes={this.handleDelete}
           onNo={() => this.setState({ showDeleteOverlay: false })}
         />
+
+        <Warning>
+         ⚠️ Deletion via Porter may result in dangling resources.  
+         Please visit the AWS console to ensure that all resources have been removed.
+        </Warning>
+
       </StyledUpdateProjectModal>
       );
   }
@@ -196,4 +202,13 @@ const StyledUpdateProjectModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   background: #202227;
-`;
+`;
+
+const Warning = styled.div`
+  width: 65%;
+  margin-top: 3px;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  text-align: justify;
+`

+ 14 - 1
dashboard/src/main/home/modals/UpdateProjectModal.tsx

@@ -128,6 +128,10 @@ export default class UpdateProjectModal extends Component<PropsType, StateType>
           onYes={this.handleDelete}
           onNo={() => this.setState({ showDeleteOverlay: false })}
         />
+        <Warning>
+         ⚠️ Deletion via Porter may result in dangling resources.  
+         Please visit the AWS console to ensure that all resources have been removed.
+        </Warning>
       </StyledUpdateProjectModal>
       );
   }
@@ -228,4 +232,13 @@ const StyledUpdateProjectModal= styled.div`
   overflow: hidden;
   border-radius: 6px;
   background: #202227;
-`;
+`;
+
+const Warning = styled.div`
+  width: 65%;
+  margin-top: 3px;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  text-align: justify;
+`

+ 3 - 3
dashboard/src/main/home/navbar/Feedback.tsx

@@ -34,12 +34,12 @@ export default class Feedback extends Component<PropsType, StateType> {
     }
   }
 
-  handleSubmitFeedback = () => {
+  onSubmitFeedback = () => {
     let { user } = this.context;
     let msg = '👤 ' + user.email + ' 📍 ' + this.props.currentView + ': ' + this.state.feedbackText;
     handleSubmitFeedback(msg, () => {
       this.setState({ feedbackSent: true, feedbackText: '' });
-    })
+    });
   }
 
   renderFeedbackDropdown = () => {
@@ -61,7 +61,7 @@ export default class Feedback extends Component<PropsType, StateType> {
             />
             <SendButton 
               disabled={disabled} 
-              onClick={() => !disabled && this.handleSubmitFeedback()}
+              onClick={() => !disabled && this.onSubmitFeedback()}
             >
               <i className="material-icons">send</i> Send
             </SendButton>

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

@@ -6,6 +6,7 @@ import close from '../../../assets/close.png';
 import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
 import { integrationList } from '../../../shared/common';
+import { handleSubmitFeedback } from '../../../shared/feedback';
 import { ProjectType } from '../../../shared/types';
 
 import InputRow from '../../../components/values-form/InputRow';
@@ -48,13 +49,19 @@ export default class NewProject extends Component<PropsType, StateType> {
     return true;
   }
 
+  handleSelectProvider = (provider: string) => {
+    let msg = '🤔 ' + this.context.user.email + ' selected ' + provider + '.';
+    handleSubmitFeedback(msg);
+    this.setState({ selectedProvider: provider });
+  }
+
   renderTemplateList = () => {
     return providers.map((provider: string, i: number) => {
       let providerInfo = integrationList[provider];
       return (
         <Block 
           key={i} 
-          onClick={() => this.setState({ selectedProvider: provider })}
+          onClick={() => this.handleSelectProvider(provider)}
         >
           <Icon src={providerInfo.icon} />
           <BlockTitle>
@@ -311,6 +318,9 @@ export default class NewProject extends Component<PropsType, StateType> {
           let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
           this.context.setCurrentProject(proj);
           
+          let msg = '🏗️ ' + this.context.user.email + ' began provisioning.';
+          handleSubmitFeedback(msg);
+
           if (this.state.selectedProvider === 'aws') {
             this.provisionECR(proj, this.provisionEKS)
 

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

@@ -4,7 +4,7 @@ import styled from 'styled-components';
 import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
 import ansiparse from '../../../shared/ansiparser'
-import { integrationList } from '../../../shared/common';
+import { handleSubmitFeedback } from '../../../shared/feedback';
 import loading from '../../../assets/loading.gif';
 import warning from '../../../assets/warning.png';
 
@@ -190,6 +190,8 @@ export default class Provisioner extends Component<PropsType, StateType> {
   }
 
   onEnd = () => {
+    let msg = '🛠️ ' + this.context.user.email + ' completed provisioning.';
+    handleSubmitFeedback(msg);
     let myInterval = setInterval(() => {
       console.log('interval')
       api.getClusters('<token>', {}, { id: this.context.currentProject.id }, (err: any, res: any) => {

+ 60 - 0
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -9,6 +9,7 @@ import { PorterTemplate, ChoiceType, ClusterType, StorageType } from '../../../.
 import Selector from '../../../../components/Selector';
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
+import SaveButton from '../../../../components/SaveButton';
 import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 import { safeDump } from 'js-yaml';
@@ -50,6 +51,28 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     namespaceOptions: [] as { label: string, value: string }[],
   };
 
+  onSubmitAddon = () => {
+    let { currentCluster, currentProject } = this.context;
+    let name = randomWords({ exactly: 3, join: '-' });
+    api.deployTemplate('<token>', {
+      templateName: this.props.currentTemplate.name,
+      storage: StorageType.Secret,
+      namespace: this.state.selectedNamespace,
+      name,
+    }, {
+      id: currentProject.id,
+      cluster_id: currentCluster.id,
+      name: this.props.currentTemplate.name.toLowerCase().trim(),
+      version: 'latest',
+    }, (err: any, res: any) => {
+      if (err) {
+        this.setState({ saveValuesStatus: 'error' });
+      } else {
+        this.setState({ saveValuesStatus: 'successful' });
+      }
+    });
+  }
+
   onSubmit = (rawValues: any) => {
     let { currentCluster, currentProject } = this.context;
     let name = randomWords({ exactly: 3, join: '-' });
@@ -125,6 +148,10 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
+    if (this.props.currentTemplate.name !== 'docker') {
+      this.setState({ saveValuesStatus: '' });
+    }
+
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
     this.props.form?.tabs.map((tab: any, i: number) => {
@@ -193,6 +220,20 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           {this.renderTabContents()}
         </TabRegion>
       );
+    } else {
+      return (
+        <Wrapper>
+          <Placeholder>
+            No additional settings found.
+          </Placeholder>
+          <SaveButton
+            text='Deploy'
+            onClick={() => this.onSubmitAddon()}
+            status={this.state.saveValuesStatus}
+            makeFlush={true}
+          />
+        </Wrapper>
+      );
     }
   }
 
@@ -262,6 +303,25 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 LaunchTemplate.contextType = Context;
 
+const Wrapper = styled.div`
+  width: 100%;
+  position: relative;
+  padding-bottom: 70px;
+`;
+
+const Placeholder = styled.div`
+  width: 100%;
+  height: 200px;
+  background: #ffffff11;
+  border: 1px solid #ffffff44;
+  border-radius: 5px;
+  color: #ffffff44;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
 const DarkMatter = styled.div`
   width: 100%;
   margin-top: -15px;

+ 2 - 2
dashboard/src/shared/api.tsx

@@ -175,8 +175,8 @@ const deleteProject = baseApi<{}, { id: number }>('DELETE', pathParams => {
 
 const deployTemplate = baseApi<{
   templateName: string,
-  imageURL: string,
-  formValues: any,
+  imageURL?: string,
+  formValues?: any,
   storage: StorageType,
   namespace: string,
   name: string,

+ 17 - 15
dashboard/src/shared/feedback.tsx

@@ -1,19 +1,21 @@
 import axios from 'axios';
 
 export const handleSubmitFeedback = (msg: string, callback?: (err: any, res: any) => void) => {
-  axios.post(process.env.FEEDBACK_ENDPOINT, {
-    key: process.env.DISCORD_KEY,
-    cid: process.env.DISCORD_CID,
-    message: msg,
-  }, {
-    headers: {
-      Authorization: `Bearer <>`
-    }
-  })
-  .then(res => {
-    callback && callback(null, res);
-  })
-  .catch(err => {
-    callback && callback(err, null);
-  });
+  if (!window.location.href.includes('localhost:8080')) {
+    axios.post(process.env.FEEDBACK_ENDPOINT, {
+      key: process.env.DISCORD_KEY,
+      cid: process.env.DISCORD_CID,
+      message: msg,
+    }, {
+      headers: {
+        Authorization: `Bearer <>`
+      }
+    })
+    .then(res => {
+      callback && callback(null, res);
+    })
+    .catch(err => {
+      callback && callback(err, null);
+    });
+  }
 }

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

@@ -61,7 +61,7 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 	env = conf.attachDefaultEnv(env)
 
 	ttl := int32(3600)
-	backoffLimit := int32(3)
+	backoffLimit := int32(5)
 
 	labels := map[string]string{
 		"app": "provisioner",

+ 2 - 1
server/api/registry_handler.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/porter-dev/porter/internal/registry"
@@ -198,7 +199,7 @@ func (app *App) HandleGetProjectRegistryGCRToken(w http.ResponseWriter, r *http.
 	var expiresAt *time.Time
 
 	for _, reg := range regs {
-		if reg.GCPIntegrationID != 0 && reg.URL == reqBody.ServerURL {
+		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, reqBody.ServerURL) {
 			_reg := registry.Registry(*reg)
 
 			tokenCache, err := _reg.GetGCRToken(*app.Repo)