Jelajahi Sumber

Merge branch 'master' of https://github.com/porter-dev/porter

sunguroku 5 tahun lalu
induk
melakukan
8fbe0b1b9a
37 mengubah file dengan 1741 tambahan dan 1292 penghapusan
  1. 118 0
      cli/cmd/test.go
  2. 0 2
      dashboard/src/components/values-form/ValuesForm.tsx
  3. 1 18
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  4. 32 14
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  5. 7 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  6. 11 14
      dashboard/src/main/home/templates/Templates.tsx
  7. 62 10
      dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx
  8. 20 10
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  9. 14 13
      dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx
  10. 13 3
      dashboard/src/shared/api.tsx
  11. 6 7
      dashboard/src/shared/types.tsx
  12. 1 0
      go.sum
  13. 22 0
      internal/forms/chart.go
  14. 45 34
      internal/helm/agent.go
  15. 6 1
      internal/helm/config.go
  16. 89 0
      internal/helm/loader/loader.go
  17. 18 0
      internal/kubernetes/config.go
  18. 53 46
      internal/models/templates.go
  19. 180 0
      internal/templater/dynamic/reader.go
  20. 101 0
      internal/templater/dynamic/writer.go
  21. 35 0
      internal/templater/form.go
  22. 77 0
      internal/templater/helm/manifests_reader.go
  23. 63 0
      internal/templater/helm/values_reader.go
  24. 69 0
      internal/templater/helm/writer.go
  25. 219 0
      internal/templater/parser/parser.go
  26. 59 0
      internal/templater/utils/query.go
  27. 89 0
      internal/templater/utils/values.go
  28. 21 0
      node_modules/@types/random-words/LICENSE
  29. 16 0
      node_modules/@types/random-words/README.md
  30. 34 0
      node_modules/@types/random-words/index.d.ts
  31. 54 0
      node_modules/@types/random-words/package.json
  32. 1 844
      package-lock.json
  33. 33 137
      server/api/deploy_handler.go
  34. 61 0
      server/api/release_handler.go
  35. 55 100
      server/api/template_handler.go
  36. 46 36
      server/api/template_handler_test.go
  37. 10 2
      server/router/router.go

+ 118 - 0
cli/cmd/test.go

