Bläddra i källkod

Merge branch 'launch-template' into beta.3.deploy-agent

sunguroku 5 år sedan
förälder
incheckning
5c3df57e60

+ 10 - 0
dashboard/package-lock.json

@@ -401,6 +401,11 @@
         "jest-diff": "^24.3.0"
         "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=="
+    },
     "@types/js-yaml": {
     "@types/js-yaml": {
       "version": "3.12.5",
       "version": "3.12.5",
       "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz",
       "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz",
@@ -4183,6 +4188,11 @@
       "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz",
       "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz",
       "integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU="
       "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": {
     "js-tokens": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

+ 2 - 0
dashboard/package.json

@@ -4,12 +4,14 @@
   "private": true,
   "private": true,
   "dependencies": {
   "dependencies": {
     "@fullstory/browser": "^1.4.5",
     "@fullstory/browser": "^1.4.5",
+    "@types/js-base64": "^3.0.0",
     "@types/js-yaml": "^3.12.5",
     "@types/js-yaml": "^3.12.5",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/qs": "^6.9.5",
     "@types/qs": "^6.9.5",
     "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",
+    "js-base64": "^3.6.0",
     "js-yaml": "^3.14.0",
     "js-yaml": "^3.14.0",
     "markdown-to-jsx": "^7.0.1",
     "markdown-to-jsx": "^7.0.1",
     "posthog-node": "^1.0.6",
     "posthog-node": "^1.0.6",

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

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

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

@@ -11,7 +11,9 @@ import InputRow from './InputRow';
 import SelectRow from './SelectRow';
 import SelectRow from './SelectRow';
 
 
 type PropsType = {
 type PropsType = {
-  sections?: Section[]
+  onSubmit: (formValues: any) => void,
+  sections?: Section[],
+  disabled?: boolean,
 };
 };
 
 
 type StateType = any;
 type StateType = any;
@@ -21,13 +23,13 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   updateFormState() {
   updateFormState() {
     let formState: any = {};
     let formState: any = {};
     this.props.sections.forEach((section: Section, i: number) => {
     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
         // 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':
           case 'checkbox':
             formState[key] = def ? def : false;
             formState[key] = def ? def : false;
             break;
             break;
@@ -35,10 +37,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             formState[key] = def ? def : '';
             formState[key] = def ? def : '';
             break;
             break;
           case 'number-input':
           case 'number-input':
-            formState[key] = def.toString() ? def.toString() : '';
+            formState[key] = def.toString() ? def : '';
             break;
             break;
           case 'select':
           case 'select':
-            formState[key] = def ? def : item.Settings.Options[0].Value;
+            formState[key] = def ? def : item.settings.options[0].value;
           default:
           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) => {
   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
       // 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':
         case 'heading':
-          return <Heading key={i}>{item.Label}</Heading>
+          return <Heading key={i}>{item.label}</Heading>
         case 'subtitle':
         case 'subtitle':
-          return <Helper key={i}>{item.Label}</Helper>
+          return <Helper key={i}>{item.label}</Helper>
         case 'checkbox':
         case 'checkbox':
           return (
           return (
             <CheckboxRow
             <CheckboxRow
               key={i}
               key={i}
               checked={this.state[key]}
               checked={this.state[key]}
               toggle={() => this.setState({ [key]: !this.state[key] })}
               toggle={() => this.setState({ [key]: !this.state[key] })}
-              label={item.Label}
+              label={item.label}
             />
             />
           );
           );
         case 'string-input':
         case 'string-input':
@@ -98,8 +85,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               type='text'
               type='text'
               value={this.state[key]}
               value={this.state[key]}
               setValue={(x: string) => this.setState({ [key]: x })}
               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':
         case 'number-input':
@@ -108,9 +95,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               key={i}
               key={i}
               type='number'
               type='number'
               value={this.state[key]}
               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':
         case 'select':
@@ -119,9 +106,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               key={i}
               key={i}
               value={this.state[key]}
               value={this.state[key]}
               setActiveValue={(val) => this.setState({ [key]: val })}
               setActiveValue={(val) => this.setState({ [key]: val })}
-              options={item.Settings.Options}
+              options={item.settings.options}
               dropdownLabel=''
               dropdownLabel=''
-              label={item.Label}
+              label={item.label}
             />
             />
           );
           );
         default:
         default:
@@ -134,8 +121,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
       return this.props.sections.map((section: Section, i: number) => {
       return this.props.sections.map((section: Section, i: number) => {
 
 
         // Hide collapsible section if deciding field is false
         // 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;
             return null;
           }
           }
         }
         }
@@ -157,8 +144,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           {this.renderFormContents()}
           {this.renderFormContents()}
         </StyledValuesForm>
         </StyledValuesForm>
         <SaveButton
         <SaveButton
+          disabled={this.props.disabled}
           text='Deploy'
           text='Deploy'
-          onClick={this.handleDeploy}
+          onClick={() => this.props.onSubmit(this.state)}
           status={null}
           status={null}
           makeFlush={true}
           makeFlush={true}
         />
         />

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

@@ -1,5 +1,7 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import styled from 'styled-components';
 import styled from 'styled-components';
+import yaml from 'js-yaml';
+import { Base64 } from 'js-base64';
 import close from '../../../../assets/close.png';
 import close from '../../../../assets/close.png';
 
 
 import { ResourceType, ChartType, StorageType, ChoiceType } from '../../../../shared/types';
 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 = () => {
   refreshTabs = () => {
-    // Generate settings tabs from the provided form
+    let formData = this.getFormData();
     let tabOptions = [] as ChoiceType[];
     let tabOptions = [] as ChoiceType[];
     let tabContents = [] as any;
     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
     // Append universal tabs
     tabOptions.push(
     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) => {
     return this.state.porterCharts.map((template: PorterChart, i: number) => {
-      let { Name, Icon, Description } = template.Form;
+      let { name, icon, description } = template.form;
       return (
       return (
         <TemplateBlock key={i} onClick={() => this.setState({ currentTemplate: template })}>
         <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>
           <TemplateTitle>
-            {Name ? Name : template.Name}
+            {name ? name : template.name}
           </TemplateTitle>
           </TemplateTitle>
           <TemplateDescription>
           <TemplateDescription>
-            {Description ? Description : template.Description}
+            {description ? description : template.description}
           </TemplateDescription>
           </TemplateDescription>
         </TemplateBlock>
         </TemplateBlock>
       )
       )

+ 32 - 7
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -34,17 +34,42 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     tabContents: [] as any,
     tabContents: [] as any,
   };
   };
 
 
+  onSubmit = (formValues: any) => {
+    console.log(formValues);
+
+    let { currentCluster, currentProject } = this.context;
+    console.log(formValues);
+    api.deployTemplate('<token>', {
+      templateName: this.props.currentTemplate.name,
+      clusterID: currentCluster.id,
+      imageURL: "index.docker.io/bitnami/redis",
+      formValues,
+    }, {
+      id: currentProject.id,
+    }, (err: any, res: any) => {
+      if (err) {
+        // console.log(err)
+      } else {
+        // console.log(res.data)
+      }
+    });
+  }
+
   componentDidMount() {
   componentDidMount() {
 
 
     // Generate settings tabs from the provided form
     // Generate settings tabs from the provided form
     let tabOptions = [] as ChoiceType[];
     let tabOptions = [] as ChoiceType[];
     let tabContents = [] as any;
     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({
       tabContents.push({
-        value: tab.Name, component: (
+        value: tab.name, component: (
           <ValuesFormWrapper>
           <ValuesFormWrapper>
-            <ValuesForm sections={tab.Sections} />
+            <ValuesForm 
+              sections={tab.sections} 
+              onSubmit={this.onSubmit}
+              disabled={this.state.selectedImageUrl === ''}
+            />
           </ValuesFormWrapper>
           </ValuesFormWrapper>
         ),
         ),
       });
       });
@@ -76,9 +101,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
   }
 
 
   render() {
   render() {
-    let { Name, Icon, Description } = this.props.currentTemplate.Form;
+    let { name, icon, description } = this.props.currentTemplate.form;
     let { currentTemplate } = this.props;
     let { currentTemplate } = this.props;
-    let name = Name ? Name : currentTemplate.Name;
+    name = name ? name : currentTemplate.name;
 
 
     return (
     return (
       <StyledLaunchTemplate>
       <StyledLaunchTemplate>
@@ -92,7 +117,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </TitleSection>
         </TitleSection>
         <ClusterSection>
         <ClusterSection>
           <Template>
           <Template>
-            {Icon ? this.renderIcon(Icon) : this.renderIcon(currentTemplate.Icon)}
+            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
             {name}
             {name}
           </Template>
           </Template>
           <i className="material-icons">arrow_right_alt</i>
           <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 = () => {
   renderTagList = () => {
-    return this.props.currentTemplate.Form.Tags.map((tag: string, i: number) => {
+    return this.props.currentTemplate.form.tags.map((tag: string, i: number) => {
       return (
       return (
         <Tag key={i}>{tag}</Tag>
         <Tag key={i}>{tag}</Tag>
       )
       )
@@ -37,19 +37,19 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
 
 
   renderMarkdown = () => {
   renderMarkdown = () => {
     let { currentTemplate } = this.props;
     let { currentTemplate } = this.props;
-    if (currentTemplate.Markdown) {
+    if (currentTemplate.markdown) {
       return (
       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 = () => {
   renderTagSection = () => {
-    if (this.props.currentTemplate.Form.Tags) {
+    if (this.props.currentTemplate.form.tags) {
       return (
       return (
         <TagSection>
         <TagSection>
           <i className="material-icons">local_offer</i>
           <i className="material-icons">local_offer</i>
@@ -61,9 +61,9 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
 
 
   render() {
   render() {
     let { currentCluster } = this.context;
     let { currentCluster } = this.context;
-    let { Name, Icon } = this.props.currentTemplate.Form;
+    let { name, icon } = this.props.currentTemplate.form;
     let { currentTemplate } = this.props;
     let { currentTemplate } = this.props;
-    let name = Name ? Name : currentTemplate.Name;
+    name = name ? name : currentTemplate.name;
     return (
     return (
       <StyledExpandedTemplate>
       <StyledExpandedTemplate>
         <TitleSection>
         <TitleSection>
@@ -71,7 +71,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
             <i className="material-icons" onClick={() => this.props.setCurrentTemplate(null)}>
             <i className="material-icons" onClick={() => this.props.setCurrentTemplate(null)}>
               keyboard_backspace
               keyboard_backspace
             </i>
             </i>
-            {Icon ? this.renderIcon(Icon) : this.renderIcon(currentTemplate.Icon)}
+            {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
             <Title>{name}</Title>
             <Title>{name}</Title>
           </Flex>
           </Flex>
           <Button
           <Button

+ 6 - 1
dashboard/src/shared/api.tsx

@@ -162,7 +162,12 @@ const deleteProject = baseApi<{}, { id: number }>('DELETE', pathParams => {
   return `/api/projects/${pathParams.id}`;
   return `/api/projects/${pathParams.id}`;
 });
 });
 
 
-const deployTemplate = baseApi<{}, { id: number }>('POST', pathParams => {
+const deployTemplate = baseApi<{
+  templateName: string,
+  clusterID: number,
+  imageURL: string,
+  formValues: any
+}, { id: number }>('POST', pathParams => {
   return `/api/projects/${pathParams.id}/deploy`;
   return `/api/projects/${pathParams.id}/deploy`;
 });
 });
 
 

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

@@ -9,4 +9,10 @@ export const getRegistryIcon = (kind: string) => {
     default:
     default:
       return null
       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,
       icon: string,
       apiVersion: string
       apiVersion: string
     },
     },
+    files?: {
+      data: string,
+      name: string,
+    }[],
   },
   },
   config: string,
   config: string,
   version: number,
   version: number,
@@ -64,42 +68,42 @@ export enum StorageType {
 
 
 // PorterChart represents a bundled Porter template
 // PorterChart represents a bundled Porter template
 export interface PorterChart {
 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
 // FormYAML represents a chart's values.yaml form abstraction
 export interface FormYAML {
 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 {
 export interface Section {
-  Name?: string,
-  ShowIf?: string,
-  Contents: FormElement[]
+  name?: string,
+  show_if?: string,
+  contents: FormElement[]
 }
 }
 
 
 // FormElement represents a form element
 // FormElement represents a form element
 export interface FormElement {
 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
   }
   }
 }
 }
 
 

+ 24 - 23
internal/models/templates.go

@@ -23,34 +23,35 @@ type ChartYAML []struct {
 
 
 // PorterChart represents a bundled Porter template
 // PorterChart represents a bundled Porter template
 type PorterChart struct {
 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
 // FormYAML represents a chart's values.yaml form abstraction
 type FormYAML struct {
 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 {
 	Tabs        []struct {
-		Name     string `yaml:"name"`
-		Label    string `yaml:"label"`
+		Name     string `yaml:"name" json:"name"`
+		Label    string `yaml:"label" json:"label"`
 		Sections []struct {
 		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 {
 			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 {
 				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"`
+}

+ 78 - 14
server/api/deploy_handler.go

@@ -4,6 +4,7 @@ import (
 	"archive/tar"
 	"archive/tar"
 	"bytes"
 	"bytes"
 	"compress/gzip"
 	"compress/gzip"
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
@@ -15,15 +16,83 @@ import (
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 )
 )
 
 
+// DeployTemplateForm describes the parameters of a deploy template request
+type DeployTemplateForm struct {
+	TemplateName string
+	ClusterID    int
+	ImageURL     string
+	FormValues   map[string]interface{}
+}
+
 // HandleDeployTemplate triggers a chart deployment from a template
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
-	tgt := "hello-porter"
+
+	// TODO: use create form
+	requestForm := make(map[string]interface{})
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(&requestForm); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// TODO: use create form
+	params := DeployTemplateForm{}
+	params.TemplateName = requestForm["templateName"].(string)
+	params.ClusterID = int(requestForm["clusterID"].(float64))
+	params.ImageURL = requestForm["imageURL"].(string)
+	params.FormValues = requestForm["formValues"].(map[string]interface{})
 
 
 	baseURL := "https://porter-dev.github.io/chart-repo/"
 	baseURL := "https://porter-dev.github.io/chart-repo/"
+	defaultValues, err := getDefaultValues(params.TemplateName, baseURL)
+	if err != nil {
+		return
+	}
+
+	// Set image URL
+	(*defaultValues)["image"].(map[interface{}]interface{})["repository"] = params.ImageURL
+
+	// Loop through form params to override
+	for k := range params.FormValues {
+		switch v := interface{}(k).(type) {
+		case string:
+			splits := strings.Split(v, ".")
+
+			// Validate that the field to override exists
+			currentLoc := *defaultValues
+			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 := params.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")
+		}
+	}
+
+	d, err := yaml.Marshal(defaultValues)
+	if err != nil {
+		return
+	}
+
+	// Output values.yaml string
+	fmt.Println(string(d))
+}
+
+func getDefaultValues(templateName string, baseURL string) (*map[interface{}]interface{}, error) {
 	resp, err := http.Get(baseURL + "index.yaml")
 	resp, err := http.Get(baseURL + "index.yaml")
 	if err != nil {
 	if err != nil {
 		fmt.Println(err)
 		fmt.Println(err)
-		return
+		return nil, err
 	}
 	}
 
 
 	defer resp.Body.Close()
 	defer resp.Body.Close()
@@ -32,7 +101,7 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	form := models.IndexYAML{}
 	form := models.IndexYAML{}
 	if err := yaml.Unmarshal([]byte(body), &form); err != nil {
 	if err := yaml.Unmarshal([]byte(body), &form); err != nil {
 		fmt.Println(err)
 		fmt.Println(err)
-		return
+		return nil, err
 	}
 	}
 
 
 	// Loop over charts in index.yaml
 	// Loop over charts in index.yaml
@@ -47,25 +116,20 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 
 
 		// Unpack the target chart and retrieve values.yaml
 		// Unpack the target chart and retrieve values.yaml
-		if strAcc == tgt {
+		if strAcc == templateName {
 			tgtURL := baseURL + tarURL
 			tgtURL := baseURL + tarURL
 			values, err := processValues(tgtURL)
 			values, err := processValues(tgtURL)
 			if err != nil {
 			if err != nil {
 				fmt.Println(err)
 				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)
 	resp, err := http.Get(tgtURL)
 	if err != nil {
 	if err != nil {
 		fmt.Println(err)
 		fmt.Println(err)
@@ -110,7 +174,7 @@ func processValues(tgtURL string) (*map[string]interface{}, error) {
 				}
 				}
 
 
 				// Unmarshal yaml byte buffer
 				// Unmarshal yaml byte buffer
-				form := make(map[string]interface{})
+				form := make(map[interface{}]interface{})
 				if err := yaml.Unmarshal(bufForm.Bytes(), &form); err != nil {
 				if err := yaml.Unmarshal(bufForm.Bytes(), &form); err != nil {
 					fmt.Println(err)
 					fmt.Println(err)
 					return nil, err
 					return nil, err