Kaynağa Gözat

helm and k8s contexts complete

Alexander Belanger 5 yıl önce
ebeveyn
işleme
793ef7dd13

+ 0 - 385
cli/cmd/test.go

@@ -1,385 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"reflect"
-	"strings"
-	"text/tabwriter"
-	"time"
-
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/dynamic"
-	di "k8s.io/client-go/dynamic/dynamicinformer"
-	"k8s.io/client-go/tools/cache"
-	"k8s.io/client-go/tools/clientcmd"
-	"k8s.io/client-go/util/jsonpath"
-
-	"github.com/fatih/color"
-	"github.com/porter-dev/porter/internal/kubernetes/local"
-	"github.com/spf13/cobra"
-	"gopkg.in/yaml.v2"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-)
-
-type Object struct {
-	Group    string `yaml:"Group"`
-	Version  string `yaml:"Version"`
-	Resource string `yaml:"Resource"`
-}
-
-type Field struct {
-	Kind  string `yaml:"Kind"`
-	Query string `yaml:"Query"`
-}
-
-type Operation struct {
-	Kind   string   `yaml:"Kind"`
-	Object *Object  `yaml:"Object"`
-	Fields []*Field `yaml:"Fields"`
-}
-
-func getOperations() ([]*Operation, error) {
-	ops := make([]*Operation, 0)
-
-	yamlFile, err := ioutil.ReadFile("./cmd/test.yaml")
-
-	if err != nil {
-		return nil, err
-	}
-
-	err = yaml.Unmarshal(yamlFile, &ops)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return ops, nil
-}
-
-func initTabWriterArgs(fields []*Field) (string, []interface{}) {
-	firstArg := ""
-	nextArgs := make([]interface{}, 0)
-
-	for i, field := range fields {
-		firstArg += "%s"
-
-		if len(fields) > i+1 {
-			firstArg += "\t"
-		} else {
-			firstArg += "\n"
-		}
-
-		nextArgs = append(nextArgs, strings.ToUpper(field.Kind))
-	}
-
-	return firstArg, nextArgs
-}
-
-func PrintList(op *Operation, items []unstructured.Unstructured) {
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
-	firstTWArg, twHeaders := initTabWriterArgs(op.Fields)
-	fmt.Fprintf(w, firstTWArg, twHeaders...)
-
-	for _, item := range items {
-		printRes := make([]interface{}, 0)
-
-		for _, field := range op.Fields {
-			name := item.GetName()
-			j := jsonpath.New(name)
-			j.AllowMissingKeys(true)
-
-			err := j.Parse(field.Query)
-
-			if err != nil {
-				fmt.Printf("could not parse query for object %s: error=%s", name, err)
-				continue
-			}
-
-			fullResults, err := j.FindResults(item.Object)
-
-			if err != nil {
-				fmt.Printf("query error for object %s: error=%s", name, err)
-				continue
-			}
-
-			res := make([]string, 0)
-			for ix := range fullResults {
-				for _, result := range fullResults[ix] {
-					res = append(res, fmt.Sprintf("%v", reflect.ValueOf(result.Interface())))
-				}
-			}
-
-			switch field.Kind {
-			case "Title", "Status":
-				printRes = append(printRes, strings.Join(res, ""))
-			case "Array":
-				printRes = append(printRes, strings.Join(res, ","))
-			}
-		}
-
-		fmt.Fprintf(w, firstTWArg, printRes...)
-	}
-
-	w.Flush()
-}
-
-func PrintRead(op *Operation, item *unstructured.Unstructured) {
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
-	firstTWArg, twHeaders := initTabWriterArgs(op.Fields)
-	fmt.Fprintf(w, firstTWArg, twHeaders...)
-
-	printRes := make([]interface{}, 0)
-
-	for _, field := range op.Fields {
-		name := item.GetName()
-		j := jsonpath.New(name)
-		j.AllowMissingKeys(true)
-
-		err := j.Parse(field.Query)
-
-		if err != nil {
-			fmt.Printf("could not parse query for object %s: error=%s", name, err)
-			continue
-		}
-
-		fullResults, err := j.FindResults(item.Object)
-
-		if err != nil {
-			fmt.Printf("query error for object %s: error=%s", name, err)
-			continue
-		}
-
-		res := make([]string, 0)
-		for ix := range fullResults {
-			for _, result := range fullResults[ix] {
-				res = append(res, fmt.Sprintf("%v", reflect.ValueOf(result.Interface())))
-			}
-		}
-
-		switch field.Kind {
-		case "Title", "Status":
-			printRes = append(printRes, strings.Join(res, ""))
-		case "Array":
-			printRes = append(printRes, strings.Join(res, ","))
-		}
-	}
-
-	fmt.Fprintf(w, firstTWArg, printRes...)
-
-	w.Flush()
-}
-
-func listOperation(client dynamic.Interface, op *Operation) {
-	objRes := schema.GroupVersionResource{
-		Group:    op.Object.Group,
-		Version:  op.Object.Version,
-		Resource: op.Object.Resource,
-	}
-
-	namespace := "default"
-
-	fmt.Printf("Listing deployments in namespace %q:\n", namespace)
-
-	list, err := client.Resource(objRes).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
-
-	if err != nil {
-		red := color.New(color.FgRed)
-		red.Println("Error:", err.Error())
-		os.Exit(1)
-	}
-
-	PrintList(op, list.Items)
-}
-
-var testCmd = &cobra.Command{
-	Use:   "test",
-	Short: "Testing",
-	Run: func(cmd *cobra.Command, args []string) {
-
-		contextName := "gke_porter-dev-273614_us-central1-f_c-cz2vr"
-		rawBytes, err := local.GetKubeconfigFromHost("", []string{contextName})
-
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error:", err.Error())
-			os.Exit(1)
-		}
-
-		conf, err := clientcmd.NewClientConfigFromBytes(rawBytes)
-
-		rawConf, err := conf.RawConfig()
-
-		conf = clientcmd.NewDefaultClientConfig(rawConf, &clientcmd.ConfigOverrides{
-			CurrentContext: contextName,
-		})
-
-		restConf, err := conf.ClientConfig()
-
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error:", err.Error())
-			os.Exit(1)
-		}
-
-		client, err := dynamic.NewForConfig(restConf)
-
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error:", err.Error())
-			os.Exit(1)
-		}
-
-		ops, err := getOperations()
-
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error:", err.Error())
-			os.Exit(1)
-		}
-
-		StreamDynamic(client, ops[0])
-
-		// listOperation(client, ops[0])
-	},
-}
-
-type Message struct {
-	EventType string
-	Object    interface{}
-	Kind      string
-}
-
-func StreamDynamic(client dynamic.Interface, op *Operation) error {
-	factory := di.NewDynamicSharedInformerFactory(
-		client,
-		10*time.Second,
-	)
-
-	objRes := schema.GroupVersionResource{
-		Group:    op.Object.Group,
-		Version:  op.Object.Version,
-		Resource: op.Object.Resource,
-	}
-
-	informer := factory.ForResource(objRes).Informer()
-
-	stopper := make(chan struct{})
-	errorchan := make(chan error)
-	defer close(errorchan)
-	defer close(stopper)
-
-	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
-		UpdateFunc: func(oldObj, newObj interface{}) {
-			u := newObj.(*unstructured.Unstructured)
-
-			PrintRead(op, u)
-		},
-		DeleteFunc: func(obj interface{}) {
-			u := obj.(*unstructured.Unstructured)
-
-			PrintRead(op, u)
-		},
-	})
-
-	// TODO -- websocket
-	// go func() {
-	// 	// listens for websocket closing handshake
-	// 	for {
-	// 		if _, _, err := conn.ReadMessage(); err != nil {
-	// 			defer conn.Close()
-	// 			defer close(stopper)
-	// 			defer fmt.Println("Successfully closed controller status stream")
-	// 			errorchan <- nil
-	// 			return
-	// 		}
-	// 	}
-	// }()
-
-	go informer.Run(stopper)
-
-	for {
-		select {
-		case err := <-errorchan:
-			return err
-		}
-	}
-}
-
-func init() {
-	rootCmd.AddCommand(testCmd)
-}
-
-// replace arrays and scalar values
-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 oMapVal, ok := oVal.(map[string]interface{}); ok {
-				bMapVal, ok := val.(map[string]interface{})
-
-				if !ok {
-					continue
-				}
-
-				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
-}

