Procházet zdrojové kódy

Merge branch 'master' of https://github.com/porter-dev/porter into beta.2.integration-backend

mergin
Alexander Belanger před 5 roky
rodič
revize
28d1c52be0

+ 2 - 1
.gitignore

@@ -5,4 +5,5 @@ app
 *.db
 test.yaml
 dist
-gon*.hcl
+gon*.hcl
+*prod.Dockerfile

+ 38 - 13
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -14,7 +14,10 @@ import TagList from './TagList';
 type PropsType = {
   forceExpanded?: boolean,
   selectedImageUrl: string | null,
-  setSelectedImageUrl: (x: string) => void
+  selectedTag: string | null,
+  setSelectedImageUrl: (x: string) => void,
+  setSelectedTag: (x: string) => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -43,7 +46,8 @@ export default class ImageSelector extends Component<PropsType, StateType> {
       if (err) {
         console.log(err);
       } else {
-        res.data.forEach(async (registry: any, i: number) => {
+        let registries = res.data;
+        registries.forEach(async (registry: any, i: number) => {
           await new Promise((nextController: (res?: any) => void) => {           
             api.getImageRepos('<token>', {}, 
               { 
@@ -51,7 +55,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
                 registry_id: registry.id,
               }, (err: any, res: any) => {
               if (err && this.state.loading) {
-                this.setState({ error: true });
+                this.setState({ error: true, loading: false });
               } else {
                 let newImg = res.data.map((img: any) => {
                   return {
@@ -59,14 +63,16 @@ export default class ImageSelector extends Component<PropsType, StateType> {
                     source: img.name
                   }
                 })
-                this.setState({
-                  images: [...images, ...newImg],
-                  registryId: registry.id,
-                  loading: false,
-                  error: false,
-                }, () => {
-                  nextController()
-                })
+                images.push(...newImg)
+                if (i == registries.length - 1) {
+                  this.setState({
+                    images,
+                    registryId: registry.id,
+                    loading: false,
+                    error: false,
+                  });
+                }
+                nextController()
               }
             });    
           })
@@ -81,6 +87,15 @@ export default class ImageSelector extends Component<PropsType, StateType> {
       return <LoadingWrapper><Loading /></LoadingWrapper>
     } else if (error || !images) {
       return <LoadingWrapper>Error loading repos</LoadingWrapper>
+    } else if (images.length === 0) {
+      return (
+        <LoadingWrapper>
+          No registries found. 
+          <Highlight onClick={() => this.props.setCurrentView('integrations')}>
+            Link your registry.
+          </Highlight>
+        </LoadingWrapper>
+      );
     }
 
     return images.map((image: ImageType, i: number) => {
@@ -123,7 +138,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   }
 
   renderExpanded = () => {
-    let { selectedImageUrl, setSelectedImageUrl } = this.props;
+    let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
     if (!this.state.clickedImage) {
       return (
         <div>
@@ -138,8 +153,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
         <div>
           <ExpandedWrapper>
             <TagList
+              selectedTag={selectedTag}
               selectedImageUrl={selectedImageUrl}
-              setSelectedImageUrl={setSelectedImageUrl}
+              setSelectedTag={setSelectedTag}
               registryId={this.state.registryId}
             />
           </ExpandedWrapper>
@@ -204,6 +220,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
 
 ImageSelector.contextType = Context;
 
+const Highlight = styled.div`
+  text-decoration: underline;
+  margin-left: 10px;
+  color: #949eff;
+  cursor: pointer;
+  padding: 3px 0;
+`;
+
 const BackButton = styled.div`
   display: flex;
   align-items: center;
@@ -233,6 +257,7 @@ const Input = styled.input`
   outline: 0;
   background: none;
   border: 0;
+  font-size: 13px;
   width: calc(100% - 60px);
   color: white;
 `;

+ 5 - 13
dashboard/src/components/image-selector/TagList.tsx

@@ -9,7 +9,8 @@ import { Context } from '../../shared/Context';
 import Loading from '../Loading';
 
 type PropsType = {
-  setSelectedImageUrl: (x: string) => void,
+  setSelectedTag: (x: string) => void,
+  selectedTag: string,
   selectedImageUrl: string,
   registryId: number,
 };
@@ -38,6 +39,7 @@ export default class TagList extends Component<PropsType, StateType> {
         repo_name: this.props.selectedImageUrl,
       }, (err: any, res: any) => {
       if (err) {
+        console.log(err)
         this.setState({ loading: false, error: true });
       } else {
         let tags = res.data.map((tag: any, i: number) => {
@@ -49,18 +51,8 @@ export default class TagList extends Component<PropsType, StateType> {
   }
 
   setTag = (tag: string) => {
-    let { selectedImageUrl, setSelectedImageUrl} = this.props;
-    let splits = selectedImageUrl.split(':');
-    if (splits[splits.length - 1] === this.state.currentTag) {
-      selectedImageUrl = splits.reduce((acc: string, curr: string) => {
-        if (curr !== this.state.currentTag) {
-          return acc + ':' + curr;
-        } else {
-          return acc;
-        }
-      });
-    }
-    setSelectedImageUrl(selectedImageUrl + ':' + tag);
+    let { selectedTag, setSelectedTag } = this.props;
+    setSelectedTag(tag);
     this.setState({ currentTag: tag });
   }
 

+ 5 - 1
dashboard/src/main/home/Home.tsx

@@ -97,7 +97,11 @@ export default class Home extends Component<PropsType, StateType> {
       return <Integrations />;
     }
 
-    return <Templates />;
+    return (
+      <Templates 
+        setCurrentView={(x: string) => this.setState({ currentView: x })} 
+      />
+    );
   }
 
   render() {

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -75,7 +75,7 @@ export default class ClusterDashboard extends Component<PropsType, StateType> {
   }
 
   renderContents = () => {
-    let { currentCluster, setSidebar } = this.props;
+    let { currentCluster, setSidebar, setCurrentView } = this.props;
 
     if (this.state.currentChart) {
       return (
@@ -84,6 +84,7 @@ export default class ClusterDashboard extends Component<PropsType, StateType> {
           refreshChart={this.refreshChart}
           setCurrentChart={(x: ChartType | null) => this.setState({ currentChart: x })}
           setSidebar={setSidebar}
+          setCurrentView={setCurrentView} // Link to integrations from chart settings
         />
       );
     }

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -63,7 +63,8 @@ export default class ChartList extends Component<PropsType, StateType> {
 
   setupWebsocket = (kind: string) => {
       let { currentCluster, currentProject } = this.context;
-      let ws = new WebSocket(`ws://localhost:8080/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
+      let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
       ws.onopen = () => {
         console.log('connected to websocket')
       }

+ 9 - 6
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -21,7 +21,8 @@ type PropsType = {
   currentChart: ChartType,
   setCurrentChart: (x: ChartType | null) => void,
   refreshChart: () => void,
-  setSidebar: (x: boolean) => void
+  setSidebar: (x: boolean) => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -147,18 +148,18 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     tabOptions.push(
       { label: 'Status', value: 'status' },
       //{ label: 'Deploy', value: 'deploy' },
+      { label: 'Chart Overview', value: 'graph' },
       { label: 'Settings', value: 'settings' },
     );
 
     if (this.state.devOpsMode) {
       tabOptions.push(
-        { label: 'Chart Overview', value: 'graph' },
         { label: 'Manifests', value: 'list' },
         { label: 'Raw Values', value: 'values' }
       );
     }
 
-    let { currentChart, refreshChart, setSidebar } = this.props;
+    let { currentChart, refreshChart, setSidebar, setCurrentView } = this.props;
     let chart = this.state.revisionPreview || currentChart;
     tabContents.push(
       {
@@ -173,7 +174,11 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       },
       {
         value: 'settings', component: (
-          <SettingsSection /> 
+          <SettingsSection
+            currentChart={chart}
+            refreshChart={refreshChart}
+            setCurrentView={setCurrentView}
+          /> 
         ),
       },
       {
@@ -253,14 +258,12 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       let { tabOptions } = this.state;
       tabOptions.pop();
       tabOptions.pop();
-      tabOptions.pop();
       this.setState({ devOpsMode: false, checkTabExists: true, tabOptions }, () => {
         localStorage.setItem('devOpsMode', 'false')
       });
     } else {
       let { tabOptions } = this.state;
       tabOptions.push(
-        { label: 'Chart Overview', value: 'graph' },
         { label: 'Manifests', value: 'list' },
         { label: 'Raw Values', value: 'values' }
       );

+ 49 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -1,21 +1,62 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
+import api from '../../../../shared/api';
+import yaml from 'js-yaml';
 
-import { RepoType } from '../../../../shared/types';
+import { ChartType, RepoType, StorageType } from '../../../../shared/types';
+import { Context } from '../../../../shared/Context';
 
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import SaveButton from '../../../../components/SaveButton';
 
 type PropsType = {
+  currentChart: ChartType,
+  refreshChart: () => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
   selectedImageUrl: string | null,
+  selectedTag: string | null,
+  saveValuesStatus: string | null,
+  values: string,
 };
 
 export default class SettingsSection extends Component<PropsType, StateType> {
   state = {
     selectedImageUrl: '',
+    selectedTag: '',
+    values: '',
+    saveValuesStatus: null as (string | null),
+  }
+
+  redeployWithNewImage = (img: string, tag: string) => {
+    let { currentCluster, currentProject } = this.context;
+    let image = {
+      image: {
+        repository: img,
+        tag: tag,
+      }
+    }
+
+    let values = yaml.dump(image);
+    api.upgradeChartValues('<token>', {
+      namespace: this.props.currentChart.namespace,
+      storage: StorageType.Secret,
+      values,
+    }, {
+      id: currentProject.id, 
+      name: this.props.currentChart.name,
+      cluster_id: currentCluster.id,
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+        this.setState({ saveValuesStatus: 'error' });
+      } else {
+        this.setState({ saveValuesStatus: 'successful' });
+        this.props.refreshChart();
+      }
+    });
   }
 
   render() {
@@ -25,14 +66,17 @@ export default class SettingsSection extends Component<PropsType, StateType> {
           <Subtitle>Connected source</Subtitle>
           <ImageSelector
             selectedImageUrl={this.state.selectedImageUrl}
+            selectedTag={this.state.selectedTag}
             setSelectedImageUrl={(x: string) => this.setState({ selectedImageUrl: x })}
+            setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
             forceExpanded={true}
+            setCurrentView={this.props.setCurrentView}
           />
         </StyledSettingsSection>
         <SaveButton
           text='Save Settings'
-          onClick={() => console.log(this.state)}
-          status={null}
+          onClick={() => this.redeployWithNewImage(this.state.selectedImageUrl, this.state.selectedTag)}
+          status={this.state.saveValuesStatus}
           makeFlush={true}
         />
       </Wrapper>
@@ -40,6 +84,8 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   }
 }
 
+SettingsSection.contextType = Context;
+
 const Subtitle = styled.div`
   color: #aaaabb;
   font-size: 13px;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -41,8 +41,8 @@ export default class Logs extends Component<PropsType, StateType> {
     let { currentCluster, currentProject } = this.context;
     let { selectedPod } = this.props;
     if (!selectedPod.metadata?.name) return
-
-    let ws = new WebSocket(`ws://localhost:8080/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
+    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+    let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
 
     this.setState({ ws }, () => {
       if (!this.state.ws) return;

+ 10 - 1
dashboard/src/main/home/integrations/integration-form/DockerHubForm.tsx

@@ -26,6 +26,14 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
     dockerPassword: ''
   }
 
+  isDisabled = (): boolean => {
+    let { registryURL, dockerEmail, dockerUsername, dockerPassword } = this.state;
+    if (registryURL === '' || dockerEmail === '' || dockerUsername === '' || dockerPassword === '') {
+      return true;
+    }
+    return false;
+  }
+
   handleSubmit = () => {
     // TODO: implement once api is restructured
   }
@@ -70,7 +78,8 @@ export default class DockerHubForm extends Component<PropsType, StateType> {
         <SaveButton
           text='Save Settings'
           makeFlush={true}
-          onClick={this.handleSubmit}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
         />
       </StyledForm>
     );

+ 11 - 1
dashboard/src/main/home/integrations/integration-form/EKSForm.tsx

@@ -31,6 +31,15 @@ export default class EKSForm extends Component<PropsType, StateType> {
     awsSecretKey: '',
   }
 
+  isDisabled = (): boolean => {
+    let { clusterName, clusterEndpoint, clusterCA, awsAccessId, awsSecretKey } = this.state;
+    if (clusterName === '' || clusterEndpoint === '' || clusterCA === '' 
+      || awsAccessId === '' || awsSecretKey === '') {
+      return true;
+    }
+    return false;
+  }
+
   handleSubmit = () => {
     // TODO: implement once api is restructured
   }
@@ -87,7 +96,8 @@ export default class EKSForm extends Component<PropsType, StateType> {
         <SaveButton
           text='Save Settings'
           makeFlush={true}
-          onClick={this.handleSubmit}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
         />
       </StyledForm>
     );

+ 10 - 1
dashboard/src/main/home/integrations/integration-form/GCRForm.tsx

@@ -25,6 +25,14 @@ export default class GCRForm extends Component<PropsType, StateType> {
     serviceAccountKey: '',
   }
 
+  isDisabled = (): boolean => {
+    let { credentialsName, serviceAccountKey } = this.state;
+    if (credentialsName === '' || serviceAccountKey === '') {
+      return true;
+    }
+    return false;
+  }
+  
   handleSubmit = () => {
     // TODO: implement once api is restructured
   }
@@ -56,7 +64,8 @@ export default class GCRForm extends Component<PropsType, StateType> {
         <SaveButton
           text='Save Settings'
           makeFlush={true}
-          onClick={this.handleSubmit}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
         />
       </StyledForm>
     );

+ 10 - 1
dashboard/src/main/home/integrations/integration-form/GKEForm.tsx

@@ -29,6 +29,14 @@ export default class GKEForm extends Component<PropsType, StateType> {
     serviceAccountKey: ''
   }
 
+  isDisabled = (): boolean => {
+    let { clusterName, clusterEndpoint, clusterCA, serviceAccountKey } = this.state;
+    if (clusterName === '' || clusterEndpoint === '' || clusterCA === '' || serviceAccountKey === '') {
+      return true;
+    }
+    return false;
+  }
+
   handleSubmit = () => {
     // TODO: implement once api is restructured
   }
@@ -76,7 +84,8 @@ export default class GKEForm extends Component<PropsType, StateType> {
         <SaveButton
           text='Save Settings'
           makeFlush={true}
-          onClick={this.handleSubmit}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
         />
       </StyledForm>
     );

+ 1 - 1
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -45,12 +45,12 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
             </Code>
             3. Log in to the Porter CLI:
             <Code>
+              porter config set-host {location.protocol + '//' + location.host}<br/>
               porter auth login
             </Code>
             4. Configure the Porter CLI and link your current context:
             <Code>
               porter config set-project {this.context.currentProject.id}<br/>
-              porter config set-host {location.protocol + '//' + location.host}<br/>
               porter connect kubeconfig
             </Code>
           </Placeholder>

+ 2 - 0
dashboard/src/main/home/templates/Templates.tsx

@@ -14,6 +14,7 @@ const tabOptions = [
 ];
 
 type PropsType = {
+  setCurrentView: (x: string) => void, // Link to add integration from source selector
 };
 
 type StateType = {
@@ -96,6 +97,7 @@ export default class Templates extends Component<PropsType, StateType> {
         <ExpandedTemplate
           currentTemplate={this.state.currentTemplate}
           setCurrentTemplate={(currentTemplate: PorterChart) => this.setState({ currentTemplate })}
+          setCurrentView={this.props.setCurrentView}
         />
       );
     }

+ 3 - 1
dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx

@@ -8,7 +8,8 @@ import LaunchTemplate from './LaunchTemplate';
 
 type PropsType = {
   currentTemplate: PorterChart,
-  setCurrentTemplate: (x: PorterChart) => void
+  setCurrentTemplate: (x: PorterChart) => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -26,6 +27,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
         <LaunchTemplate
           currentTemplate={this.props.currentTemplate}
           hideLaunch={() => this.setState({ showLaunchTemplate: false })}
+          setCurrentView={this.props.setCurrentView}
         />
       );
     }

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

@@ -13,6 +13,7 @@ import ValuesForm from '../../../../components/values-form/ValuesForm';
 type PropsType = {
   currentTemplate: PorterChart,
   hideLaunch: () => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -20,6 +21,7 @@ type StateType = {
   clusterOptions: { label: string, value: string }[],
   selectedCluster: string,
   selectedImageUrl: string | null,
+  selectedTag: string | null,
   tabOptions: ChoiceType[],
   tabContents: any
 };
@@ -30,6 +32,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     clusterOptions: [] as { label: string, value: string }[],
     selectedCluster: this.context.currentCluster.name,
     selectedImageUrl: '' as string | null,
+    selectedTag: '' as string | null,
     tabOptions: [] as ChoiceType[],
     tabContents: [] as any,
   };
@@ -144,9 +147,12 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         <Subtitle>Select the container image you would like to connect to this template.</Subtitle>
         <Br />
         <ImageSelector
+          selectedTag={this.state.selectedTag}
           selectedImageUrl={this.state.selectedImageUrl}
           setSelectedImageUrl={(x: string) => this.setState({ selectedImageUrl: x })}
+          setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
           forceExpanded={true}
+          setCurrentView={this.props.setCurrentView}
         />
 
         <br />

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

@@ -14,7 +14,7 @@ import { StorageType } from './types';
 const checkAuth = baseApi('GET', '/api/auth/check');
 
 const registerUser = baseApi<{ 
-  email: string, 
+  email: string,
   password: string
 }>('POST', '/api/users');
 
@@ -105,7 +105,7 @@ const rollbackChart = baseApi<{
   id: number,
   name: string,
   cluster_id: number,
-  }>('POST', pathParams => {
+}>('POST', pathParams => {
   let { id, name, cluster_id } = pathParams;
   return `/api/projects/${id}/releases/${name}/rollback?cluster_id=${cluster_id}`;
 });
@@ -118,7 +118,7 @@ const upgradeChartValues = baseApi<{
   id: number,
   name: string,
   cluster_id: number,
-  }>('POST', pathParams => {
+}>('POST', pathParams => {
   let { id, name, cluster_id } = pathParams;
   return `/api/projects/${id}/releases/${name}/upgrade?cluster_id=${cluster_id}`;
 });
@@ -199,7 +199,7 @@ const createECR = baseApi<{
 const getImageRepos = baseApi<{}, {   
   project_id: number,
   registry_id: number,
- }>('GET', pathParams => {
+}>('GET', pathParams => {
   return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories`;
 });
 

+ 1 - 2
internal/config/config.go

@@ -25,8 +25,7 @@ type ServerConf struct {
 	TimeoutRead    time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutWrite   time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
 	TimeoutIdle    time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
-
-	IsLocal bool `env:"IS_LOCAL,default=false"`
+	IsLocal        bool          `env:"IS_LOCAL,default=false"`
 
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`