app-manager: add Actions
diff --git a/appmanager/actions.go b/appmanager/actions.go
new file mode 100644
index 0000000..88d2551
--- /dev/null
+++ b/appmanager/actions.go
@@ -0,0 +1,34 @@
+package appmanager
+
+import (
+	"io/ioutil"
+	"os"
+
+	"gopkg.in/yaml.v2"
+)
+
+type Action struct {
+	Name     string `yaml:"name"`
+	Template string `yaml:"template"`
+}
+
+type Actions struct {
+	Actions []Action `yaml:"actions"`
+}
+
+func FromYaml(str string, out interface{}) error {
+	return yaml.Unmarshal([]byte(str), out)
+}
+
+func FromYamlFile(actionsFile string, out interface{}) error {
+	f, err := os.Open(actionsFile)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	b, err := ioutil.ReadAll(f)
+	if err != nil {
+		return err
+	}
+	return FromYaml(string(b), out)
+}
diff --git a/appmanager/cmd/main.go b/appmanager/cmd/main.go
index 9d55071..9bf0b4e 100644
--- a/appmanager/cmd/main.go
+++ b/appmanager/cmd/main.go
@@ -7,6 +7,7 @@
 	"flag"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
@@ -45,8 +46,9 @@
 `
 
 type handler struct {
+	client   *kubernetes.Clientset
 	manager  *app.Manager
-	nsClient corev1.NamespaceInterface
+	launcher app.Launcher
 }
 
 func (hn *handler) handleInstall(w http.ResponseWriter, r *http.Request) {
@@ -113,9 +115,6 @@
 	triggerOnEvent := r.Form["trigger_on_event"][0]
 	var triggers []trigger
 	for _, a := range hn.manager.Apps {
-		if a.Triggers == nil {
-			continue
-		}
 		for _, t := range a.Triggers.Triggers {
 			if t.TriggerOn.Type == triggerOnType && t.TriggerOn.Event == triggerOnEvent {
 				triggers = append(triggers, trigger{a.Namespace, t.Template})
@@ -133,6 +132,44 @@
 	}
 }
 
+type actionReq struct {
+	App    string                 `json:"app"`
+	Action string                 `json:"action"`
+	Args   map[string]interface{} `json:"args"`
+}
+
+func (hn *handler) handleLaunchAction(w http.ResponseWriter, r *http.Request) {
+	actionStr, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	var req actionReq
+	if err := json.Unmarshal(actionStr, &req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	for _, a := range hn.manager.Apps {
+		if a.Name != req.App {
+			continue
+		}
+		for _, action := range a.Actions.Actions {
+			if action.Name != req.Action {
+				continue
+			}
+			err := hn.launcher.Launch(a.Namespace, action.Template, req.Args)
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+			}
+			return
+		}
+	}
+	http.Error(
+		w,
+		fmt.Sprintf("Application action not found: %s %s", req.App, req.Action),
+		http.StatusBadRequest)
+}
+
 func (hn *handler) installHelmChart(path string) error {
 	h, err := app.HelmChartFromTar(path)
 	if err != nil {
@@ -143,7 +180,8 @@
 	}
 	glog.Infof("Installed schema: %s", h.Schema)
 	namespace := fmt.Sprintf("app-%s", h.Name)
-	if err = createNamespace(hn.nsClient, namespace); err != nil {
+	err = createNamespace(hn.client.CoreV1().Namespaces(), namespace)
+	if err != nil {
 		return err
 	}
 	glog.Infof("Created namespaces: %s", namespace)
@@ -157,7 +195,7 @@
 	} else {
 		glog.Info("Skipping deployment as we got library chart.")
 	}
-	hn.manager.Apps[h.Name] = app.App{namespace, h.Triggers}
+	hn.manager.Apps[h.Name] = app.App{h.Name, namespace, h.Triggers, h.Actions}
 	app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
 	glog.Info("Installed")
 	return nil
@@ -191,13 +229,13 @@
 	if err != nil {
 		glog.Fatalf("Could not create Kubernetes API client: %v", err)
 	}
-	namespaces := clientset.CoreV1().Namespaces()
 	manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
 	if err != nil {
 		glog.Fatalf("Could ot initialize manager: %v", err)
 	}
-	h := handler{manager, namespaces}
+	h := handler{clientset, manager, app.NewK8sLauncher(clientset)}
 	http.HandleFunc("/triggers", h.handleTriggers)
+	http.HandleFunc("/launch_action", h.handleLaunchAction)
 	http.HandleFunc("/", h.handleInstall)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
 
diff --git a/appmanager/helm.go b/appmanager/helm.go
index 9fdf4ed..ede5faf 100644
--- a/appmanager/helm.go
+++ b/appmanager/helm.go
@@ -23,29 +23,31 @@
 type HelmChart struct {
 	Chart
 	chartDir string
-	Schema   *Schema
-	Triggers *Triggers
+	Schema   Schema
+	Triggers Triggers
+	Actions  Actions
 	Yamls    []string
 }
 
 func HelmChartFromDir(chartDir string) (*HelmChart, error) {
 	var chart HelmChart
 	chart.chartDir = chartDir
-	c, err := ReadChart(path.Join(chartDir, "Chart.yaml"))
+	err := FromYamlFile(path.Join(chartDir, "Chart.yaml"), &chart.Chart)
 	if err != nil {
 		return nil, err
 	}
-	chart.Chart = *c
-	schema, err := ReadSchema(path.Join(chartDir, "Schema.yaml"))
+	err = FromYamlFile(path.Join(chartDir, "Schema.yaml"), &chart.Schema)
 	if err != nil && !os.IsNotExist(err) {
 		return nil, err
 	}
-	chart.Schema = schema
-	triggers, err := ReadTriggers(path.Join(chartDir, "Triggers.yaml"))
+	err = FromYamlFile(path.Join(chartDir, "Triggers.yaml"), &chart.Triggers)
 	if err != nil && !os.IsNotExist(err) {
 		return nil, err
 	}
-	chart.Triggers = triggers
+	err = FromYamlFile(path.Join(chartDir, "Actions.yaml"), &chart.Actions)
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
 	return &chart, nil
 }
 
diff --git a/appmanager/installer.go b/appmanager/installer.go
index 006fc31..7628686 100644
--- a/appmanager/installer.go
+++ b/appmanager/installer.go
@@ -7,8 +7,8 @@
 	"strings"
 )
 
-func InstallSchema(schema *Schema, apiAddr string) error {
-	if schema == nil || len(schema.Schema) == 0 {
+func InstallSchema(schema Schema, apiAddr string) error {
+	if len(schema.Schema) == 0 {
 		return nil
 	}
 	resp, err := http.Post(apiAddr, "application/text", strings.NewReader(schema.Schema))
diff --git a/appmanager/launcher.go b/appmanager/launcher.go
new file mode 100644
index 0000000..aeed5b7
--- /dev/null
+++ b/appmanager/launcher.go
@@ -0,0 +1,57 @@
+package appmanager
+
+import (
+	"bytes"
+	"context"
+	"text/template"
+
+	apiv1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/client-go/kubernetes"
+
+	"github.com/golang/glog"
+)
+
+type Launcher interface {
+	Launch(ns, tmpl string, args map[string]interface{}) error
+}
+
+type k8sLauncher struct {
+	client *kubernetes.Clientset
+}
+
+func NewK8sLauncher(client *kubernetes.Clientset) Launcher {
+	return &k8sLauncher{client}
+}
+
+func (k *k8sLauncher) Launch(ns, tmpl string, args map[string]interface{}) error {
+	pod, err := renderTemplate(tmpl, args)
+	if err != nil {
+		return err
+	}
+	pods := k.client.CoreV1().Pods(ns)
+	resp, err := pods.Create(context.TODO(), pod, metav1.CreateOptions{})
+	if err != nil {
+		return err
+	}
+	glog.Infof("Pod created: %s", resp)
+	return nil
+}
+
+func renderTemplate(tmpl string, args map[string]interface{}) (*apiv1.Pod, error) {
+	t, err := template.New("action").Parse(tmpl)
+	if err != nil {
+		return nil, err
+	}
+	var b bytes.Buffer
+	if err := t.Execute(&b, args); err != nil {
+		return nil, err
+	}
+	var pod apiv1.Pod
+	dec := yaml.NewYAMLOrJSONDecoder(&b, 100)
+	if err := dec.Decode(&pod); err != nil {
+		return nil, err
+	}
+	return &pod, nil
+}
diff --git a/appmanager/manager.go b/appmanager/manager.go
index e54e8e1..fa12c33 100644
--- a/appmanager/manager.go
+++ b/appmanager/manager.go
@@ -6,8 +6,10 @@
 )
 
 type App struct {
+	Name      string
 	Namespace string
-	Triggers  *Triggers
+	Triggers  Triggers
+	Actions   Actions
 }
 
 // TODO(giolekva): add interface
diff --git a/appmanager/schema.go b/appmanager/schema.go
index d1da046..a5774e1 100644
--- a/appmanager/schema.go
+++ b/appmanager/schema.go
@@ -1,34 +1,5 @@
 package appmanager
 
-import (
-	"io/ioutil"
-	"os"
-
-	"gopkg.in/yaml.v2"
-)
-
 type Schema struct {
 	Schema string `yaml:"schema"`
 }
-
-func SchemaFromYaml(str string) (*Schema, error) {
-	var s Schema
-	err := yaml.Unmarshal([]byte(str), &s)
-	if err != nil {
-		return nil, err
-	}
-	return &s, nil
-}
-
-func ReadSchema(schemaFile string) (*Schema, error) {
-	f, err := os.Open(schemaFile)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	b, err := ioutil.ReadAll(f)
-	if err != nil {
-		return nil, err
-	}
-	return SchemaFromYaml(string(b))
-}
diff --git a/appmanager/triggers.go b/appmanager/triggers.go
index 3eb88d9..aee109f 100644
--- a/appmanager/triggers.go
+++ b/appmanager/triggers.go
@@ -1,12 +1,5 @@
 package appmanager
 
-import (
-	"io/ioutil"
-	"os"
-
-	"gopkg.in/yaml.v2"
-)
-
 type TriggerOn struct {
 	Type  string `yaml:"type"`
 	Event string `yaml:"event"`
@@ -21,25 +14,3 @@
 type Triggers struct {
 	Triggers []Trigger `yaml:"triggers"`
 }
-
-func TriggersFromYaml(str string) (*Triggers, error) {
-	var s Triggers
-	err := yaml.Unmarshal([]byte(str), &s)
-	if err != nil {
-		return nil, err
-	}
-	return &s, nil
-}
-
-func ReadTriggers(actionsFile string) (*Triggers, error) {
-	f, err := os.Open(actionsFile)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	b, err := ioutil.ReadAll(f)
-	if err != nil {
-		return nil, err
-	}
-	return TriggersFromYaml(string(b))
-}