Przeglądaj źródła

Merge branch 'beta.3.deploy-agent'

sunguroku 5 lat temu
rodzic
commit
7fc189d043

+ 1 - 1
dashboard/docker/dev.Dockerfile

@@ -5,7 +5,7 @@ WORKDIR /webpack
 
 COPY package*.json ./
 
-RUN npm i
+RUN npm install
 
 ENV NODE_ENV=development
 

+ 14 - 3
dashboard/package-lock.json

@@ -401,6 +401,12 @@
         "jest-diff": "^24.3.0"
       }
     },
+    "@types/js-base64": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-3.0.0.tgz",
+      "integrity": "sha512-BnEyOcDE4H6bkg8m84xhdbkYoAoCg8sYERmAvE4Ff50U8jTfbmOinRdJpauBn1P9XsCCQgCLuSiyz3PM4WHYOA==",
+      "dev": true
+    },
     "@types/js-yaml": {
       "version": "3.12.5",
       "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz",
@@ -4183,6 +4189,11 @@
       "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz",
       "integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU="
     },
+    "js-base64": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.6.0.tgz",
+      "integrity": "sha512-wVdUBYQeY2gY73RIlPrysvpYx+2vheGo8Y1SNQv/BzHToWpAZzJU7Z6uheKMAe+GLSBig5/Ps2nxg/8tRB73xg=="
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6851,9 +6862,9 @@
       "dev": true
     },
     "typescript": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
