app-manager: support triggers
diff --git a/appmanager/cmd/main.go b/appmanager/cmd/main.go
index 149a11d..4ad45cc 100644
--- a/appmanager/cmd/main.go
+++ b/appmanager/cmd/main.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"io"
@@ -26,6 +28,7 @@
 var helmBin = flag.String("helm_bin", "/usr/local/bin/helm", "Path to the Helm binary.")
 var port = flag.Int("port", 1234, "Port to listen on.")
 var apiAddr = flag.String("api_addr", "", "PCloud API service address.")
+var managerStoreFile = flag.String("manager_store_file", "", "Persistent file containing installed application information.")
 
 var helmUploadPage = `
 <html>
@@ -42,10 +45,11 @@
 `
 
 type handler struct {
+	manager  *app.Manager
 	nsClient corev1.NamespaceInterface
 }
 
-func (hn *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (hn *handler) handleInstall(w http.ResponseWriter, r *http.Request) {
 	if r.Method == "GET" {
 		_, err := io.WriteString(w, helmUploadPage)
 		if err != nil {
@@ -82,7 +86,7 @@
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if err = installHelmChart(p, hn.nsClient); err != nil {
+		if err = hn.installHelmChart(p); err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
@@ -90,7 +94,46 @@
 	}
 }
 
-func installHelmChart(path string, nsClient corev1.NamespaceInterface) error {
+type trigger struct {
+	Namespace string `json:"namespace"`
+	Template  string `json:"template"`
+}
+
+func (hn *handler) handleTriggers(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "GET" {
+		http.Error(w, "Only GET method is supported on /triggers", http.StatusBadRequest)
+		return
+	}
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	// TODO(giolekva): check if exists
+	triggerOnType := r.Form["trigger_on_type"][0]
+	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})
+			}
+		}
+	}
+	respBody, err := json.Marshal(triggers)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if _, err := io.Copy(w, bytes.NewReader(respBody)); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (hn *handler) installHelmChart(path string) error {
 	h, err := app.HelmChartFromTar(path)
 	if err != nil {
 		return err
@@ -100,7 +143,7 @@
 	}
 	glog.Infof("Installed schema: %s", h.Schema)
 	namespace := fmt.Sprintf("app-%s", h.Name)
-	if err = createNamespace(nsClient, namespace); err != nil {
+	if err = createNamespace(hn.nsClient, namespace); err != nil {
 		return err
 	}
 	glog.Infof("Created namespaces: %s", namespace)
@@ -109,6 +152,9 @@
 		map[string]string{}); err != nil {
 		return err
 	}
+	glog.Info("Deployed")
+	hn.manager.Apps[h.Name] = app.App{namespace, h.Triggers}
+	app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
 	glog.Info("Installed")
 	return nil
 }
@@ -142,7 +188,13 @@
 		glog.Fatalf("Could not create Kubernetes API client: %v", err)
 	}
 	namespaces := clientset.CoreV1().Namespaces()
-	http.Handle("/", &handler{namespaces})
+	manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
+	if err != nil {
+		glog.Fatalf("Could ot initialize manager: %v", err)
+	}
+	h := handler{manager, namespaces}
+	http.HandleFunc("/triggers", h.handleTriggers)
+	http.HandleFunc("/", h.handleInstall)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
 
 }
diff --git a/appmanager/helm.go b/appmanager/helm.go
index db60a4b..f651fad 100644
--- a/appmanager/helm.go
+++ b/appmanager/helm.go
@@ -23,6 +23,7 @@
 	Chart
 	chartDir string
 	Schema   *Schema
+	Triggers *Triggers
 	Yamls    []string
 }
 
@@ -39,6 +40,11 @@
 		return nil, err
 	}
 	chart.Schema = schema
+	triggers, err := ReadTriggers(path.Join(chartDir, "Triggers.yaml"))
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+	chart.Triggers = triggers
 	return &chart, nil
 }
 
diff --git a/appmanager/manager.go b/appmanager/manager.go
new file mode 100644
index 0000000..e54e8e1
--- /dev/null
+++ b/appmanager/manager.go
@@ -0,0 +1,50 @@
+package appmanager
+
+import (
+	"encoding/gob"
+	"os"
+)
+
+type App struct {
+	Namespace string
+	Triggers  *Triggers
+}
+
+// TODO(giolekva): add interface
+type Manager struct {
+	Apps map[string]App
+}
+
+func NewEmptyManager() *Manager {
+	return &Manager{make(map[string]App)}
+}
+
+func LoadManagerStateFromFile(path string) (*Manager, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return NewEmptyManager(), nil
+		}
+		return nil, err
+	}
+	defer f.Close()
+	dec := gob.NewDecoder(f)
+	var m Manager
+	if err := dec.Decode(&m); err != nil {
+		return nil, err
+	}
+	return &m, nil
+}
+
+func StoreManagerStateToFile(m *Manager, path string) error {
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	enc := gob.NewEncoder(f)
+	if err := enc.Encode(*m); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/appmanager/triggers.go b/appmanager/triggers.go
new file mode 100644
index 0000000..3eb88d9
--- /dev/null
+++ b/appmanager/triggers.go
@@ -0,0 +1,45 @@
+package appmanager
+
+import (
+	"io/ioutil"
+	"os"
+
+	"gopkg.in/yaml.v2"
+)
+
+type TriggerOn struct {
+	Type  string `yaml:"type"`
+	Event string `yaml:"event"`
+}
+
+type Trigger struct {
+	Name      string    `yaml:"name"`
+	TriggerOn TriggerOn `yaml:"triggerOn"`
+	Template  string    `yaml:"template"`
+}
+
+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))
+}
diff --git a/appmanager/triggers_test.go b/appmanager/triggers_test.go
new file mode 100644
index 0000000..e23e399
--- /dev/null
+++ b/appmanager/triggers_test.go
@@ -0,0 +1,34 @@
+package appmanager
+
+import (
+	"log"
+	"testing"
+)
+
+var tmpl = `
+actions:
+- name: DetectFaces
+  triggerOn:
+    type: Image
+    event: NEW
+  template: |
+    kind: Pod
+    apiVersion: v1
+    metadata:
+      name: detect-faces-{{ .Image.Id }}
+    spec:
+      containers:
+      - name: detect-faces
+        image: giolekva/face-detector:latest
+        imagePullPolicy: Always
+        command: ["python3", "main.py"]
+        args: [{{ .PCloudApiAddr }}, {{ .ObjectStoreAddr }}, {{ .Image.Id }}]
+      restartPolicy: Never`
+
+func TestParse(t *testing.T) {
+	a, err := ActionsFromYaml(tmpl)
+	if err != nil {
+		panic(err)
+	}
+	log.Print(a)
+}