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)
+}