-      "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
+      "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
       "dev": true
     },
     "union-value": {

+ 3 - 1
dashboard/package.json

@@ -10,6 +10,7 @@
     "ace-builds": "^1.4.12",
     "axios": "^0.20.0",
     "dotenv": "^8.2.0",
+    "js-base64": "^3.6.0",
     "js-yaml": "^3.14.0",
     "markdown-to-jsx": "^7.0.1",
     "posthog-node": "^1.0.6",
@@ -31,6 +32,7 @@
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
     "@types/jest": "^24.0.0",
+    "@types/js-base64": "^3.0.0",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
     "@types/react": "^16.9.49",
@@ -44,7 +46,7 @@
     "qs": "^6.9.4",
     "source-map-loader": "^1.1.0",
     "ts-loader": "^8.0.4",
-    "typescript": "^4.0.3",
+    "typescript": "^4.1.2",
     "webpack": "^4.44.2",
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0"

+ 10 - 4
dashboard/src/components/values-form/InputRow.tsx

@@ -5,7 +5,7 @@ type PropsType = {
   label?: string,
   type: string,
   value: string | number,
-  setValue: (x: string) => void,
+  setValue: (x: string | number) => void,
   unit?: string
   placeholder?: string
   width?: string
@@ -16,6 +16,14 @@ type StateType = {
 };
 
 export default class InputRow extends Component<PropsType, StateType> {
+  handleChange = (e: ChangeEvent<HTMLInputElement>) => {
+    if (this.props.type === 'number') {
+      this.props.setValue(parseInt(e.target.value));
+    } else {
+      this.props.setValue(e.target.value);
+    }
+  }
+
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
@@ -28,9 +36,7 @@ export default class InputRow extends Component<PropsType, StateType> {
             width={width}
             type={type}
             value={value || ''}
-            onChange={(e: ChangeEvent<HTMLInputElement>) =>
-              this.props.setValue(e.target.value)
-            }
+            onChange={this.handleChange}
           />
           <Unit>{unit}</Unit>
         </InputWrapper>

+ 26 - 38
dashboard/src/components/values-form/ValuesForm.tsx

@@ -11,7 +11,9 @@ import InputRow from './InputRow';
 import SelectRow from './SelectRow';
 
 type PropsType = {
-  sections?: Section[]
+  onSubmit: (formValues: any) => void,
+  sections?: Section[],
+  disabled?: boolean,
 };
 
 type StateType = any;
@@ -21,13 +23,13 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   updateFormState() {
     let formState: any = {};
     this.props.sections.forEach((section: Section, i: number) => {
-      section.Contents.forEach((item: FormElement, i: number) => {
+      section.contents.forEach((item: FormElement, i: number) => {
 
         // If no name is assigned use values.yaml variable as identifier
-        let key = item.Name || item.Variable;
+        let key = item.name || item.variable;
         
-        let def = item.Settings.Default;
-        switch (item.Type) {
+        let def = item.settings && item.settings.default;
+        switch (item.type) {
           case 'checkbox':
             formState[key] = def ? def : false;
             break;
@@ -35,10 +37,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             formState[key] = def ? def : '';
             break;
           case 'number-input':
-            formState[key] = def.toString() ? def.toString() : '';
+            formState[key] = def.toString() ? def : '';
             break;
           case 'select':
-            formState[key] = def ? def : item.Settings.Options[0].Value;
+            formState[key] = def ? def : item.settings.options[0].value;
           default:
         }
       });
@@ -57,38 +59,23 @@ export default class ValuesForm extends Component<PropsType, StateType> {
     }
   }
 
-  handleDeploy = () => {
-    console.log(this.state);
-    let { currentProject } = this.context;
-
-    api.deployTemplate('<token>', {}, {
-      id: currentProject.id,
-    }, (err: any, res: any) => {
-      if (err) {
-        // console.log(err)
-      } else {
-        // console.log(res.data)
-      }
-    });
-  }
-
   renderSection = (section: Section) => {
-    return section.Contents.map((item: FormElement, i: number) => {
+    return section.contents.map((item: FormElement, i: number) => {
 
       // If no name is assigned use values.yaml variable as identifier
-      let key = item.Name || item.Variable;
-      switch (item.Type) {
+      let key = item.name || item.variable;
+      switch (item.type) {
         case 'heading':
-          return <Heading key={i}>{item.Label}</Heading>
+          return <Heading key={i}>{item.label}</Heading>
         case 'subtitle':
-          return <Helper key={i}>{item.Label}</Helper>
+          return <Helper key={i}>{item.label}</Helper>
         case 'checkbox':
           return (
             <CheckboxRow
               key={i}
               checked={this.state[key]}
               toggle={() => this.setState({ [key]: !this.state[key] })}
-              label={item.Label}
+              label={item.label}
             />
           );
         case 'string-input':
@@ -98,8 +85,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               type='text'
               value={this.state[key]}
               setValue={(x: string) => this.setState({ [key]: x })}
-              label={item.Label}
-              unit={item.Settings ? item.Settings.Unit : null}
+              label={item.label}
+              unit={item.settings ? item.settings.unit : null}
             />
           );
         case 'number-input':
@@ -108,9 +95,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               key={i}
               type='number'
               value={this.state[key]}
-              setValue={(x: string) => this.setState({ [key]: x })}
-              label={item.Label}
-              unit={item.Settings ? item.Settings.Unit : null}
+              setValue={(x: number) => this.setState({ [key]: x })}
+              label={item.label}
+              unit={item.settings ? item.settings.unit : null}
             />
           );
         case 'select':
@@ -119,9 +106,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               key={i}
               value={this.state[key]}
               setActiveValue={(val) => this.setState({ [key]: val })}
-              options={item.Settings.Options}
+              options={item.settings.options}
               dropdownLabel=''
-              label={item.Label}
+              label={item.label}
             />
           );
         default:
@@ -134,8 +121,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
       return this.props.sections.map((section: Section, i: number) => {
 
         // Hide collapsible section if deciding field is false
-        if (section.ShowIf) {
-          if (!this.state[section.ShowIf]) {
+        if (section.show_if) {
+          if (!this.state[section.show_if]) {
             return null;
           }
         }
@@ -157,8 +144,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           {this.renderFormContents()}
         </StyledValuesForm>
         <SaveButton
+          disabled={this.props.disabled}
           text='Deploy'
-          onClick={this.handleDeploy}
+          onClick={() => this.props.onSubmit(this.state)}
           status={null}
           makeFlush={true}
         />

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

@@ -116,7 +116,7 @@ export default class ChartList extends Component<PropsType, StateType> {
       // don't retrieve controllers for chart that failed to even deploy.
       if (chart.info.status == 'failed') return;
 
-      await new Promise(next => {
+      await new Promise((next: (res?: any) => void) => {
         api.getChartControllers('<token>', {
           namespace: chart.namespace,
           cluster_id: currentCluster.id,
@@ -138,7 +138,7 @@ export default class ChartList extends Component<PropsType, StateType> {
           })
 
           res.data.forEach(async (c: any) => {
-            await new Promise(nextController => {
+            await new Promise((nextController: (res?: any) => void) => {
               this.setState({
                 chartLookupTable: {
                   ...this.state.chartLookupTable,

+ 31 - 11
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,5 +1,7 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
+import yaml from 'js-yaml';
+import { Base64 } from 'js-base64';
 import close from '../../../../assets/close.png';
 
 import { ResourceType, ChartType, StorageType, ChoiceType } from '../../../../shared/types';
@@ -82,21 +84,39 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     });
   }
 
+  getFormData = (): any => {
+    let { files } = this.props.currentChart.chart;
+    for (const file of files) { 
+      if (file.name === 'form.yaml') {
+        let formData = yaml.load(Base64.decode(file.data));
+        return formData;
+      }
+    };
+    return null;
+  }
+
   refreshTabs = () => {
-    // Generate settings tabs from the provided form
+    let formData = this.getFormData();
     let tabOptions = [] as ChoiceType[];
     let tabContents = [] as any;
-    dummyFormTabs.map((tab: any, i: number) => {
-      tabOptions.push({ value: '@' + tab.Name, label: tab.Label });
-      tabContents.push({
-        value: '@' + tab.Name,
-        component: (
-          <ValuesFormWrapper>
-            <ValuesForm sections={tab.Sections} />
-          </ValuesFormWrapper>
-        ),
+
+    // Generate form tabs if form.yaml exists
+    if (formData && formData.tabs) {
+      formData.tabs.map((tab: any, i: number) => {
+        tabOptions.push({ value: '@' + tab.name, label: tab.label });
+        tabContents.push({
+          value: '@' + tab.name,
+          component: (
+            <ValuesFormWrapper>
+              <ValuesForm 
+                sections={tab.sections} 
+                onSubmit={(x: any) => console.log(x)}
+              />
+            </ValuesFormWrapper>
+          ),
+        });
       });
-    });
+    }
 
     // Append universal tabs
     tabOptions.push(

+ 4 - 4
dashboard/src/main/home/templates/Templates.tsx

@@ -75,15 +75,15 @@ export default class Templates extends Component<PropsType, StateType> {
     }
 
     return this.state.porterCharts.map((template: PorterChart, i: number) => {
-      let { Name, Icon, Description } = template.Form;
+      let { name, icon, description } = template.form;
       return (
         <TemplateBlock key={i} onClick={() => this.setState({ currentTemplate: template })}>
-          {Icon ? this.renderIcon(Icon) : this.renderIcon(template.Icon)}
+          {icon ? this.renderIcon(icon) : this.renderIcon(template.icon)}
           <TemplateTitle>
-            {Name ? Name : template.Name}
+            {name ? name : template.name}
           </TemplateTitle>
           <TemplateDescription>
-            {Description ? Description : template.Description}
+            {description ? description : template.description}
           </TemplateDescription>
         </TemplateBlock>
       )

+ 33 - 9
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -4,7 +4,7 @@ import styled from 'styled-components';
 import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 
-import { PorterChart, ChoiceType, Cluster } from '../../../../shared/types';
+import { PorterChart, ChoiceType, Cluster, StorageType } from '../../../../shared/types';
 import Selector from '../../../../components/Selector';
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
@@ -34,17 +34,41 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     tabContents: [] as any,
   };
 
-  componentDidMount() {
+  onSubmit = (formValues: any) => {
+    let { currentCluster, currentProject } = this.context;
+    api.deployTemplate('<token>', {
+      templateName: this.props.currentTemplate.name,
+      imageURL: "index.docker.io/bitnami/redis",
+      storage: StorageType.Secret,
+      formValues,
+    }, {
+      id: currentProject.id,
+      cluster_id: currentCluster.id,
+      service_account_id: currentCluster.service_account_id,
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        console.log(res.data)
+      }
+    });
+  }
 
+  componentDidMount() {
     // Generate settings tabs from the provided form
     let tabOptions = [] as ChoiceType[];
     let tabContents = [] as any;
-    this.props.currentTemplate.Form.Tabs.map((tab: any, i: number) => {
-      tabOptions.push({ value: tab.Name, label: tab.Label });
+    this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
+      tabOptions.push({ value: tab.name, label: tab.label });
       tabContents.push({
-        value: tab.Name, component: (
+        value: tab.name, component: (
           <ValuesFormWrapper>
-            <ValuesForm sections={tab.Sections} />
+            <ValuesForm 
+              sections={tab.sections} 
+              onSubmit={this.onSubmit}
+              // disabled={this.state.selectedImageUrl === ''}
+              disabled={false}
+            />
           </ValuesFormWrapper>
         ),
       });
@@ -76,9 +100,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   render() {
-    let { Name, Icon, Description } = this.props.currentTemplate.Form;
+    let { name, icon, description } = this.props.currentTemplate.form;
     let { currentTemplate } = this.props;
-    let name = Name ? Name : currentTemplate.Name;
+    name = name ? name : currentTemplate.name;
 
     return (
       <StyledLaunchTemplate>
@@ -92,7 +116,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </TitleSection>
         <ClusterSection>
           <Template>
-            {Icon ? this.renderIcon(Icon) : this.renderIcon(currentTemplate.Icon)}
+            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
             {name}
           </Template>
           <i className="material-icons">arrow_right_alt</i>

+ 10 - 10
dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx

@@ -28,7 +28,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
   }
 
   renderTagList = () => {
-    return this.props.currentTemplate.Form.Tags.map((tag: string, i: number) => {
+    return this.props.currentTemplate.form.tags.map((tag: string, i: number) => {
       return (
         <Tag key={i}>{tag}</Tag>
       )
@@ -37,19 +37,19 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
 
   renderMarkdown = () => {
     let { currentTemplate } = this.props;
-    if (currentTemplate.Markdown) {
+    if (currentTemplate.markdown) {
       return (
-        <Markdown>{currentTemplate.Markdown}</Markdown>
+        <Markdown>{currentTemplate.markdown}</Markdown>
       );
-    } else if (currentTemplate.Form.Description) {
-      return currentTemplate.Form.Description;
+    } else if (currentTemplate.form.description) {
+      return currentTemplate.form.description;
     }
 
-    return currentTemplate.Description;
+    return currentTemplate.description;
   }
 
   renderTagSection = () => {
-    if (this.props.currentTemplate.Form.Tags) {
+    if (this.props.currentTemplate.form.tags) {
       return (
         <TagSection>
           <i className="material-icons">local_offer</i>
@@ -61,9 +61,9 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
 
   render() {
     let { currentCluster } = this.context;
-    let { Name, Icon } = this.props.currentTemplate.Form;
+    let { name, icon } = this.props.currentTemplate.form;
     let { currentTemplate } = this.props;
-    let name = Name ? Name : currentTemplate.Name;
+    name = name ? name : currentTemplate.name;
     return (
       <StyledExpandedTemplate>
         <TitleSection>
@@ -71,7 +71,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
             <i className="material-icons" onClick={() => this.props.setCurrentTemplate(null)}>
               keyboard_backspace
             </i>
-            {Icon ? this.renderIcon(Icon) : this.renderIcon(currentTemplate.Icon)}
+            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
             <Title>{name}</Title>
           </Flex>
           <Button

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

@@ -162,8 +162,14 @@ const deleteProject = baseApi<{}, { id: number }>('DELETE', pathParams => {
   return `/api/projects/${pathParams.id}`;
 });
 
-const deployTemplate = baseApi<{}, { id: number }>('POST', pathParams => {
-  return `/api/projects/${pathParams.id}/deploy`;
+const deployTemplate = baseApi<{
+  templateName: string,
+  imageURL: string,
+  formValues: any,
+  storage: StorageType,
+}, { id: number, cluster_id: number, service_account_id: number }>('POST', pathParams => {
+  let {id, cluster_id, service_account_id} = pathParams;
+  return `/api/projects/${id}/deploy?cluster_id=${cluster_id}&service_account_id=${service_account_id}`;
 });
 
 // Bundle export to allow default api import (api.<method> is more readable)

+ 6 - 0
dashboard/src/shared/common.tsx

@@ -9,4 +9,10 @@ export const getRegistryIcon = (kind: string) => {
     default:
       return null
   }
+}
+
+export const getIgnoreCase = (object: any, key: string) => {
+  return object[Object.keys(object)
+    .find(k => k.toLowerCase() === key.toLowerCase())
+  ];
 }

+ 28 - 24
dashboard/src/shared/types.tsx

@@ -23,6 +23,10 @@ export interface ChartType {
       icon: string,
       apiVersion: string
     },
+    files?: {
+      data: string,
+      name: string,
+    }[],
   },
   config: string,
   version: number,
@@ -64,42 +68,42 @@ export enum StorageType {
 
 // PorterChart represents a bundled Porter template
 export interface PorterChart {
-	Name: string,
-	Description: string,
-	Icon: string,
-  Form: FormYAML,
-  Markdown?: string,
+	name: string,
+	description: string,
+	icon: string,
+  form: FormYAML,
+  markdown?: string,
 }
 
 // FormYAML represents a chart's values.yaml form abstraction
 export interface FormYAML {
-	Name?: string,  
-	Icon?: string,   
-	Description?: string,   
-  Tags?: string[],
-  Tabs?: {
-    Name: string,
-    Label: string,
-    Sections?: Section[]
+	name?: string,  
+	icon?: string,   
+	description?: string,   
+  tags?: string[],
+  tabs?: {
+    name: string,
+    label: string,
+    sections?: Section[]
   }[]
 }
 
 export interface Section {
-  Name?: string,
-  ShowIf?: string,
-  Contents: FormElement[]
+  name?: string,
+  show_if?: string,
+  contents: FormElement[]
 }
 
 // FormElement represents a form element
 export interface FormElement {
-  Type: string,
-  Label: string,
-  Name?: string,
-  Variable?: string,
-  Settings?: {
-    Default?: number | string | boolean,
-    Options?: any[],
-    Unit?: string
+  type: string,
+  label: string,
+  name?: string,
+  variable?: string,
+  settings?: {
+    default?: number | string | boolean,
+    options?: any[],
+    unit?: string
   }
 }
 

+ 2 - 0
go.sum

@@ -1882,8 +1882,10 @@ gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpA
 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
 helm.sh/helm v2.16.12+incompatible h1:nQfifk10KcpAGD1RJaNZVW/fWiqluV0JMuuDwdba4rw=
 helm.sh/helm v2.16.12+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4=
+helm.sh/helm v2.17.0+incompatible h1:cSe3FaQOpRWLDXvTObQNj0P7WI98IG5yloU6tQVls2k=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
 helm.sh/helm/v3 v3.3.4/go.mod h1:CyCGQa53/k1JFxXvXveGwtfJ4cuB9zkaBSGa5rnAiHU=
+helm.sh/helm/v3 v3.4.1 h1:NIdlBGKFRTAkhz0ooYKw1VBbmTldxNAZRY1nH6Glk6I=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 13 - 0
internal/forms/release.go

@@ -123,3 +123,16 @@ type UpgradeReleaseForm struct {
 	Name   string `json:"name" form:"required"`
 	Values string `json:"values" form:"required"`
 }
+
+// ChartTemplateForm represents the accepted values for installing a new chart from a template.
+type ChartTemplateForm struct {
+	TemplateName string                 `json:"templateName" form:"required"`
+	ImageURL     string                 `json:"imageURL" form:"required"`
+	FormValues   map[string]interface{} `json:"formValues"`
+}
+
+// InstallChartTemplateForm represents the accepted values for installing a new chart from a template.
+type InstallChartTemplateForm struct {
+	*ReleaseForm
+	*ChartTemplateForm
+}

+ 56 - 0
internal/helm/agent.go

@@ -3,7 +3,10 @@ package helm
 import (
 	"fmt"
 
+	"github.com/pkg/errors"
 	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/release"
 	"k8s.io/helm/pkg/chartutil"
 )
@@ -77,6 +80,47 @@ func (a *Agent) UpgradeRelease(
 	return res, nil
 }
 
+// InstallChart installs a new chart by URL, absolute or relative filepaths.
+// Equivalent to `helm install [CHART_NAME] [cp]` where cp is one of the following:
+//  1) Absolute URL: https://example.com/charts/nginx-1.2.3.tgz
+//  2) path to packaged chart ./nginx-1.2.3.tgz
+//  3) path to unpacked chart ./nginx
+func (a *Agent) InstallChart(
+	cp string,
+	values []byte,
+) (*release.Release, error) {
+	cmd := action.NewInstall(a.ActionConfig)
+	valuesYaml, err := chartutil.ReadValues(values)
+
+	if err != nil {
+		return nil, fmt.Errorf("Values could not be parsed: %v", err)
+	}
+
+	// Only supports filepaths for now, URL option WIP.
+	// Check chart dependencies to make sure all are present in /charts
+	chartRequested, err := loader.Load(cp)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := checkIfInstallable(chartRequested); err != nil {
+		return nil, err
+	}
+
+	if chartRequested.Metadata.Deprecated {
+		return nil, fmt.Errorf("This chart is deprecated")
+	}
+
+	if req := chartRequested.Metadata.Dependencies; req != nil {
+		if err := action.CheckDependencies(chartRequested, req); err != nil {
+			// TODO: Handle dependency updates.
+			return nil, err
+		}
+	}
+
+	return cmd.Run(chartRequested, valuesYaml)
+}
+
 // RollbackRelease rolls a release back to a specified revision/version
 func (a *Agent) RollbackRelease(
 	name string,
@@ -86,3 +130,15 @@ func (a *Agent) RollbackRelease(
 	cmd.Version = version
 	return cmd.Run(name)
 }
+
+// ------------------------ Helm agent helper functions ------------------------ //
+
+// checkIfInstallable validates if a chart can be installed
+// Application chart type is only installable
+func checkIfInstallable(ch *chart.Chart) error {
+	switch ch.Metadata.Type {
+	case "", "application":
+		return nil
+	}
+	return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
+}

+ 24 - 23
internal/models/templates.go

@@ -23,34 +23,35 @@ type ChartYAML []struct {
 
 // PorterChart represents a bundled Porter template
 type PorterChart struct {
-	Name        string
-	Description string
-	Icon        string
-	Form        FormYAML
-	Markdown    string
+	Name        string   `json:"name"`
+	Description string   `json:"description"`
+	Icon        string   `json:"icon"`
+	Form        FormYAML `json:"form"`
+	Markdown    string   `json:"markdown"`
 }
 
 // FormYAML represents a chart's values.yaml form abstraction
 type FormYAML struct {
-	Name        string   `yaml:"name"`
-	Icon        string   `yaml:"icon"`
-	Description string   `yaml:"description"`
-	Tags        []string `yaml:"tags"`
+	Name        string   `yaml:"name" json:"name"`
+	Icon        string   `yaml:"icon" json:"icon"`
+	Description string   `yaml:"description" json:"description"`
+	Tags        []string `yaml:"tags" json:"tags"`
 	Tabs        []struct {
-		Name     string `yaml:"name"`
-		Label    string `yaml:"label"`
+		Name     string `yaml:"name" json:"name"`
+		Label    string `yaml:"label" json:"label"`
 		Sections []struct {
-			Name     string `yaml:"name"`
-			ShowIf   string `yaml:"show_if"`
+			Name     string `yaml:"name" json:"name"`
+			ShowIf   string `yaml:"show_if" json:"show_if"`
 			Contents []struct {
-				Type     string `yaml:"type"`
-				Label    string `yaml:"label"`
-				Name     string `yaml:"name,omitempty"`
-				Variable string `yaml:"variable,omitempty"`
+				Type     string `yaml:"type" json:"type"`
+				Label    string `yaml:"label" json:"label"`
+				Name     string `yaml:"name,omitempty" json:"name,omitempty"`
+				Variable string `yaml:"variable,omitempty" json:"variable,omitempty"`
 				Settings struct {
-					Default interface{}
-				} `yaml:"settings,omitempty"`
-			} `yaml:"contents"`
-		} `yaml:"sections"`
-	} `yaml:"tabs"`
-}
+					Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
+					Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
+				} `yaml:"settings,omitempty" json:"settings,omitempty"`
+			} `yaml:"contents" json:"contents,omitempty"`
+		} `yaml:"sections" json:"sections,omitempty"`
+	} `yaml:"tabs" json:"tabs,omitempty"`
+}

+ 21 - 0
node_modules/@types/js-base64/LICENSE

@@ -0,0 +1,21 @@
+    MIT License
+
+    Copyright (c) Microsoft Corporation.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in all
+    copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+    SOFTWARE

+ 16 - 0
node_modules/@types/js-base64/README.md

@@ -0,0 +1,16 @@
+# Installation
+> `npm install --save @types/js-base64`
+
+# Summary
+This package contains type definitions for js-base64 (https://github.com/dankogai/js-base64).
+
+# Details
+Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/js-base64.
+
+### Additional Details
+ * Last updated: Sat, 18 Jul 2020 15:47:12 GMT
+ * Dependencies: none
+ * Global values: `Base64`
+
+# Credits
+These definitions were written by [Denis Carriere](https://github.com/DenisCarriere), [Tommy Lent](https://github.com/tlent), and [JounQin](https://github.com/JounQin).

+ 82 - 0
node_modules/@types/js-base64/index.d.ts

@@ -0,0 +1,82 @@
+// Type definitions for js-base64 3.0
+// Project: https://github.com/dankogai/js-base64
+// Definitions by: Denis Carriere <https://github.com/DenisCarriere>
+//                 Tommy Lent <https://github.com/tlent>
+//                 JounQin <https://github.com/JounQin>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+
+export interface Base64 {
+    VERSION: string;
+    encode(s: string, uriSafe?: boolean): string;
+    encodeURI(s: string): string;
+    encodeURL: Base64['encodeURI'];
+    decode(base64: string): string;
+    atob(base64: string): string;
+    btoa(s: string): string;
+    fromBase64(base64: string): string;
+    toBase64(s: string, uriSafe?: boolean): string;
+    btou(s: string): string;
+    utob(s: string): string;
+    fromUint8Array(uint8Array: Uint8Array, uriSafe?: boolean): string;
+    toUint8Array(s: string): Uint8Array;
+    extendString(): void;
+    extendUint8Array(): void;
+    extendBuiltins(): void;
+}
+
+export const Base64: Base64;
+
+export const VERSION: string;
+
+export const encode: Base64['encode'];
+
+export const encodeURI: Base64['encodeURI'];
+
+export const encodeURL: Base64['encodeURL'];
+
+export const decode: Base64['decode'];
+
+export const atob: Base64['atob'];
+
+export const btoa: Base64['btoa'];
+
+export const fromBase64: Base64['fromBase64'];
+
+export const toBase64: Base64['toBase64'];
+
+export const btou: Base64['btou'];
+
+export const utob: Base64['utob'];
+
+export const fromUint8Array: Base64['fromUint8Array'];
+
+export const toUint8Array: Base64['toUint8Array'];
+
+export const extendString: Base64['extendString'];
+
+export const extendUint8Array: Base64['extendUint8Array'];
+
+export const extendBuiltins: Base64['extendBuiltins'];
+
+/**
+ * only for global usage, not available in esm actually
+ */
+export function noConflict(): Base64;
+
+export as namespace Base64;
+
+declare global {
+    interface String {
+        fromBase64(): string;
+        toBase64(uriSafe?: boolean): string;
+        toBase64URI(): string;
+        toBase64URL(): string;
+        toUint8Array(): Uint8Array;
+    }
+
+    interface Uint8Array {
+        toBase64(uriSafe?: boolean): string;
+        toBase64URI(): string;
+        toBase64URL(): string;
+    }
+}

+ 62 - 0
node_modules/@types/js-base64/package.json

@@ -0,0 +1,62 @@
+{
+  "_from": "@types/js-base64",
+  "_id": "@types/js-base64@3.0.0",
+  "_inBundle": false,
+  "_integrity": "sha512-BnEyOcDE4H6bkg8m84xhdbkYoAoCg8sYERmAvE4Ff50U8jTfbmOinRdJpauBn1P9XsCCQgCLuSiyz3PM4WHYOA==",
+  "_location": "/@types/js-base64",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "@types/js-base64",
+    "name": "@types/js-base64",
+    "escapedName": "@types%2fjs-base64",
+    "scope": "@types",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-3.0.0.tgz",
+  "_shasum": "b7b4c130facefefd5c57ba82664c41e2995f91be",
+  "_spec": "@types/js-base64",
+  "_where": "/Users/trevorshim/Development/porter-dev/porter",
+  "bugs": {
+    "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/issues"
+  },
+  "bundleDependencies": false,
+  "contributors": [
+    {
+      "name": "Denis Carriere",
+      "url": "https://github.com/DenisCarriere"
+    },
+    {
+      "name": "Tommy Lent",
+      "url": "https://github.com/tlent"
+    },
+    {
+      "name": "JounQin",
+      "url": "https://github.com/JounQin"
+    }
+  ],
+  "dependencies": {},
+  "deprecated": false,
+  "description": "TypeScript definitions for js-base64",
+  "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped#readme",
+  "license": "MIT",
+  "main": "",
+  "name": "@types/js-base64",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/DefinitelyTyped/DefinitelyTyped.git",
+    "directory": "types/js-base64"
+  },
+  "scripts": {},
+  "typeScriptVersion": "3.0",
+  "types": "index.d.ts",
+  "typesPublisherContentHash": "4b5afb34917caed330bdb1d07cae9ec4f28c8f27affcb5551a4412b3f9d082eb",
+  "version": "3.0.0"
+}

+ 105 - 14
server/api/deploy_handler.go

@@ -4,26 +4,122 @@ import (
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
 	"gopkg.in/yaml.v2"
 )
 
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
-	tgt := "hello-porter"
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	form := &forms.InstallChartTemplateForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
+		},
+		ChartTemplateForm: &forms.ChartTemplateForm{},
+	}
+
+	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+		vals,
+		app.repo.ServiceAccount,
+	)
+
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	agent, err := app.getAgentFromReleaseForm(
+		w,
+		r,
+		form.ReleaseForm,
+	)
+
+	if err != nil {
+		return
+	}
 
 	baseURL := "https://porter-dev.github.io/chart-repo/"
+	values, err := getDefaultValues(form.ChartTemplateForm.TemplateName, baseURL)
+	if err != nil {
+		return
+	}
+
+	// Set image URL
+	(*values)["image"].(map[interface{}]interface{})["repository"] = form.ChartTemplateForm.ImageURL
+
+	// Loop through form params to override
+	for k := range form.ChartTemplateForm.FormValues {
+		switch v := interface{}(k).(type) {
+		case string:
+			splits := strings.Split(v, ".")
+
+			// Validate that the field to override exists
+			currentLoc := *values
+			for s := range splits {
+				key := splits[s]
+				val := currentLoc[key]
+				if val == nil {
+					fmt.Printf("No such field: %v\n", key)
+				} else if s == len(splits)-1 {
+					newValue := form.ChartTemplateForm.FormValues[v]
+					fmt.Printf("Overriding default %v with %v\n", val, newValue)
+					currentLoc[key] = newValue
+				} else {
+					fmt.Println("Traversing...")
+					currentLoc = val.(map[interface{}]interface{})
+				}
+			}
+		default:
+			fmt.Println("Non-string type")
+		}
+	}
+
+	v, err := yaml.Marshal(values)
+
+	if err != nil {
+		return
+	}
+
+	// Output values.yaml string
+	_, err = agent.InstallChart(baseURL+"react-0.1.5.tgz", v)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error installing a new chart" + err.Error()},
+		}, w)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
+// ------------------------ Deploy handler helper functions ------------------------ //
+
+func getDefaultValues(templateName string, baseURL string) (*map[interface{}]interface{}, error) {
 	resp, err := http.Get(baseURL + "index.yaml")
 	if err != nil {
 		fmt.Println(err)
-		return
+		return nil, err
 	}
 
 	defer resp.Body.Close()
@@ -32,7 +128,7 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	form := models.IndexYAML{}
 	if err := yaml.Unmarshal([]byte(body), &form); err != nil {
 		fmt.Println(err)
-		return
+		return nil, err
 	}
 
 	// Loop over charts in index.yaml
@@ -47,25 +143,20 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		}
 
 		// Unpack the target chart and retrieve values.yaml
-		if strAcc == tgt {
+		if strAcc == templateName {
 			tgtURL := baseURL + tarURL
 			values, err := processValues(tgtURL)
 			if err != nil {
 				fmt.Println(err)
-				return
-			}
-
-			defaultValues := *values
-			defaultValues["replicaCount"] = 87
-			fmt.Println(defaultValues["replicaCount"])
-			for k := range *values {
-				fmt.Println(k)
+				return nil, err
 			}
+			return values, nil
 		}
 	}
+	return nil, errors.New("no values.yaml found")
 }
 
-func processValues(tgtURL string) (*map[string]interface{}, error) {
+func processValues(tgtURL string) (*map[interface{}]interface{}, error) {
 	resp, err := http.Get(tgtURL)
 	if err != nil {
 		fmt.Println(err)
@@ -110,7 +201,7 @@ func processValues(tgtURL string) (*map[string]interface{}, error) {
 				}
 
 				// Unmarshal yaml byte buffer
-				form := make(map[string]interface{})
+				form := make(map[interface{}]interface{})
 				if err := yaml.Unmarshal(bufForm.Bytes(), &form); err != nil {
 					fmt.Println(err)
 					return nil, err

+ 2 - 1
server/api/release_handler.go

@@ -159,7 +159,8 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 	}
 }
 
-// HandleGetReleaseControllers retrieves a single release based on a name and revision
+// HandleGetReleaseControllers retrieves controllers that belong to a release.
+// Used to display status of charts.
 func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
 	revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)

+ 9 - 1
server/router/router.go

@@ -275,7 +275,15 @@ func New(
 		r.Method(
 			"POST",
 			"/projects/{project_id}/deploy",
-			auth.BasicAuthenticate(requestlog.NewHandler(a.HandleDeployTemplate, l)),
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveServiceAccountAccess(
+					requestlog.NewHandler(a.HandleDeployTemplate, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
 		)
 
 		// /api/templates routes