@@ -0,0 +1,118 @@
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/spf13/cobra"
+)
+
+var testCmd = &cobra.Command{
+	Use:   "test",
+	Short: "Testing",
+	Run: func(cmd *cobra.Command, args []string) {
+		// chart, err := loader.LoadChart("https://porter-dev.github.io/chart-repo", "docker", "0.0.1")
+
+		// if err != nil {
+		// 	red := color.New(color.FgRed)
+		// 	red.Println("Error running test:", err.Error())
+		// 	os.Exit(1)
+		// }
+
+		// bytes, err := yaml.Marshal(chart)
+
+		// if err != nil {
+		// 	red := color.New(color.FgRed)
+		// 	red.Println("Error running test:", err.Error())
+		// 	os.Exit(1)
+		// }
+
+		// fmt.Println(string(bytes))
+
+		form := &models.FormYAML{
+			Tabs: []*models.FormTab{
+				&models.FormTab{
+					Context: &models.FormContext{
+						Type: "helm/values",
+					},
+					Name:  "main",
+					Label: "Main Settings",
+					Sections: []*models.FormSection{
+						&models.FormSection{
+							Name: "section_one",
+							Contents: []*models.FormContent{
+								&models.FormContent{
+									Type:  "number-input",
+									Value: "service.targetPort",
+									Label: "Target Port",
+									Settings: struct {
+										Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
+										Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
+									}{
+										Default: 8000,
+									},
+								},
+							},
+						},
+					},
+				},
+				&models.FormTab{
+					Context: &models.FormContext{
+						Type: "cluster",
+					},
+					Name:  "crd",
+					Label: "CRDs",
+					Sections: []*models.FormSection{
+						&models.FormSection{
+							Name: "section_one",
+							Contents: []*models.FormContent{
+								&models.FormContent{
+									Type:  "resourcelist",
+									Value: `[{"name": "resource_1"}]`,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		bytes, err := json.Marshal(form)
+
+		if err != nil {
+			red := color.New(color.FgRed)
+			red.Println("Error running test:", err.Error())
+			os.Exit(1)
+		}
+
+		fmt.Println(string(bytes))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(testCmd)
+}
+
+// // FormSection is a section of a form
+// type FormSection struct {
+// 	Context  *FormContext   `yaml:"context" json:"context"`
+// 	Name     string         `yaml:"name" json:"name"`
+// 	ShowIf   string         `yaml:"show_if" json:"show_if"`
+// 	Contents []*FormContent `yaml:"contents" json:"contents,omitempty"`
+// }
+
+// // FormContent is a form's atomic unit
+// type FormContent struct {
+// 	Context  *FormContext `yaml:"context" json:"context"`
+// 	Type     string       `yaml:"type" json:"type"`
+// 	Label    string       `yaml:"label" json:"label"`
+// 	Name     string       `yaml:"name,omitempty" json:"name,omitempty"`
+// 	Value    interface{}  `yaml:"value,omitempty" json:"value,omitempty"`
+// 	Settings struct {
+// 		Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
+// 		Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
+// 	} `yaml:"settings,omitempty" json:"settings,omitempty"`
+// }

+ 0 - 2
dashboard/src/components/values-form/ValuesForm.tsx

@@ -62,7 +62,6 @@ export default class ValuesForm extends Component<PropsType, StateType> {
 
   // Initialize corresponding state fields for form blocks
   componentDidMount() {
-    console.log(this.props.sections)
     this.updateFormState();
   }
 
@@ -149,7 +148,6 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   }
 
   render() {
-    console.log('save values status', this.props.saveValuesStatus)
     return (
       <Wrapper>
         <StyledValuesForm>

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

@@ -221,15 +221,6 @@ const Button = styled.div`
   }
 `;
 
-const ButtonStack = styled(Button)`
-  min-width: 119px;
-  max-width: 119px;
-  background: #616FEEcc;
-  :hover {
-    background: #505edddd;
-  }
-`;
-
 const ButtonAlt = styled(Button)`
   min-width: 150px;
   max-width: 150px;
@@ -240,11 +231,6 @@ const ButtonAlt = styled(Button)`
   }
 `;
 
-const ConfigButtonAlt = styled(ButtonAlt)`
-  min-width: 166px;
-  max-width: 166px;
-`;
-
 const LineBreak = styled.div`
   width: calc(100% - 180px);
   height: 2px;
@@ -252,10 +238,6 @@ const LineBreak = styled.div`
   margin: 10px 80px 35px;
 `;
 
-const ServiceSection = styled.div`
-  padding-bottom: 150px;
-`;
-
 const Overlay = styled.div`
   height: 100%;
   width: 100%;
@@ -282,6 +264,7 @@ const DashboardImage = styled.img`
 const DashboardIcon = styled.div`
   position: relative;
   height: 45px;
+  min-width: 45px;
   width: 45px;
   border-radius: 5px;
   display: flex;

+ 32 - 14
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,7 +1,6 @@
 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 _ from 'lodash';
 
@@ -37,6 +36,7 @@ type StateType = {
   tabContents: any,
   currentTab: string | null,
   saveValuesStatus: string | null,
+  forceRefreshRevisions: boolean, // Update revisions after upgrading values
 };
 
 /*
@@ -58,6 +58,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     tabContents: [] as any,
     currentTab: null as string | null,
     saveValuesStatus: null as (string | null),
+    forceRefreshRevisions: false,
   }
 
   updateResources = () => {
@@ -93,14 +94,26 @@ 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;
+    return new Promise(resolve => {
+      let { files } = this.props.currentChart.chart;
+      for (let file of files) { 
+        if (file.name === 'form.yaml') {
+          let chartName = this.props.currentChart.chart.metadata.name;
+          api.getTemplateInfo('<token>', {}, {
+            name: chartName.toLowerCase().trim(),
+            version: 'latest',
+          }, (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+              resolve(null);
+            } else {
+              let { form } = res.data;
+              resolve(form);
+            }
+          });
+        }
+      };
+    });
   }
 
   upgradeValues = (rawValues: any) => {
@@ -130,8 +143,10 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         setCurrentError(err);
         this.setState({ saveValuesStatus: 'error' });
       } else {
-        this.setState({ saveValuesStatus: 'successful' });
-        this.props.refreshChart();
+        this.setState({ 
+          saveValuesStatus: 'successful',
+          forceRefreshRevisions: true, 
+        });
       }
     });
   }
@@ -202,7 +217,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
             if (tab.value === currentTab) {
               
               return (
-                <ValuesFormWrapper>
+                <ValuesFormWrapper key={i}>
                   <ValuesForm 
                     sections={tab.sections} 
                     onSubmit={this.upgradeValues}
@@ -217,8 +232,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     }
   }
 
-  updateTabs = () => {
-    let formData = this.getFormData();
+  async updateTabs() {
+    let formData = await this.getFormData();
+    console.log(formData);
     let tabOptions = [] as any[];
 
     // Generate form tabs if form.yaml exists
@@ -348,6 +364,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               chart={chart}
               refreshChart={refreshChart}
               setRevisionPreview={this.setRevisionPreview}
+              forceRefreshRevisions={this.state.forceRefreshRevisions}
+              refreshRevisionsOff={() => this.setState({ forceRefreshRevisions: false })}
             />
           </HeaderWrapper>
 

+ 7 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -14,6 +14,8 @@ type PropsType = {
   chart: ChartType,
   refreshChart: () => void,
   setRevisionPreview: (preview: ChartType) => void
+  forceRefreshRevisions: boolean,
+  refreshRevisionsOff: () => void,
 };
 
 type StateType = {
@@ -55,7 +57,11 @@ export default class RevisionSection extends Component<PropsType, StateType> {
 
   // Handle update of values.yaml
   componentDidUpdate(prevProps: PropsType) {
-    if (this.props.chart !== prevProps.chart) {
+    if (this.props.forceRefreshRevisions) {
+      this.props.refreshRevisionsOff();
+      this.props.refreshChart();
+      this.refreshHistory();
+    } else if (this.props.chart !== prevProps.chart) {
       this.refreshHistory();
     }
   }

+ 11 - 14
dashboard/src/main/home/templates/Templates.tsx

@@ -3,7 +3,7 @@ import styled from 'styled-components';
 
 import { Context } from '../../../shared/Context';
 import api from '../../../shared/api';
-import { PorterChart } from '../../../shared/types';
+import { PorterTemplate } from '../../../shared/types';
 
 import TabSelector from '../../../components/TabSelector';
 import ExpandedTemplate from './expanded-template/ExpandedTemplate';
@@ -18,31 +18,28 @@ type PropsType = {
 };
 
 type StateType = {
-  currentTemplate: PorterChart | null,
+  currentTemplate: PorterTemplate | null,
   currentTab: string,
-  porterCharts: PorterChart[],
+  PorterTemplates: PorterTemplate[],
   loading: boolean,
   error: boolean
 };
 
 export default class Templates extends Component<PropsType, StateType> {
   state = {
-    currentTemplate: null as (PorterChart | null),
+    currentTemplate: null as (PorterTemplate | null),
     currentTab: 'community',
-    porterCharts: [] as PorterChart[],
+    PorterTemplates: [] as PorterTemplate[],
     loading: true,
     error: false,
   }
 
   componentDidMount() {
-
-    // Get templates
     api.getTemplates('<token>', {}, {}, (err: any, res: any) => {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
-        console.log(res.data)
-        this.setState({ porterCharts: res.data, loading: false, error: false });
+        this.setState({ PorterTemplates: res.data, loading: false, error: false });
       }
     });
   }
@@ -58,7 +55,7 @@ export default class Templates extends Component<PropsType, StateType> {
   }
 
   renderTemplateList = () => {
-    let { loading, error, porterCharts } = this.state;
+    let { loading, error, PorterTemplates } = this.state;
 
     if (loading) {
       return <LoadingWrapper><Loading /></LoadingWrapper>
@@ -68,7 +65,7 @@ export default class Templates extends Component<PropsType, StateType> {
           <i className="material-icons">error</i> Error retrieving templates.
         </Placeholder>
       );
-    } else if (porterCharts.length === 0) {
+    } else if (PorterTemplates.length === 0) {
       return (
         <Placeholder>
           <i className="material-icons">category</i> No templates found.
@@ -76,8 +73,8 @@ export default class Templates extends Component<PropsType, StateType> {
       );
     }
 
-    return this.state.porterCharts.map((template: PorterChart, i: number) => {
-      let { name, icon, description } = template.form;
+    return this.state.PorterTemplates.map((template: PorterTemplate, i: number) => {
+      let { name, icon, description } = template;
       return (
         <TemplateBlock key={i} onClick={() => this.setState({ currentTemplate: template })}>
           {icon ? this.renderIcon(icon) : this.renderIcon(template.icon)}
@@ -97,7 +94,7 @@ export default class Templates extends Component<PropsType, StateType> {
       return (
         <ExpandedTemplate
           currentTemplate={this.state.currentTemplate}
-          setCurrentTemplate={(currentTemplate: PorterChart) => this.setState({ currentTemplate })}
+          setCurrentTemplate={(currentTemplate: PorterTemplate) => this.setState({ currentTemplate })}
           setCurrentView={this.props.setCurrentView}
         />
       );

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

@@ -1,43 +1,82 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
-import { PorterChart } from '../../../../shared/types';
+import { PorterTemplate } from '../../../../shared/types';
+import api from '../../../../shared/api';
 
 import TemplateInfo from './TemplateInfo';
 import LaunchTemplate from './LaunchTemplate';
+import Loading from '../../../../components/Loading';
 
 type PropsType = {
-  currentTemplate: PorterChart,
-  setCurrentTemplate: (x: PorterChart) => void,
+  currentTemplate: PorterTemplate,
+  setCurrentTemplate: (x: PorterTemplate) => void,
   setCurrentView: (x: string) => void,
 };
 
 type StateType = {
-  showLaunchTemplate: boolean
+  showLaunchTemplate: boolean,
+  form: any | null,
+  values: any | null,
+  loading: boolean,
+  error: boolean,
+  markdown: string | null,
+  keywords: string[],
 };
 
 export default class ExpandedTemplate extends Component<PropsType, StateType> {
   state = {
-    showLaunchTemplate: false
+    showLaunchTemplate: false,
+    form: null as any | null,
+    values: null as any | null,
+    loading: true,
+    error: false,
+    markdown: null as string | null,
+    keywords: [] as string[],
+  }
+
+  componentDidMount() {
+    this.setState({ loading: true });
+    api.getTemplateInfo('<token>', {}, {
+      name: this.props.currentTemplate.name.toLowerCase().trim(),
+      version: 'latest',
+    }, (err: any, res: any) => {
+      if (err) {
+        this.setState({ loading: false, error: true });
+      } else {
+        let { form, values, markdown, metadata } = res.data;
+        let keywords = metadata.keywords;
+        this.setState({ form, values, markdown, keywords, loading: false, error: false });
+      }
+    });
   }
 
   renderContents = () => {
+      if (this.state.loading) {
+        return <LoadingWrapper><Loading /></LoadingWrapper>;
+      }
     if (this.state.showLaunchTemplate) {
       return (
         <LaunchTemplate
           currentTemplate={this.props.currentTemplate}
           hideLaunch={() => this.setState({ showLaunchTemplate: false })}
           setCurrentView={this.props.setCurrentView}
+          values={this.state.values}
+          form={this.state.form}
         />
       );
     }
 
     return (
-      <TemplateInfo
-        currentTemplate={this.props.currentTemplate}
-        setCurrentTemplate={this.props.setCurrentTemplate}
-        launchTemplate={() => this.setState({ showLaunchTemplate: true })}
-      />
+      <FadeWrapper>
+        <TemplateInfo
+          currentTemplate={this.props.currentTemplate}
+          setCurrentTemplate={this.props.setCurrentTemplate}
+          launchTemplate={() => this.setState({ showLaunchTemplate: true })}
+          markdown={this.state.markdown}
+          keywords={this.state.keywords}
+        />
+      </FadeWrapper>
     );
   }
 
@@ -50,6 +89,19 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
   }
 }
 
+const FadeWrapper = styled.div`
+  animation: fadeIn 0.2s;
+  @keyframes fadeIn {
+    from: { opacity: 0 }
+    to: { opacity: 1 }
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  height: calc(100vh - 200px);
+  width: 100%;
+`;
+
 const StyledExpandedTemplate = styled.div`
   width: calc(90% - 150px);
   min-width: 300px;

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

@@ -1,19 +1,22 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 import randomWords from 'random-words';
+import _ from 'lodash';
 import { Context } from '../../../../shared/Context';
 import api from '../../../../shared/api';
 
-import { PorterChart, ChoiceType, Cluster, StorageType } from '../../../../shared/types';
+import { PorterTemplate, ChoiceType, Cluster, StorageType } from '../../../../shared/types';
 import Selector from '../../../../components/Selector';
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 
 type PropsType = {
-  currentTemplate: PorterChart,
+  currentTemplate: any,
   hideLaunch: () => void,
   setCurrentView: (x: string) => void,
+  values: any,
+  form: any,
 };
 
 type StateType = {
@@ -45,21 +48,29 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     namespaceOptions: [] as { label: string, value: string }[],
   };
 
-  onSubmit = (formValues: any) => {
+  onSubmit = (rawValues: any) => {
     let { currentCluster, currentProject } = this.context;
-    let name = randomWords({ exactly: 3, join: '-' })
+    let name = randomWords({ exactly: 3, join: '-' });
     this.setState({ saveValuesStatus: 'loading' });
 
+    // Convert dotted keys to nested objects
+    let values = {};
+    for (let key in rawValues) {
+      _.set(values, key, rawValues[key]);
+    }
+
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
       imageURL: this.state.selectedImageUrl,
       storage: StorageType.Secret,
-      formValues,
+      formValues: values,
       namespace: this.state.selectedNamespace,
       name,
     }, {
       id: currentProject.id,
       cluster_id: currentCluster.id,
+      name: this.props.currentTemplate.name.toLowerCase().trim(),
+      version: 'latest',
     }, (err: any, res: any) => {
       if (err) {
         this.setState({ saveValuesStatus: 'error' });
@@ -70,12 +81,12 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   renderTabContents = () => {
-    return this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
+    return this.props.form.tabs.map((tab: any, i: number) => {
 
       // If tab is current, render
       if (tab.name === this.state.currentTab) {
         return (
-          <ValuesFormWrapper>
+          <ValuesFormWrapper key={i}>
             <ValuesForm 
               key={tab.name}
               sections={tab.sections} 
@@ -94,7 +105,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
-    this.props.currentTemplate.form.tabs.map((tab: any, i: number) => {
+    this.props.form.tabs.map((tab: any, i: number) => {
       tabOptions.push({ value: tab.name, label: tab.label });
     });
     this.setState({ tabOptions });
@@ -105,7 +116,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       if (err) {
         // console.log(err)
       } else if (res.data) {
-        console.log(res.data)
         let clusterOptions = res.data.map((x: Cluster) => { return { label: x.name, value: x.name } });
         if (res.data.length > 0) {
           this.setState({ clusterOptions });
@@ -140,7 +150,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   render() {
-    let { name, icon, description } = this.props.currentTemplate.form;
+    let { name, icon, description } = this.props.form;
     let { currentTemplate } = this.props;
     name = name ? name : currentTemplate.name;
 

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

@@ -4,13 +4,17 @@ import launch from '../../../../assets/launch.svg';
 import Markdown from 'markdown-to-jsx';
 
 import { Context } from '../../../../shared/Context';
+import api from '../../../../shared/api';
+import Loading from '../../../../components/Loading';
 
-import { PorterChart } from '../../../../shared/types';
+import { PorterTemplate } from '../../../../shared/types';
 
 type PropsType = {
-  currentTemplate: PorterChart,
-  setCurrentTemplate: (x: PorterChart) => void,
-  launchTemplate: () => void
+  currentTemplate: any,
+  setCurrentTemplate: (x: PorterTemplate) => void,
+  launchTemplate: () => void,
+  markdown: string | null,
+  keywords: string[],
 };
 
 type StateType = {
@@ -28,7 +32,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
   }
 
   renderTagList = () => {
-    return this.props.currentTemplate.form.tags.map((tag: string, i: number) => {
+    return this.props.keywords.map((tag: string, i: number) => {
       return (
         <Tag key={i}>{tag}</Tag>
       )
@@ -36,20 +40,17 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
   }
 
   renderMarkdown = () => {
-    let { currentTemplate } = this.props;
-    if (currentTemplate.markdown) {
+    let { currentTemplate, markdown } = this.props;
+    if (markdown) {
       return (
-        <Markdown>{currentTemplate.markdown}</Markdown>
+        <Markdown>{markdown}</Markdown>
       );
-    } else if (currentTemplate.form.description) {
-      return currentTemplate.form.description;
     }
-
     return currentTemplate.description;
   }
 
   renderTagSection = () => {
-    if (this.props.currentTemplate.form.tags) {
+    if (this.props.keywords.length > 0) {
       return (
         <TagSection>
           <i className="material-icons">local_offer</i>
@@ -61,7 +62,7 @@ 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;
     let { currentTemplate } = this.props;
     name = name ? name : currentTemplate.name;
     return (

+ 13 - 3
dashboard/src/shared/api.tsx

@@ -125,6 +125,10 @@ const upgradeChartValues = baseApi<{
 
 const getTemplates = baseApi('GET', '/api/templates');
 
+const getTemplateInfo = baseApi<{}, { name: string, version: string }>('GET', pathParams => {
+  return `/api/templates/${pathParams.name}/${pathParams.version}`;
+});
+
 const getRepos = baseApi<{}, { id: number }>('GET', pathParams => {
   return `/api/projects/${pathParams.id}/repos`;
 });
@@ -160,9 +164,14 @@ const deployTemplate = baseApi<{
   storage: StorageType,
   namespace: string,
   name: string,
-}, { id: number, cluster_id: number }>('POST', pathParams => {
-  let { cluster_id, id } = pathParams;
-  return `/api/projects/${id}/deploy?cluster_id=${cluster_id}`;
+}, { 
+  id: number,
+  cluster_id: number, 
+  name: string, 
+  version: string 
+}>('POST', pathParams => {
+  let { cluster_id, id, name, version } = pathParams;
+  return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}`;
 });
 
 const getClusterIntegrations = baseApi('GET', '/api/integrations/cluster');
@@ -234,6 +243,7 @@ export default {
   rollbackChart,
   upgradeChartValues,
   getTemplates,
+  getTemplateInfo,
   getBranches,
   getBranchContents,
   getProjects,

+ 6 - 7
dashboard/src/shared/types.tsx

@@ -66,13 +66,12 @@ export enum StorageType {
   Memory = 'memory'
 }
 
-// PorterChart represents a bundled Porter template
-export interface PorterChart {
-	name: string,
-	description: string,
-	icon: string,
-  form: FormYAML,
-  markdown?: string,
+// PorterTemplate represents a bundled Porter template
+export interface PorterTemplate {
+  name: string,
+  version: string,
+  description: string,
+  icon: string,
 }
 
 // FormYAML represents a chart's values.yaml form abstraction

+ 1 - 0
go.sum

@@ -1942,6 +1942,7 @@ k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949Koz
 k8s.io/helm v2.9.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
 k8s.io/helm v2.16.12+incompatible h1:K2zhF8+B85Ya1n7n3eH34xwwp5qNUM42TBFENDZJT7w=
 k8s.io/helm v2.16.12+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
+k8s.io/helm v2.17.0+incompatible h1:Bpn6o1wKLYqKM3+Osh8e+1/K2g/GsQJ4F4yNF2+deao=
 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=

+ 22 - 0
internal/forms/chart.go

@@ -0,0 +1,22 @@
+package forms
+
+import "net/url"
+
+// ChartForm is the base type for CRUD operations on charts
+type ChartForm struct {
+	RepoURL string
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+// PopulateRepoURLFromQueryParams populates the repo url in the ChartForm using the passed
+// url.Values (the parsed query params)
+func (cf *ChartForm) PopulateRepoURLFromQueryParams(
+	vals url.Values,
+) error {
+	if repoURL, ok := vals["repo_url"]; ok && len(repoURL) == 1 {
+		cf.RepoURL = repoURL[0]
+	}
+
+	return nil
+}

+ 45 - 34
internal/helm/agent.go

@@ -6,7 +6,6 @@ import (
 	"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"
 )
@@ -54,6 +53,20 @@ func (a *Agent) GetReleaseHistory(
 func (a *Agent) UpgradeRelease(
 	name string,
 	values string,
+) (*release.Release, error) {
+	valuesYaml, err := chartutil.ReadValues([]byte(values))
+
+	if err != nil {
+		return nil, fmt.Errorf("Values could not be parsed: %v", err)
+	}
+
+	return a.UpgradeReleaseByValues(name, valuesYaml)
+}
+
+// UpgradeReleaseByValues upgrades a release by unmarshaled yaml values
+func (a *Agent) UpgradeReleaseByValues(
+	name string,
+	values map[string]interface{},
 ) (*release.Release, error) {
 	// grab the latest release
 	rel, err := a.GetRelease(name, 0)
@@ -65,13 +78,7 @@ func (a *Agent) UpgradeRelease(
 	ch := rel.Chart
 
 	cmd := action.NewUpgrade(a.ActionConfig)
-	valuesYaml, err := chartutil.ReadValues([]byte(values))
-
-	if err != nil {
-		return nil, fmt.Errorf("Values could not be parsed: %v", err)
-	}
-
-	res, err := cmd.Run(name, ch, valuesYaml)
+	res, err := cmd.Run(name, ch, values)
 
 	if err != nil {
 		return nil, fmt.Errorf("Upgrade failed: %v", err)
@@ -80,40 +87,44 @@ 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,
+// InstallChartConfig is the config required to install a chart
+type InstallChartConfig struct {
+	Chart     *chart.Chart
+	Name      string
+	Namespace string
+	Values    map[string]interface{}
+}
+
+// InstallChartFromValuesBytes reads the raw values and calls Agent.InstallChart
+func (a *Agent) InstallChartFromValuesBytes(
+	conf *InstallChartConfig,
 	values []byte,
-	name string,
-	namespace string,
 ) (*release.Release, error) {
-	client := action.NewInstall(a.ActionConfig)
-
-	if client.Version == "" && client.Devel {
-		client.Version = ">0.0.0-0"
-	}
-
-	client.ReleaseName = name
-	client.Namespace = namespace
 	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)
+	conf.Values = valuesYaml
 
-	if err != nil {
-		return nil, err
+	return a.InstallChart(conf)
+}
+
+// InstallChart installs a new chart
+func (a *Agent) InstallChart(
+	conf *InstallChartConfig,
+) (*release.Release, error) {
+	cmd := action.NewInstall(a.ActionConfig)
+
+	if cmd.Version == "" && cmd.Devel {
+		cmd.Version = ">0.0.0-0"
 	}
 
-	if err := checkIfInstallable(chartRequested); err != nil {
+	cmd.ReleaseName = conf.Name
+	cmd.Namespace = conf.Namespace
+
+	if err := checkIfInstallable(conf.Chart); err != nil {
 		return nil, err
 	}
 
@@ -121,14 +132,14 @@ func (a *Agent) InstallChart(
 	// 	return nil, fmt.Errorf("This chart is deprecated")
 	// }
 
-	if req := chartRequested.Metadata.Dependencies; req != nil {
-		if err := action.CheckDependencies(chartRequested, req); err != nil {
+	if req := conf.Chart.Metadata.Dependencies; req != nil {
+		if err := action.CheckDependencies(conf.Chart, req); err != nil {
 			// TODO: Handle dependency updates.
 			return nil, err
 		}
 	}
 
-	return client.Run(chartRequested, valuesYaml)
+	return cmd.Run(conf.Chart, conf.Values)
 }
 
 // RollbackRelease rolls a release back to a specified revision/version

+ 6 - 1
internal/helm/config.go

@@ -40,6 +40,11 @@ func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 		return nil, err
 	}
 
+	return GetAgentFromK8sAgent(form.Storage, form.Namespace, l, k8sAgent)
+}
+
+// GetAgentFromK8sAgent creates a new Agent
+func GetAgentFromK8sAgent(stg string, ns string, l *logger.Logger, k8sAgent *kubernetes.Agent) (*Agent, error) {
 	clientset, ok := k8sAgent.Clientset.(*k8s.Clientset)
 
 	if !ok {
@@ -50,7 +55,7 @@ func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	return &Agent{&action.Configuration{
 		RESTClientGetter: k8sAgent.RESTClientGetter,
 		KubeClient:       kube.New(k8sAgent.RESTClientGetter),
-		Releases:         StorageMap[form.Storage](l, clientset.CoreV1(), form.Namespace),
+		Releases:         StorageMap[stg](l, clientset.CoreV1(), ns),
 		Log:              l.Printf,
 	}}, nil
 }

+ 89 - 0
internal/helm/loader/loader.go

@@ -0,0 +1,89 @@
+package loader
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	"k8s.io/helm/pkg/repo"
+	"sigs.k8s.io/yaml"
+
+	"helm.sh/helm/v3/pkg/chart"
+	chartloader "helm.sh/helm/v3/pkg/chart/loader"
+)
+
+// LoadRepoIndex loads an index file from a remote Helm repo
+func LoadRepoIndex(indexURL string) (*repo.IndexFile, error) {
+	resp, err := http.Get(indexURL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	data, err := ioutil.ReadAll(resp.Body)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// index not found in the cache, parse it
+	index := &repo.IndexFile{}
+	err = yaml.Unmarshal(data, index)
+
+	if err != nil {
+		return index, err
+	}
+
+	index.SortEntries()
+
+	return index, nil
+}
+
+// LoadChart returns a Helm3 (v2) chart from a remote repo. If chartVersion is an
+// empty string, the most stable latest version is found.
+//
+// TODO: this is an expensive operation, so after retrieving the digest from the
+// repo index, this should check the digest in the cache
+func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
+	repoIndex, err := LoadRepoIndex(trimmedRepoURL + "/index.yaml")
+
+	if err != nil {
+		return nil, err
+	}
+
+	cv, err := repoIndex.Get(chartName, chartVersion)
+
+	if err != nil {
+		return nil, err
+	} else if len(cv.URLs) == 0 {
+		return nil, fmt.Errorf("%s:%s no valid download urls", chartName, chartVersion)
+	}
+
+	chartURL := trimmedRepoURL + "/" + strings.TrimPrefix(cv.URLs[0], "/")
+
+	fmt.Println(chartURL)
+
+	// download tgz
+	resp, err := http.Get(chartURL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	data, err := ioutil.ReadAll(resp.Body)
+
+	// fmt.Println("DATA IS", string(data))
+
+	if err != nil {
+		return nil, err
+	}
+
+	return chartloader.LoadArchive(bytes.NewReader(data))
+}

+ 18 - 0
internal/kubernetes/config.go

@@ -14,6 +14,7 @@ import (
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/discovery"
 	diskcached "k8s.io/client-go/discovery/cached/disk"
+	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/fake"
 	"k8s.io/client-go/rest"
@@ -28,6 +29,23 @@ import (
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
 )
 
+// GetDynamicClientOutOfClusterConfig creates a new dynamic client using the OutOfClusterConfig
+func GetDynamicClientOutOfClusterConfig(conf *OutOfClusterConfig) (dynamic.Interface, error) {
+	restConf, err := conf.ToRESTConfig()
+
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := dynamic.NewForConfig(restConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return client, nil
+}
+
 // GetAgentOutOfClusterConfig creates a new Agent using the OutOfClusterConfig
 func GetAgentOutOfClusterConfig(conf *OutOfClusterConfig) (*Agent, error) {
 	restConf, err := conf.ToRESTConfig()

+ 53 - 46
internal/models/templates.go

@@ -1,57 +1,64 @@
 package models
 
-// IndexYAML represents a chart repo's index.yaml
-type IndexYAML struct {
-	APIVersion string                    `yaml:"apiVersion"`
-	Generated  string                    `yaml:"generated"`
-	Entries    map[interface{}]ChartYAML `yaml:"entries"`
+import "helm.sh/helm/v3/pkg/chart"
+
+// PorterChartList is how a chart gets displayed when listed
+type PorterChartList struct {
+	Name        string `json:"name"`
+	Version     string `json:"version"`
+	Description string `json:"description"`
+	Icon        string `json:"icon"`
+}
+
+// PorterChartRead is a chart with detailed information and a form for reading
+type PorterChartRead struct {
+	Markdown string                 `json:"markdown"`
+	Metadata *chart.Metadata        `json:"metadata"`
+	Values   map[string]interface{} `json:"values"`
+	Form     *FormYAML              `json:"form"`
+}
+
+// FormContext is the target context
+type FormContext struct {
+	Type   string            `yaml:"type" json:"type"`
+	Config map[string]string `yaml:"config" json:"config"`
+}
+
+// FormTab is a tab rendered in a form
+type FormTab struct {
+	Context  *FormContext   `yaml:"context" json:"context"`
+	Name     string         `yaml:"name" json:"name"`
+	Label    string         `yaml:"label" json:"label"`
+	Sections []*FormSection `yaml:"sections" json:"sections,omitempty"`
 }
 
-// ChartYAML represents the data for chart in index.yaml
-type ChartYAML []struct {
-	APIVersion  string   `yaml:"apiVersion"`
-	AppVersion  string   `yaml:"appVersion"`
-	Created     string   `yaml:"created"`
-	Description string   `yaml:"description"`
-	Digest      string   `yaml:"digest"`
-	Icon        string   `yaml:"icon"`
-	Name        string   `yaml:"name"`
-	Type        string   `yaml:"type"`
-	Urls        []string `yaml:"urls"`
-	Version     string   `yaml:"version"`
+// FormSection is a section of a form
+type FormSection struct {
+	Context  *FormContext   `yaml:"context" json:"context"`
+	Name     string         `yaml:"name" json:"name"`
+	ShowIf   string         `yaml:"show_if" json:"show_if"`
+	Contents []*FormContent `yaml:"contents" json:"contents,omitempty"`
 }
 
-// PorterChart represents a bundled Porter template
-type PorterChart struct {
-	Name        string   `json:"name"`
-	Description string   `json:"description"`
-	Icon        string   `json:"icon"`
-	Form        FormYAML `json:"form"`
-	Markdown    string   `json:"markdown"`
+// FormContent is a form's atomic unit
+type FormContent struct {
+	Context  *FormContext `yaml:"context" json:"context"`
+	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"`
+	Value    interface{}  `yaml:"value,omitempty" json:"value,omitempty"`
+	Settings struct {
+		Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
+		Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
+	} `yaml:"settings,omitempty" json:"settings,omitempty"`
 }
 
 // FormYAML represents a chart's values.yaml form abstraction
 type FormYAML struct {
-	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" json:"name"`
-		Label    string `yaml:"label" json:"label"`
-		Sections []struct {
-			Name     string `yaml:"name" json:"name"`
-			ShowIf   string `yaml:"show_if" json:"show_if"`
-			Contents []struct {
-				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:"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"`
+	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        []*FormTab `yaml:"tabs" json:"tabs,omitempty"`
 }

+ 180 - 0
internal/templater/dynamic/reader.go

@@ -0,0 +1,180 @@
+package dynamic
+
+import (
+	"context"
+	"time"
+
+	"github.com/porter-dev/porter/internal/templater/utils"
+
+	"github.com/porter-dev/porter/internal/templater"
+	"k8s.io/client-go/dynamic"
+	di "k8s.io/client-go/dynamic/dynamicinformer"
+	"k8s.io/client-go/tools/cache"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// Object identifies a set of k8s objects, during Group-Version-Kind, and optionally
+// a namespace and name to isolate a single object
+type Object struct {
+	Group    string
+	Version  string
+	Resource string
+
+	// Optional, if resource is namespacable
+	Namespace string
+
+	// Optional, if attempting to get an object by name
+	Name string
+}
+
+// DynamicTemplateReader reads any resource registered with the k8s apiserver
+type DynamicTemplateReader struct {
+	// The object to read from, identified by its group-version-kind
+	Object *Object
+
+	// The set of queries to execute, identified by a key and query string
+	Queries []*templater.TemplateReaderQuery
+
+	Client dynamic.Interface
+
+	// The resource that's being queried
+	gvr      schema.GroupVersionResource
+	resource dynamic.ResourceInterface
+}
+
+// NewDynamicTemplateReader creates a new DynamicTemplateReader
+func NewDynamicTemplateReader(client dynamic.Interface, obj *Object) templater.TemplateReader {
+	r := &DynamicTemplateReader{
+		Object: obj,
+		Client: client,
+	}
+
+	objRes := schema.GroupVersionResource{
+		Group:    r.Object.Group,
+		Version:  r.Object.Version,
+		Resource: r.Object.Resource,
+	}
+
+	r.gvr = objRes
+
+	r.resource = r.Client.Resource(objRes).Namespace(r.Object.Namespace)
+
+	return r
+}
+
+// ValuesFromTarget retrieves cluster values from the k8s apiserver
+func (r *DynamicTemplateReader) ValuesFromTarget() (map[string]interface{}, error) {
+	// if name is not empty, this is a get operation
+	if r.Object.Name != "" {
+		return r.valuesFromGet()
+	}
+
+	return r.valuesFromList()
+}
+
+// RegisterQuery adds a query to the list of queries to execute
+func (r *DynamicTemplateReader) RegisterQuery(query *templater.TemplateReaderQuery) error {
+	r.Queries = append(r.Queries, query)
+
+	return nil
+}
+
+// Read returns the resulting queried data
+func (r *DynamicTemplateReader) Read() (map[string]interface{}, error) {
+	values, err := r.ValuesFromTarget()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return utils.QueryValues(values, r.Queries)
+}
+
+// ReadStream listens for CRUD 	operations on resources and returns resulting
+// queried data
+func (r *DynamicTemplateReader) ReadStream(
+	on templater.OnDataStream,
+	stopCh <-chan struct{},
+) error {
+	factory := di.NewDynamicSharedInformerFactory(
+		r.Client,
+		10*time.Second,
+	)
+
+	informer := factory.ForResource(r.gvr).Informer()
+
+	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc: func(obj interface{}) {
+			pkt := make(map[string]interface{})
+			pkt["kind"] = "create"
+
+			u := obj.(*unstructured.Unstructured)
+
+			data, err := utils.QueryValues(u.Object, r.Queries)
+
+			if err != nil {
+				return
+			}
+
+			pkt["data"] = data
+			on(pkt)
+		},
+		UpdateFunc: func(oldObj, newObj interface{}) {
+			pkt := make(map[string]interface{})
+			pkt["kind"] = "update"
+
+			u := newObj.(*unstructured.Unstructured)
+
+			data, err := utils.QueryValues(u.Object, r.Queries)
+
+			if err != nil {
+				return
+			}
+
+			pkt["data"] = data
+			on(pkt)
+		},
+		DeleteFunc: func(obj interface{}) {
+			pkt := make(map[string]interface{})
+			pkt["kind"] = "delete"
+
+			u := obj.(*unstructured.Unstructured)
+
+			data, err := utils.QueryValues(u.Object, r.Queries)
+
+			if err != nil {
+				return
+			}
+
+			pkt["data"] = data
+			on(pkt)
+		},
+	})
+
+	go informer.Run(stopCh)
+
+	return nil
+}
+
+func (r *DynamicTemplateReader) valuesFromList() (map[string]interface{}, error) {
+	list, err := r.resource.List(context.TODO(), metav1.ListOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return list.UnstructuredContent(), nil
+}
+
+func (r *DynamicTemplateReader) valuesFromGet() (map[string]interface{}, error) {
+	get, err := r.resource.Get(context.TODO(), r.Object.Name, metav1.GetOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return get.Object, nil
+}

+ 101 - 0
internal/templater/dynamic/writer.go

@@ -0,0 +1,101 @@
+package dynamic
+
+import (
+	"context"
+
+	"github.com/porter-dev/porter/internal/templater/utils"
+
+	"github.com/porter-dev/porter/internal/templater"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"k8s.io/client-go/dynamic"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type DynamicTemplateWriter struct {
+	// The object to read from, identified by its group-version-kind
+	Object *Object
+
+	Client dynamic.Interface
+
+	// The resource that's being written to
+	resource dynamic.ResourceInterface
+
+	// The values to be written
+	vals map[string]interface{}
+
+	// The base values
+	base map[string]interface{}
+}
+
+func NewDynamicTemplateWriter(
+	client dynamic.Interface,
+	obj *Object,
+	base map[string]interface{},
+) templater.TemplateWriter {
+	w := &DynamicTemplateWriter{
+		Object: obj,
+		Client: client,
+		base:   base,
+	}
+
+	objRes := schema.GroupVersionResource{
+		Group:    w.Object.Group,
+		Version:  w.Object.Version,
+		Resource: w.Object.Resource,
+	}
+
+	w.resource = w.Client.Resource(objRes).Namespace(w.Object.Namespace)
+
+	return w
+}
+
+func (w *DynamicTemplateWriter) Transform() error {
+	w.vals = utils.CoalesceValues(w.base, w.vals)
+
+	return nil
+}
+
+func (w *DynamicTemplateWriter) Write() (map[string]interface{}, error) {
+	return nil, nil
+}
+
+func (w *DynamicTemplateWriter) Create(vals map[string]interface{}) (map[string]interface{}, error) {
+	w.vals = vals
+	err := w.Transform()
+
+	if err != nil {
+		return nil, err
+	}
+
+	create, err := w.resource.Create(context.TODO(), &unstructured.Unstructured{
+		Object: w.vals,
+	}, metav1.CreateOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return create.Object, nil
+}
+
+func (w *DynamicTemplateWriter) Update(vals map[string]interface{}) (map[string]interface{}, error) {
+	w.vals = vals
+	err := w.Transform()
+
+	if err != nil {
+		return nil, err
+	}
+
+	update, err := w.resource.Update(context.TODO(), &unstructured.Unstructured{
+		Object: w.vals,
+	}, metav1.UpdateOptions{})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return update.Object, nil
+}

+ 35 - 0
internal/templater/form.go

@@ -0,0 +1,35 @@
+package templater
+
+import (
+	"k8s.io/client-go/util/jsonpath"
+)
+
+// OnDataStream is a function that gets called when new data should be
+// streamed to the client. The data has the generic type map[string]interface{}.
+type OnDataStream func(val map[string]interface{}) error
+
+type TemplateReaderQuery struct {
+	Key         string
+	QueryString string
+
+	Template *jsonpath.JSONPath
+}
+
+// TemplateReader retrieves data from a target data source, registers a set of
+// queries against that data, and returns it via the Read() operation
+type TemplateReader interface {
+	ValuesFromTarget() (map[string]interface{}, error)
+	RegisterQuery(query *TemplateReaderQuery) error
+	Read() (map[string]interface{}, error)
+	ReadStream(
+		on OnDataStream,
+		stopCh <-chan struct{},
+	) error
+}
+
+// TemplateWriter transforms input data and writes to a deployment target
+type TemplateWriter interface {
+	Transform() error
+	Create(vals map[string]interface{}) (map[string]interface{}, error)
+	Update(vals map[string]interface{}) (map[string]interface{}, error)
+}

+ 77 - 0
internal/templater/helm/manifests_reader.go

@@ -0,0 +1,77 @@
+package helm
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/templater"
+	"github.com/porter-dev/porter/internal/templater/utils"
+	"helm.sh/helm/v3/pkg/release"
+	"sigs.k8s.io/yaml"
+)
+
+// ManifestsTemplateReader implements the TemplateReader for reading from
+// the Helm manifests of a given release.
+//
+// Note: ReadStream does nothing at the moment.
+type ManifestsTemplateReader struct {
+	Queries []*templater.TemplateReaderQuery
+
+	Release *release.Release
+}
+
+// ValuesFromTarget returns a set of values by reading from the Helm release's manifest,
+// unmarshaling from the bytes
+func (r *ManifestsTemplateReader) ValuesFromTarget() (map[string]interface{}, error) {
+	if r.Release == nil {
+		return nil, fmt.Errorf("must set release to read manifest")
+	}
+
+	res := make(map[string]interface{})
+
+	manifests := strings.Split(r.Release.Manifest, "---")
+	manifestArr := make([]map[string]interface{}, 0)
+
+	for _, manifest := range manifests {
+		man := make(map[string]interface{})
+
+		err := yaml.Unmarshal([]byte(manifest), &man)
+
+		if err != nil {
+			return nil, err
+		}
+
+		manifestArr = append(manifestArr, man)
+	}
+
+	// set the array to the "manifests" field
+	res["manifests"] = manifestArr
+
+	return res, nil
+}
+
+// RegisterQuery adds a new query to be executed against the values
+func (r *ManifestsTemplateReader) RegisterQuery(query *templater.TemplateReaderQuery) error {
+	r.Queries = append(r.Queries, query)
+
+	return nil
+}
+
+// Read executes a set of queries against the helm values in the release/chart
+func (r *ManifestsTemplateReader) Read() (map[string]interface{}, error) {
+	values, err := r.ValuesFromTarget()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return utils.QueryValues(values, r.Queries)
+}
+
+// ReadStream is unimplemented: stub just to implement TemplateReader
+func (r *ManifestsTemplateReader) ReadStream(
+	on templater.OnDataStream,
+	stopCh <-chan struct{},
+) error {
+	return nil
+}

+ 63 - 0
internal/templater/helm/values_reader.go

@@ -0,0 +1,63 @@
+package helm
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/templater"
+	"github.com/porter-dev/porter/internal/templater/utils"
+
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+// ValuesTemplateReader implements the TemplateReader for reading from
+// the Helm values.
+//
+// Note: ReadStream does nothing at the moment.
+type ValuesTemplateReader struct {
+	Queries []*templater.TemplateReaderQuery
+
+	Release *release.Release
+	Chart   *chart.Chart
+}
+
+// ValuesFromTarget returns a set of values by reading from a Helm release if set, otherwise
+// a helm chart.
+func (r *ValuesTemplateReader) ValuesFromTarget() (map[string]interface{}, error) {
+	// if release exists, read values from release
+	if r.Release != nil {
+		// merge config values with overriding values
+		return utils.CoalesceValues(r.Release.Chart.Values, r.Release.Config), nil
+	} else if r.Chart != nil {
+		return r.Chart.Values, nil
+	}
+
+	// otherwise, return the chart values
+	return nil, fmt.Errorf("must set release or chart to read values")
+}
+
+// RegisterQuery adds a new query to be executed against the values
+func (r *ValuesTemplateReader) RegisterQuery(query *templater.TemplateReaderQuery) error {
+	r.Queries = append(r.Queries, query)
+
+	return nil
+}
+
+// Read executes a set of queries against the helm values in the release/chart
+func (r *ValuesTemplateReader) Read() (map[string]interface{}, error) {
+	values, err := r.ValuesFromTarget()
+
+	if err != nil {
+		return nil, err
+	}
+
+	return utils.QueryValues(values, r.Queries)
+}
+
+// ReadStream is unimplemented: stub just to implement TemplateReader
+func (r *ValuesTemplateReader) ReadStream(
+	on templater.OnDataStream,
+	stopCh <-chan struct{},
+) error {
+	return nil
+}

+ 69 - 0
internal/templater/helm/writer.go

@@ -0,0 +1,69 @@
+package helm
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/helm"
+	"helm.sh/helm/v3/pkg/chart"
+)
+
+// ValuesTemplateWriter upgrades and installs charts by setting Helm values
+type ValuesTemplateWriter struct {
+	// The object to read from, identified by its group-version-kind
+	Agent *helm.Agent
+
+	// Chart that gets installed
+	Chart *chart.Chart
+
+	// ReleaseName for upgrading the chart or installing
+	ReleaseName string
+
+	// Namespace it gets installed to
+	Namespace string
+}
+
+// Transform does nothing, since Helm handles the transforms internally
+func (w *ValuesTemplateWriter) Transform() error {
+	return nil
+}
+
+// Create installs a new chart, ChartPath must be set
+func (w *ValuesTemplateWriter) Create(
+	vals map[string]interface{},
+) (map[string]interface{}, error) {
+	if w.Chart == nil {
+		return nil, fmt.Errorf("chart must be set")
+	}
+
+	conf := &helm.InstallChartConfig{
+		Chart:     w.Chart,
+		Name:      w.ReleaseName,
+		Namespace: w.Namespace,
+		Values:    vals,
+	}
+
+	_, err := w.Agent.InstallChart(conf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return vals, nil
+}
+
+// Update upgrades a chart, ReleaseName must be set
+func (w *ValuesTemplateWriter) Update(
+	vals map[string]interface{},
+) (map[string]interface{}, error) {
+	if w.ReleaseName != "" {
+		return nil, fmt.Errorf("release not set")
+	}
+
+	_, err := w.Agent.UpgradeReleaseByValues(w.ReleaseName, vals)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return vals, nil
+}

+ 219 - 0
internal/templater/parser/parser.go

@@ -0,0 +1,219 @@
+package parser
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/templater"
+	"github.com/porter-dev/porter/internal/templater/utils"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/release"
+	"k8s.io/client-go/dynamic"
+	"sigs.k8s.io/yaml"
+
+	td "github.com/porter-dev/porter/internal/templater/dynamic"
+	th "github.com/porter-dev/porter/internal/templater/helm"
+)
+
+// TODO -- handle all continue statements, errors should at least be logged if not
+// thrown
+
+type ClientConfigDefault struct {
+	DynamicClient dynamic.Interface
+
+	HelmAgent   *helm.Agent
+	HelmRelease *release.Release
+	HelmChart   *chart.Chart
+}
+
+func FormYAMLFromBytes(def *ClientConfigDefault, bytes []byte) (*models.FormYAML, error) {
+	form, err := unqueriedFormYAMLFromBytes(bytes)
+
+	if err != nil {
+		return nil, err
+	}
+
+	lookup := formToLookupTable(def, form)
+
+	// merge data from lookup
+	data := make(map[string]interface{})
+
+	for _, lookupVal := range lookup {
+		queryRes, err := lookupVal.TemplateReader.Read()
+
+		if err != nil {
+			continue
+		}
+
+		for queryResKey, queryResVal := range queryRes {
+			data[queryResKey] = queryResVal
+		}
+	}
+
+	for i, tab := range form.Tabs {
+		for j, section := range tab.Sections {
+			for k, content := range section.Contents {
+				key := fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k)
+
+				if val, ok := data[key]; ok {
+					content.Value = val
+				}
+			}
+		}
+	}
+
+	return form, nil
+}
+
+// unqueriedFormYAMLFromBytes returns a FormYAML without values queries populated
+func unqueriedFormYAMLFromBytes(bytes []byte) (*models.FormYAML, error) {
+	// parse bytes into object
+	form := &models.FormYAML{}
+
+	err := yaml.Unmarshal(bytes, form)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// populate all context fields, with default set to helm/values with no config
+	parent := &models.FormContext{
+		Type: "helm/values",
+	}
+
+	for _, tab := range form.Tabs {
+		if tab.Context == nil {
+			tab.Context = parent
+		}
+
+		for _, section := range tab.Sections {
+			if section.Context == nil {
+				section.Context = tab.Context
+			}
+
+			for _, content := range section.Contents {
+				if content.Context == nil {
+					content.Context = section.Context
+				}
+			}
+		}
+	}
+
+	return form, nil
+}
+
+type ContextConfig struct {
+	FromType       string   // "live" or "declared"
+	Capabilities   []string // "read", "write"
+	TemplateReader templater.TemplateReader
+	TemplateWriter templater.TemplateWriter
+}
+
+// create map[*FormContext]*ContextConfig
+// assumes all contexts populated
+func formToLookupTable(def *ClientConfigDefault, form *models.FormYAML) map[*models.FormContext]*ContextConfig {
+	lookup := make(map[*models.FormContext]*ContextConfig)
+
+	for i, tab := range form.Tabs {
+		for j, section := range tab.Sections {
+			for k, content := range section.Contents {
+				if content.Context == nil {
+					continue
+				}
+
+				if _, ok := lookup[content.Context]; !ok {
+					lookup[content.Context] = formContextToContextConfig(def, content.Context)
+				}
+
+				if content.Value != "" {
+					// TODO -- case on whether value is proper query string, if not resolve it to a
+					// proper query string
+					query, err := utils.NewQuery(
+						fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
+						fmt.Sprintf("%v", content.Value),
+					)
+
+					if err != nil {
+						continue
+					}
+
+					lookup[content.Context].TemplateReader.RegisterQuery(query)
+				} else if content.Variable != "" {
+					// if variable field set without value field set, make variable field into jsonpath
+					// query
+					query, err := utils.NewQuery(
+						fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
+						fmt.Sprintf("{ .%v }", content.Variable),
+					)
+
+					if err != nil {
+						continue
+					}
+
+					lookup[content.Context].TemplateReader.RegisterQuery(query)
+				}
+			}
+		}
+	}
+
+	return lookup
+}
+
+// TODO -- this needs to be able to construct new context configs based on
+// configuration for each context, but right now just uses the default config
+// for everything
+func formContextToContextConfig(def *ClientConfigDefault, context *models.FormContext) *ContextConfig {
+	res := &ContextConfig{}
+
+	switch context.Type {
+	case "helm/values":
+		res.FromType = "declared"
+
+		res.Capabilities = []string{"read", "write"}
+
+		res.TemplateReader = &th.ValuesTemplateReader{
+			Release: def.HelmRelease,
+			Chart:   def.HelmChart,
+		}
+
+		relName := ""
+
+		if def.HelmRelease != nil {
+			relName = def.HelmRelease.Name
+		}
+
+		res.TemplateWriter = &th.ValuesTemplateWriter{
+			Agent:       def.HelmAgent,
+			Chart:       def.HelmChart,
+			ReleaseName: relName,
+		}
+	case "helm/manifests":
+		res.FromType = "live"
+
+		res.Capabilities = []string{"read"}
+
+		res.TemplateReader = &th.ManifestsTemplateReader{
+			Release: def.HelmRelease,
+		}
+	case "cluster":
+		res.FromType = "live"
+
+		res.Capabilities = []string{"read"}
+
+		// identify object based on passed config
+		obj := &td.Object{
+			Group:     context.Config["Group"],
+			Version:   context.Config["Version"],
+			Resource:  context.Config["Resource"],
+			Namespace: context.Config["Namespace"],
+			Name:      context.Config["Name"],
+		}
+
+		res.TemplateReader = td.NewDynamicTemplateReader(def.DynamicClient, obj)
+	default:
+		return nil
+	}
+
+	return res
+}

+ 59 - 0
internal/templater/utils/query.go

@@ -0,0 +1,59 @@
+package utils
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/porter-dev/porter/internal/templater"
+	"k8s.io/client-go/util/jsonpath"
+)
+
+// NewQuery constructs a templater.TemplateReaderQuery by parsing the jsonpath
+// query string
+func NewQuery(key, query string) (*templater.TemplateReaderQuery, error) {
+	j := jsonpath.New(key)
+	j.AllowMissingKeys(true)
+
+	err := j.Parse(query)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &templater.TemplateReaderQuery{
+		Key:         key,
+		QueryString: query,
+		Template:    j,
+	}, nil
+}
+
+// QueryValues iterates through a map[string]interface{} and executes a query,
+// returning a map of the query name to the returned data
+func QueryValues(
+	values map[string]interface{},
+	queries []*templater.TemplateReaderQuery,
+) (map[string]interface{}, error) {
+	res := make(map[string]interface{})
+
+	// iterate through all registered queries, add to resulting map
+	for _, query := range queries {
+		fullResults, err := query.Template.FindResults(values)
+
+		if err != nil {
+			fmt.Printf("query error %s", err)
+			continue
+		}
+
+		queryRes := make([]reflect.Value, 0)
+
+		for ix := range fullResults {
+			for _, result := range fullResults[ix] {
+				queryRes = append(queryRes, reflect.ValueOf(result.Interface()))
+			}
+		}
+
+		res[query.Key] = queryRes
+	}
+
+	return res, nil
+}

+ 89 - 0
internal/templater/utils/values.go

@@ -0,0 +1,89 @@
+package utils
+
+import "sigs.k8s.io/yaml"
+
+// MergeYAML merges raw yaml, with preference given to override
+func MergeYAML(base, override []byte) (map[string]interface{}, error) {
+	baseVals := map[string]interface{}{}
+	overrideVals := map[string]interface{}{}
+
+	err := yaml.Unmarshal(base, &baseVals)
+
+	if err != nil {
+		return nil, err
+	}
+
+	err = yaml.Unmarshal(override, &overrideVals)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return CoalesceValues(baseVals, overrideVals), nil
+}
+
+// CoalesceValues replaces arrays and scalar values, merges maps
+func CoalesceValues(base, override map[string]interface{}) map[string]interface{} {
+	for key, val := range base {
+
+		if oVal, ok := override[key]; ok {
+			if oVal == nil {
+				delete(override, key)
+			} else if isYAMLTable(oVal) && isYAMLTable(val) {
+				oMapVal, _ := oVal.(map[string]interface{})
+				bMapVal, _ := val.(map[string]interface{})
+
+				override[key] = mergeMaps(bMapVal, oMapVal)
+			}
+		} else {
+			override[key] = val
+		}
+	}
+
+	return override
+}
+
+func isYAMLTable(v interface{}) bool {
+	_, ok := v.(map[string]interface{})
+	return ok
+}
+
+// mergeMaps merges any number of maps together, with maps later in the slice taking
+// precedent
+func mergeMaps(maps ...map[string]interface{}) map[string]interface{} {
+	// merge bottom-up
+	if len(maps) > 2 {
+		mLen := len(maps)
+		newMaps := maps[0 : mLen-2]
+
+		// reduce length of maps by 1 and merge again
+		newMaps = append(newMaps, mergeMaps(maps[mLen-2], maps[mLen-1]))
+		return mergeMaps(newMaps...)
+	} else if len(maps) == 2 {
+		if maps[0] == nil {
+			return maps[1]
+		}
+
+		if maps[1] == nil {
+			return maps[0]
+		}
+
+		for key, map0Val := range maps[0] {
+			if map1Val, ok := maps[1][key]; ok && map1Val == nil {
+				delete(maps[1], key)
+			} else if !ok {
+				maps[1][key] = map0Val
+			} else if isYAMLTable(map0Val) {
+				if isYAMLTable(map1Val) {
+					mergeMaps(map0Val.(map[string]interface{}), map1Val.(map[string]interface{}))
+				}
+			}
+		}
+
+		return maps[1]
+	} else if len(maps) == 1 {
+		return maps[0]
+	}
+
+	return nil
+}

+ 21 - 0
node_modules/@types/random-words/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/random-words/README.md

@@ -0,0 +1,16 @@
+# Installation
+> `npm install --save @types/random-words`
+
+# Summary
+This package contains type definitions for random-words (https://github.com/punkave/random-words#readme).
+
+# Details
+Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/random-words.
+
+### Additional Details
+ * Last updated: Tue, 08 Sep 2020 12:35:22 GMT
+ * Dependencies: none
+ * Global values: none
+
+# Credits
+These definitions were written by [Piotr Błażejewicz](https://github.com/peterblazejewicz).

+ 34 - 0
node_modules/@types/random-words/index.d.ts

@@ -0,0 +1,34 @@
+// Type definitions for random-words 1.1
+// Project: https://github.com/punkave/random-words#readme
+// Definitions by: Piotr Błażejewicz <https://github.com/peterblazejewicz>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+
+/**
+ * generates random words for use as sample text.
+ * We use it to generate random blog posts when testing Apostrophe.
+ */
+declare function words(
+    options?: words.Options & {
+        join: string;
+    },
+): string;
+declare function words(options: words.Options | number): string | string[];
+
+declare namespace words {
+    let wordList: string[];
+
+    interface Options {
+        exactly?: number;
+        formatter?: WordFormatter;
+        join?: string;
+        max?: number;
+        maxLength?: number;
+        min?: number;
+        separator?: string;
+        wordsPerString?: number;
+    }
+
+    type WordFormatter = (word: string, relativeIndex?: number) => string;
+}
+
+export = words;

+ 54 - 0
node_modules/@types/random-words/package.json

@@ -0,0 +1,54 @@
+{
+  "_from": "@types/random-words",
+  "_id": "@types/random-words@1.1.0",
+  "_inBundle": false,
+  "_integrity": "sha512-YZqkHIAGoXv6mlyEOwluhMjF9aotH2m9HrCCR+PNtES/ED00u5u4W7X1lWxc5AaFRKdKx+5orWTFW7iyGrZpOQ==",
+  "_location": "/@types/random-words",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "@types/random-words",
+    "name": "@types/random-words",
+    "escapedName": "@types%2frandom-words",
+    "scope": "@types",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.0.tgz",
+  "_shasum": "7f4245faf2092caa6511f6fadda303162a3e3772",
+  "_spec": "@types/random-words",
+  "_where": "/Users/jusrhee/Documents/porter",
+  "bugs": {
+    "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/issues"
+  },
+  "bundleDependencies": false,
+  "contributors": [
+    {
+      "name": "Piotr Błażejewicz",
+      "url": "https://github.com/peterblazejewicz"
+    }
+  ],
+  "dependencies": {},
+  "deprecated": false,
+  "description": "TypeScript definitions for random-words",
+  "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped#readme",
+  "license": "MIT",
+  "main": "",
+  "name": "@types/random-words",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/DefinitelyTyped/DefinitelyTyped.git",
+    "directory": "types/random-words"
+  },
+  "scripts": {},
+  "typeScriptVersion": "3.1",
+  "types": "index.d.ts",
+  "typesPublisherContentHash": "2cb4b703bd042634031da19d90d2c93ea8f42cdb8948a33990d439fd346eff93",
+  "version": "1.1.0"
+}

+ 1 - 844
package-lock.json

@@ -5,850 +5,7 @@
     "@types/random-words": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@types/random-words/-/random-words-1.1.0.tgz",
-      "integrity": "sha512-YZqkHIAGoXv6mlyEOwluhMjF9aotH2m9HrCCR+PNtES/ED00u5u4W7X1lWxc5AaFRKdKx+5orWTFW7iyGrZpOQ==",
-      "dev": true
-    },
-    "ansi-colors": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
-      "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw=="
-    },
-    "ansi-regex": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-      "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
-    },
-    "ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "requires": {
-        "color-convert": "^1.9.0"
-      }
-    },
-    "anymatch": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
-      "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
-      "requires": {
-        "normalize-path": "^3.0.0",
-        "picomatch": "^2.0.4"
-      }
-    },
-    "argparse": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "requires": {
-        "sprintf-js": "~1.0.2"
-      }
-    },
-    "balanced-match": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-    },
-    "binary-extensions": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
-      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "requires": {
-        "fill-range": "^7.0.1"
-      }
-    },
-    "browser-stdout": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
-      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
-    },
-    "call-bind": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
-      "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
-      "requires": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.0"
-      }
-    },
-    "camelcase": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
-    },
-    "chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "requires": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "dependencies": {
-        "supports-color": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        }
-      }
-    },
-    "chokidar": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
-      "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
-      "requires": {
-        "anymatch": "~3.1.1",
-        "braces": "~3.0.2",
-        "fsevents": "~2.1.1",
-        "glob-parent": "~5.1.0",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.2.0"
-      }
-    },
-    "cliui": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
-      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
-      "requires": {
-        "string-width": "^3.1.0",
-        "strip-ansi": "^5.2.0",
-        "wrap-ansi": "^5.1.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
-        },
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
-      }
-    },
-    "color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "requires": {
-        "color-name": "1.1.3"
-      }
-    },
-    "color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
-    },
-    "debug": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-      "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-      "requires": {
-        "ms": "^2.1.1"
-      }
-    },
-    "decamelize": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
-    },
-    "define-properties": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
-      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "requires": {
-        "object-keys": "^1.0.12"
-      }
-    },
-    "diff": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
-      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
-    },
-    "emoji-regex": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
-      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
-    },
-    "es-abstract": {
-      "version": "1.18.0-next.1",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
-      "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
-      "requires": {
-        "es-to-primitive": "^1.2.1",
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.1",
-        "is-callable": "^1.2.2",
-        "is-negative-zero": "^2.0.0",
-        "is-regex": "^1.1.1",
-        "object-inspect": "^1.8.0",
-        "object-keys": "^1.1.1",
-        "object.assign": "^4.1.1",
-        "string.prototype.trimend": "^1.0.1",
-        "string.prototype.trimstart": "^1.0.1"
-      },
-      "dependencies": {
-        "object.assign": {
-          "version": "4.1.2",
-          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
-          "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
-          "requires": {
-            "call-bind": "^1.0.0",
-            "define-properties": "^1.1.3",
-            "has-symbols": "^1.0.1",
-            "object-keys": "^1.1.1"
-          }
-        }
-      }
-    },
-    "es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "requires": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
-      }
-    },
-    "escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
-    },
-    "esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
-    },
-    "fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "requires": {
-        "to-regex-range": "^5.0.1"
-      }
-    },
-    "find-up": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
-      "requires": {
-        "locate-path": "^3.0.0"
-      }
-    },
-    "flat": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz",
-      "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==",
-      "requires": {
-        "is-buffer": "~2.0.3"
-      }
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
-    },
-    "fsevents": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-      "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ=="
-    },
-    "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
-    },
-    "get-caller-file": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
-    },
-    "get-intrinsic": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
-      "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
-      "requires": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.1"
-      }
-    },
-    "glob": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
-      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
-      "requires": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.0.4",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      }
-    },
-    "glob-parent": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
-      "requires": {
-        "is-glob": "^4.0.1"
-      }
-    },
-    "growl": {
-      "version": "1.10.5",
-      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
-      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
-    },
-    "has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "requires": {
-        "function-bind": "^1.1.1"
-      }
-    },
-    "has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
-    },
-    "has-symbols": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
-      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
-    },
-    "he": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
-    },
-    "is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "requires": {
-        "binary-extensions": "^2.0.0"
-      }
-    },
-    "is-buffer": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
-      "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
-    },
-    "is-callable": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
-      "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
-    },
-    "is-date-object": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
-      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
-    },
-    "is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
-    },
-    "is-fullwidth-code-point": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
-    },
-    "is-glob": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
-      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "requires": {
-        "is-extglob": "^2.1.1"
-      }
-    },
-    "is-negative-zero": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
-      "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
-    },
-    "is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
-    },
-    "is-regex": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
-      "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
-      "requires": {
-        "has-symbols": "^1.0.1"
-      }
-    },
-    "is-symbol": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
-      "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "requires": {
-        "has-symbols": "^1.0.1"
-      }
-    },
-    "isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
-    },
-    "js-yaml": {
-      "version": "3.13.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
-      "requires": {
-        "argparse": "^1.0.7",
-        "esprima": "^4.0.0"
-      }
-    },
-    "locate-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
-      "requires": {
-        "p-locate": "^3.0.0",
-        "path-exists": "^3.0.0"
-      }
-    },
-    "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
-    },
-    "log-symbols": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
-      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
-      "requires": {
-        "chalk": "^2.4.2"
-      }
-    },
-    "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "requires": {
-        "brace-expansion": "^1.1.7"
-      }
-    },
-    "minimist": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
-    },
-    "mkdirp": {
-      "version": "0.5.5",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
-      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-      "requires": {
-        "minimist": "^1.2.5"
-      }
-    },
-    "mocha": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
-      "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
-      "requires": {
-        "ansi-colors": "3.2.3",
-        "browser-stdout": "1.3.1",
-        "chokidar": "3.3.0",
-        "debug": "3.2.6",
-        "diff": "3.5.0",
-        "escape-string-regexp": "1.0.5",
-        "find-up": "3.0.0",
-        "glob": "7.1.3",
-        "growl": "1.10.5",
-        "he": "1.2.0",
-        "js-yaml": "3.13.1",
-        "log-symbols": "3.0.0",
-        "minimatch": "3.0.4",
-        "mkdirp": "0.5.5",
-        "ms": "2.1.1",
-        "node-environment-flags": "1.0.6",
-        "object.assign": "4.1.0",
-        "strip-json-comments": "2.0.1",
-        "supports-color": "6.0.0",
-        "which": "1.3.1",
-        "wide-align": "1.1.3",
-        "yargs": "13.3.2",
-        "yargs-parser": "13.1.2",
-        "yargs-unparser": "1.6.0"
-      }
-    },
-    "ms": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-      "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
-    },
-    "node-environment-flags": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
-      "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
-      "requires": {
-        "object.getownpropertydescriptors": "^2.0.3",
-        "semver": "^5.7.0"
-      }
-    },
-    "normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
-    },
-    "object-inspect": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
-      "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
-    },
-    "object-keys": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
-    },
-    "object.assign": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
-      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
-      "requires": {
-        "define-properties": "^1.1.2",
-        "function-bind": "^1.1.1",
-        "has-symbols": "^1.0.0",
-        "object-keys": "^1.0.11"
-      }
-    },
-    "object.getownpropertydescriptors": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
-      "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==",
-      "requires": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.18.0-next.1"
-      }
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "p-limit": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "requires": {
-        "p-try": "^2.0.0"
-      }
-    },
-    "p-locate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-      "requires": {
-        "p-limit": "^2.0.0"
-      }
-    },
-    "p-try": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
-    },
-    "path-exists": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
-    },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
-    },
-    "picomatch": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
-      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
-    },
-    "random-words": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/random-words/-/random-words-1.1.1.tgz",
-      "integrity": "sha512-Rdk5EoQePyt9Tz3RjeMELi2BSaCI+jDiOkBr4U+3fyBRiiW3qqEuaegGAUMOZ4yGWlQscFQGqQpdic3mAbNkrw==",
-      "requires": {
-        "mocha": "^7.1.1"
-      }
-    },
-    "readdirp": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
-      "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
-      "requires": {
-        "picomatch": "^2.0.4"
-      }
-    },
-    "require-directory": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
-    },
-    "require-main-filename": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
-    },
-    "semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-    },
-    "set-blocking": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
-    },
-    "sprintf-js": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
-    },
-    "string-width": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
-      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
-      "requires": {
-        "is-fullwidth-code-point": "^2.0.0",
-        "strip-ansi": "^4.0.0"
-      }
-    },
-    "string.prototype.trimend": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
-      "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
-      "requires": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3"
-      }
-    },
-    "string.prototype.trimstart": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
-      "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
-      "requires": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3"
-      }
-    },
-    "strip-ansi": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
-      "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
-      "requires": {
-        "ansi-regex": "^3.0.0"
-      }
-    },
-    "strip-json-comments": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
-    },
-    "supports-color": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
-      "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
-      "requires": {
-        "has-flag": "^3.0.0"
-      }
-    },
-    "to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "requires": {
-        "is-number": "^7.0.0"
-      }
-    },
-    "which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "requires": {
-        "isexe": "^2.0.0"
-      }
-    },
-    "which-module": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
-    },
-    "wide-align": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
-      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
-      "requires": {
-        "string-width": "^1.0.2 || 2"
-      }
-    },
-    "wrap-ansi": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
-      "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
-      "requires": {
-        "ansi-styles": "^3.2.0",
-        "string-width": "^3.0.0",
-        "strip-ansi": "^5.0.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
-        },
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-    },
-    "y18n": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
-      "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ=="
-    },
-    "yargs": {
-      "version": "13.3.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
-      "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
-      "requires": {
-        "cliui": "^5.0.0",
-        "find-up": "^3.0.0",
-        "get-caller-file": "^2.0.1",
-        "require-directory": "^2.1.1",
-        "require-main-filename": "^2.0.0",
-        "set-blocking": "^2.0.0",
-        "string-width": "^3.0.0",
-        "which-module": "^2.0.0",
-        "y18n": "^4.0.0",
-        "yargs-parser": "^13.1.2"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
-        },
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
-      }
-    },
-    "yargs-parser": {
-      "version": "13.1.2",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
-      "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
-      "requires": {
-        "camelcase": "^5.0.0",
-        "decamelize": "^1.2.0"
-      }
-    },
-    "yargs-unparser": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
-      "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
-      "requires": {
-        "flat": "^4.1.0",
-        "lodash": "^4.17.15",
-        "yargs": "^13.3.0"
-      }
+      "integrity": "sha512-YZqkHIAGoXv6mlyEOwluhMjF9aotH2m9HrCCR+PNtES/ED00u5u4W7X1lWxc5AaFRKdKx+5orWTFW7iyGrZpOQ=="
     }
   }
 }

+ 33 - 137
server/api/deploy_handler.go

@@ -1,26 +1,33 @@
 package api
 
 import (
-	"archive/tar"
-	"bytes"
-	"compress/gzip"
 	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
-	"strings"
 
+	"github.com/go-chi/chi"
 	"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"
+	"github.com/porter-dev/porter/internal/helm/loader"
 )
 
 // HandleDeployTemplate triggers a chart deployment from a template
 func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+	version := chi.URLParam(r, "version")
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	getChartForm := &forms.ChartForm{
+		Name:    name,
+		Version: version,
+		RepoURL: "https://porter-dev.github.io/chart-repo/",
+	}
+
+	// if a repo_url is passed as query param, it will be populated
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 
 	if err != nil {
@@ -28,6 +35,15 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	getChartForm.PopulateRepoURLFromQueryParams(vals)
+
+	chart, err := loader.LoadChart(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
 	form := &forms.InstallChartTemplateForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
@@ -54,31 +70,18 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 	)
 
 	if err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
-	values := form.ChartTemplateForm.FormValues
-
-	v, err := yaml.Marshal(values)
-	if err != nil {
-		return
-	}
-
-	var tgz string
-	switch form.ChartTemplateForm.TemplateName {
-	case "redis":
-		tgz = "redis-0.0.1.tgz"
-	case "Docker":
-		tgz = "docker-0.0.1.tgz"
+	conf := &helm.InstallChartConfig{
+		Chart:     chart,
+		Name:      form.ChartTemplateForm.Name,
+		Namespace: form.ReleaseForm.Form.Namespace,
+		Values:    form.ChartTemplateForm.FormValues,
 	}
 
-	// Output values.yaml string
-	_, err = agent.InstallChart(
-		"./internal/local_templates/"+tgz,
-		v,
-		form.ChartTemplateForm.Name,
-		form.ReleaseForm.Form.Namespace,
-	)
+	_, err = agent.InstallChart(conf)
 
 	if err != nil {
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
@@ -91,110 +94,3 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 
 	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 nil, err
-	}
-
-	defer resp.Body.Close()
-	body, _ := ioutil.ReadAll(resp.Body)
-
-	form := models.IndexYAML{}
-	if err := yaml.Unmarshal([]byte(body), &form); err != nil {
-		fmt.Println(err)
-		return nil, err
-	}
-
-	// Loop over charts in index.yaml
-	for k := range form.Entries {
-		indexChart := form.Entries[k][0]
-		tarURL := indexChart.Urls[0]
-		splits := strings.Split(tarURL, "-")
-
-		strAcc := splits[0]
-		for i := 1; i < len(splits)-1; i++ {
-			strAcc += "-" + splits[i]
-		}
-
-		// Unpack the target chart and retrieve values.yaml
-		if strAcc == templateName {
-			tgtURL := baseURL + tarURL
-			values, err := processValues(tgtURL)
-			if err != nil {
-				fmt.Println(err)
-				return nil, err
-			}
-			return values, nil
-		}
-	}
-	return nil, errors.New("no values.yaml found")
-}
-
-func processValues(tgtURL string) (*map[interface{}]interface{}, error) {
-	resp, err := http.Get(tgtURL)
-	if err != nil {
-		fmt.Println(err)
-		return nil, err
-	}
-
-	defer resp.Body.Close()
-	body, _ := ioutil.ReadAll(resp.Body)
-	buf := bytes.NewBuffer(body)
-
-	gzf, err := gzip.NewReader(buf)
-	if err != nil {
-		fmt.Println(err)
-		return nil, err
-	}
-
-	// Process tarball to generate FormYAML and retrieve markdown
-	tarReader := tar.NewReader(gzf)
-	for {
-		header, err := tarReader.Next()
-		if err == io.EOF {
-			break
-		} else if err != nil {
-			fmt.Println(err)
-			return nil, err
-		}
-
-		name := header.Name
-		switch header.Typeflag {
-		case tar.TypeDir:
-			continue
-		case tar.TypeReg:
-
-			// Handle values.yaml located in archive
-			if strings.Contains(name, "values.yaml") {
-				bufForm := new(bytes.Buffer)
-
-				_, err := io.Copy(bufForm, tarReader)
-				if err != nil {
-					fmt.Println(err)
-					return nil, err
-				}
-
-				// Unmarshal yaml byte buffer
-				form := make(map[interface{}]interface{})
-				if err := yaml.Unmarshal(bufForm.Bytes(), &form); err != nil {
-					fmt.Println(err)
-					return nil, err
-				}
-				return &form, nil
-			}
-		default:
-			fmt.Printf("%s : %c %s %s\n",
-				"Unknown type",
-				header.Typeflag,
-				"in file",
-				name,
-			)
-		}
-	}
-	return nil, errors.New("no values.yaml found")
-}

+ 61 - 0
server/api/release_handler.go

@@ -5,6 +5,11 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/templater/parser"
+	"helm.sh/helm/v3/pkg/release"
 
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
@@ -60,6 +65,12 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// PorterRelease is a helm release with a form attached
+type PorterRelease struct {
+	*release.Release
+	Form *models.FormYAML `json:"form"`
+}
+
 // HandleGetRelease retrieves a single release based on a name and revision
 func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
@@ -98,6 +109,56 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// get the filter options
+	k8sForm := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo: app.repo,
+		},
+	}
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.repo.Cluster)
+
+	// validate the form
+	if err := app.validator.Struct(k8sForm); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new dynamic client
+	dynClient, err := kubernetes.GetDynamicClientOutOfClusterConfig(k8sForm.OutOfClusterConfig)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	parserDef := &parser.ClientConfigDefault{
+		DynamicClient: dynClient,
+		HelmChart:     release.Chart,
+		HelmRelease:   release,
+	}
+
+	res := &PorterRelease{release, nil}
+
+	for _, file := range release.Chart.Files {
+		if strings.Contains(file.Name, "form.yaml") {
+			formYAML, _ := parser.FormYAMLFromBytes(parserDef, file.Data)
+			if err != nil {
+				break
+			}
+
+			res.Form = formYAML
+			break
+		}
+	}
+
 	if err := json.NewEncoder(w).Encode(release); err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return

+ 55 - 100
server/api/template_handler.go

@@ -1,65 +1,40 @@
 package api
 
 import (
-	"archive/tar"
-	"bytes"
-	"compress/gzip"
 	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
 	"net/http"
+	"net/url"
 	"strings"
 
-	"github.com/porter-dev/porter/internal/models"
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/templater/parser"
 
-	"gopkg.in/yaml.v2"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 // HandleListTemplates retrieves a list of Porter templates
 // TODO: test and reduce fragility (handle untar/parse error for individual charts)
 // TODO: separate markdown retrieval into its own query if necessary
 func (app *App) HandleListTemplates(w http.ResponseWriter, r *http.Request) {
-	baseURL := "https://porter-dev.github.io/chart-repo/"
-	resp, err := http.Get(baseURL + "index.yaml")
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-
-	defer resp.Body.Close()
-	body, _ := ioutil.ReadAll(resp.Body)
+	repoIndex, err := loader.LoadRepoIndex("https://porter-dev.github.io/chart-repo/index.yaml")
 
-	form := models.IndexYAML{}
-	if err := yaml.Unmarshal([]byte(body), &form); err != nil {
-		fmt.Println(err)
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
 	}
 
 	// Loop over charts in index.yaml
-	porterCharts := []models.PorterChart{}
-	for k := range form.Entries {
-		indexChart := form.Entries[k][0]
-		tarURL := indexChart.Urls[0]
-		if !strings.Contains(tarURL, "http://") {
-			tarURL = baseURL + tarURL
-		}
+	porterCharts := []models.PorterChartList{}
 
-		formData, markdown, err := processTarball(tarURL)
-		if err != nil {
-			fmt.Println(err)
-			return
-		}
+	for _, entry := range repoIndex.Entries {
+		indexChart := entry[0]
 
-		porterChart := models.PorterChart{}
+		porterChart := models.PorterChartList{}
 		porterChart.Name = indexChart.Name
 		porterChart.Description = indexChart.Description
 		porterChart.Icon = indexChart.Icon
-		porterChart.Form = *formData
-		if markdown != "" {
-			porterChart.Markdown = markdown
-		}
 
 		porterCharts = append(porterCharts, porterChart)
 	}
@@ -67,80 +42,60 @@ func (app *App) HandleListTemplates(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(porterCharts)
 }
 
-func processTarball(tarURL string) (*models.FormYAML, string, error) {
-	resp, err := http.Get(tarURL)
-	if err != nil {
-		fmt.Println(err)
-		return nil, "", err
+// HandleReadTemplate reads a given template with name and version field
+func (app *App) HandleReadTemplate(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+	version := chi.URLParam(r, "version")
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
 	}
 
-	defer resp.Body.Close()
-	body, _ := ioutil.ReadAll(resp.Body)
-	buf := bytes.NewBuffer(body)
+	form := &forms.ChartForm{
+		Name:    name,
+		Version: version,
+		RepoURL: "https://porter-dev.github.io/chart-repo/",
+	}
+
+	// if a repo_url is passed as query param, it will be populated
+	vals, err := url.ParseQuery(r.URL.RawQuery)
 
-	gzf, err := gzip.NewReader(buf)
 	if err != nil {
-		fmt.Println(err)
-		return nil, "", err
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
 	}
 
-	// Process tarball to generate FormYAML and retrieve markdown
-	tarReader := tar.NewReader(gzf)
-	markdown := ""
-	for {
-		header, err := tarReader.Next()
-		if err == io.EOF {
-			break
-		} else if err != nil {
-			fmt.Println(err)
-			return nil, "", err
-		}
+	form.PopulateRepoURLFromQueryParams(vals)
+
+	chart, err := loader.LoadChart(form.RepoURL, form.Name, form.Version)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
 
-		name := header.Name
-		switch header.Typeflag {
-		case tar.TypeDir:
-			continue
-		case tar.TypeReg:
+	parserDef := &parser.ClientConfigDefault{
+		HelmChart: chart,
+	}
 
-			// Handle info.md if found
-			if strings.Contains(name, "README.md") {
-				bufMd := new(bytes.Buffer)
+	res := &models.PorterChartRead{}
+	res.Metadata = chart.Metadata
+	res.Values = chart.Values
 
-				_, err := io.Copy(bufMd, tarReader)
-				if err != nil {
-					fmt.Println(err)
-					return nil, "", err
-				}
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "form.yaml") {
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data)
 
-				markdown = string(bufMd.Bytes())
+			if err != nil {
+				break
 			}
 
-			// Handle form.yaml located in archive
-			if strings.Contains(name, "form.yaml") {
-				bufForm := new(bytes.Buffer)
-
-				_, err := io.Copy(bufForm, tarReader)
-				if err != nil {
-					fmt.Println(err)
-					return nil, "", err
-				}
-
-				// Unmarshal yaml byte buffer
-				form := models.FormYAML{}
-				if err := yaml.Unmarshal(bufForm.Bytes(), &form); err != nil {
-					fmt.Println(err)
-					return nil, "", err
-				}
-				return &form, markdown, nil
-			}
-		default:
-			fmt.Printf("%s : %c %s %s\n",
-				"Unknown type",
-				header.Typeflag,
-				"in file",
-				name,
-			)
+			res.Form = formYAML
+		} else if strings.Contains(file.Name, "README.md") {
+			res.Markdown = string(file.Data)
 		}
 	}
-	return nil, "", errors.New("no form.yaml found")
+
+	json.NewEncoder(w).Encode(res)
 }

+ 46 - 36
server/api/template_handler_test.go

@@ -6,12 +6,13 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 // ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
 
-type templatesTest struct {
+type templateTest struct {
 	initializers []func(tester *tester)
 	msg          string
 	method       string
@@ -20,10 +21,10 @@ type templatesTest struct {
 	expStatus    int
 	expBody      string
 	useCookie    bool
-	validators   []func(c *templatesTest, tester *tester, t *testing.T)
+	validators   []func(c *templateTest, tester *tester, t *testing.T)
 }
 
-func testTemplatesRequests(t *testing.T, tests []*templatesTest, canQuery bool) {
+func testTemplatesRequests(t *testing.T, tests []*templateTest, canQuery bool) {
 	for _, c := range tests {
 		// create a new tester
 		tester := newTester(canQuery)
@@ -67,47 +68,56 @@ func testTemplatesRequests(t *testing.T, tests []*templatesTest, canQuery bool)
 
 // ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
 
-// var listTemplatesTests = []*templatesTest{
-// 	&templatesTest{
-// 		initializers: []func(tester *tester){
-// 			initDefaultTemplates,
-// 		},
-// 		msg:       "List templates",
-// 		method:    "GET",
-// 		endpoint:  "/api/templates",
-// 		body:      "",
-// 		expStatus: http.StatusOK,
-// 		expBody:   "unimplemented",
-// 		useCookie: true,
-// 		validators: []func(c *templatesTest, tester *tester, t *testing.T){
-// 			templatesListValidator,
-// 		},
-// 	},
-// }
-
-// func TestHandleListTemplates(t *testing.T) {
-// 	testTemplatesRequests(t, listTemplatesTests, true)
-// }
+var listTemplatesTests = []*templateTest{
+	&templateTest{
+		initializers: []func(tester *tester){
+			initUserDefault,
+		},
+		msg:       "List templates",
+		method:    "GET",
+		endpoint:  "/api/templates",
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody:   `[{"name":"Docker","description":"Template to deploy any Docker container on Porter.","icon":"https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"},{"name":"redis","description":"DEPRECATED Open source, advanced key-value store. It is often referred to as a data structure server since keys can contain strings, hashes, lists, sets and sorted sets.","icon":"https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png"}]`,
+		useCookie: true,
+		validators: []func(c *templateTest, tester *tester, t *testing.T){
+			templatesListValidator,
+		},
+	},
+}
+
+func TestHandleListTemplates(t *testing.T) {
+	testTemplatesRequests(t, listTemplatesTests, true)
+}
 
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
-func initDefaultTemplates(tester *tester) {
-	initUserDefault(tester)
+func templatesListValidator(c *templateTest, tester *tester, t *testing.T) {
+	gotBody := make([]*models.PorterChartList, 0)
+	expBody := make([]*models.PorterChartList, 0)
 
-	agent := kubernetes.GetAgentTesting(defaultObjects...)
+	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
 
-	// overwrite the test agent with new resources
-	tester.app.TestAgents.K8sAgent = agent
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
 }
 
-func templatesListValidator(c *templatesTest, tester *tester, t *testing.T) {
-	var gotBody map[string]interface{}
-	var expBody map[string]interface{}
+func templateBodyValidator(c *templateTest, tester *tester, t *testing.T) {
+	gotBody := models.PorterChartRead{}
+	expBody := models.PorterChartRead{}
 
-	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	bytes := tester.rr.Body.Bytes()
+
+	t.Errorf(string(bytes))
+
+	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 
-	if string(tester.rr.Body.Bytes()) != c.expBody {
-		t.Errorf("Mismatch")
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
 	}
 }

+ 10 - 2
server/router/router.go

@@ -121,6 +121,14 @@ func New(
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/templates/{name}/{version}",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleReadTemplate, l),
+			),
+		)
+
 		// /api/oauth routes
 		// r.Method(
 		// 	"GET",
@@ -493,10 +501,10 @@ func New(
 		// 	),
 		// )
 
-		// /api/projects/{project_id}/deploy route
+		// /api/projects/{project_id}/deploy routes
 		r.Method(
 			"POST",
-			"/projects/{project_id}/deploy",
+			"/projects/{project_id}/deploy/{name}/{version}",
 			auth.DoesUserHaveProjectAccess(
 				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleDeployTemplate, l),