+ 0 - 28
cli/cmd/test_test.go

@@ -1,28 +0,0 @@
-package cmd_test
-
-import (
-	"testing"
-
-	"github.com/porter-dev/porter/cli/cmd"
-)
-
-func TestMergeMapValues(t *testing.T) {
-	map1 := map[string]interface{}{
-		"hello": "there",
-		"i":     "have",
-	}
-
-	map2 := map[string]interface{}{
-		"hello": "general",
-		"the":   "high",
-	}
-
-	map3 := map[string]interface{}{
-		"hello":  "kenobi",
-		"ground": "!",
-	}
-
-	res := cmd.MergeMapValues(map1, map2, map3)
-
-	t.Errorf("%v\n", res)
-}

+ 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=

+ 33 - 14
internal/helm/agent.go

@@ -54,6 +54,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 +79,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,22 +88,33 @@ 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
+// InstallChart reads the raw values and calls Agent.InstallChartByValues
 func (a *Agent) InstallChart(
 	cp string,
 	values []byte,
 ) (*release.Release, error) {
-	cmd := action.NewInstall(a.ActionConfig)
 	valuesYaml, err := chartutil.ReadValues(values)
 
 	if err != nil {
 		return nil, fmt.Errorf("Values could not be parsed: %v", err)
 	}
 
+	return a.InstallChartByValues(cp, valuesYaml)
+}
+
+// InstallChartByValues installs a new chart by unmarshalled values via
+// 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) InstallChartByValues(
+	cp string,
+	values map[string]interface{},
+) (*release.Release, error) {
+	cmd := action.NewInstall(a.ActionConfig)
+
 	// Only supports filepaths for now, URL option WIP.
 	// Check chart dependencies to make sure all are present in /charts
 	chartRequested, err := loader.Load(cp)
@@ -118,7 +137,7 @@ func (a *Agent) InstallChart(
 		}
 	}
 
-	return cmd.Run(chartRequested, valuesYaml)
+	return cmd.Run(chartRequested, values)
 }
 
 // RollbackRelease rolls a release back to a specified revision/version

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

@@ -0,0 +1,180 @@
+package dynamic
+
+import (
+	"context"
+	"time"
+
+	"github.com/porter-dev/porter/cli/cmd/templater/utils"
+
+	"github.com/porter-dev/porter/cli/cmd/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/cli/cmd/templater/utils"
+
+	"github.com/porter-dev/porter/cli/cmd/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/cli/cmd/templater"
+	"github.com/porter-dev/porter/cli/cmd/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/cli/cmd/templater"
+	"github.com/porter-dev/porter/cli/cmd/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
+}

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

@@ -0,0 +1,58 @@
+package helm
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/helm"
+)
+
+// 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
+
+	// ChartPath for installing a chart
+	ChartPath string
+
+	// ReleaseName for upgrading the chart
+	ReleaseName 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.ChartPath != "" {
+		return nil, fmt.Errorf("chart path not set")
+	}
+
+	_, err := w.Agent.InstallChartByValues(w.ChartPath, vals)
+
+	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
+}

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

@@ -0,0 +1,59 @@
+package utils
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/porter-dev/porter/cli/cmd/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
+}