appmanager: move to core/appmanager
diff --git a/core/appmanager/BUILD b/core/appmanager/BUILD
new file mode 100644
index 0000000..ac84729
--- /dev/null
+++ b/core/appmanager/BUILD
@@ -0,0 +1,32 @@
+load("//:bazel_tools/docker.bzl", "docker_image")
+load("//:bazel_tools/helm.bzl", "helm_install")
+load("@rules_pkg//:pkg.bzl", "pkg_tar")
+
+# TODO(lekva): figure out how to build py_binary with pip dependencies and
+# migrate off docker_image rule
+docker_image(
+ name = "push_to_dev",
+ registry = "localhost:30500",
+ image = "giolekva/pcloud-app-manager",
+ tag = "latest",
+ dockerfile = "Dockerfile",
+ srcs = glob(["**"], exclude=["Dockerfile"]),
+)
+
+pkg_tar(
+ name = "chart",
+ srcs = glob(["chart/**"]),
+ extension = "tar.gz",
+ strip_prefix = "./chart",
+)
+
+helm_install(
+ name = "install",
+ namespace = "pcloud-app-manager",
+ release_name = "init",
+ chart = ":chart",
+ args = {
+ "image.name": "localhost:30500/giolekva/pcloud-app-manager",
+ "image.pullPolicy": "Always",
+ },
+)
diff --git a/core/appmanager/Dockerfile b/core/appmanager/Dockerfile
new file mode 100644
index 0000000..791ab67
--- /dev/null
+++ b/core/appmanager/Dockerfile
@@ -0,0 +1,23 @@
+FROM golang:1-alpine AS build
+
+ARG GOOS=linux
+ARG GOARCH=amd64
+ARG CGO_ENABLED=0
+ARG GO111MODULE=on
+
+RUN apk update && apk upgrade && \
+ apk add --no-cache bash git openssh
+
+WORKDIR /helm
+RUN wget -O helm.tar.gz https://get.helm.sh/helm-v3.2.1-$GOOS-$GOARCH.tar.gz
+RUN tar -xvf helm.tar.gz
+
+WORKDIR $GOPATH/src/github.com/giolekva/pcloud/core/appmanager
+COPY . .
+RUN go build -o $GOPATH/bin/app-manager -trimpath -ldflags="-s -w" cmd/main.go
+
+FROM alpine:latest
+COPY --from=build /go/bin/app-manager /usr/bin
+RUN chmod a+x /usr/bin/app-manager
+COPY --from=build /helm/*/helm /usr/bin/helm
+RUN chmod a+x /usr/bin/helm
diff --git a/core/appmanager/actions.go b/core/appmanager/actions.go
new file mode 100644
index 0000000..65fb061
--- /dev/null
+++ b/core/appmanager/actions.go
@@ -0,0 +1,48 @@
+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"`
+}
+
+type CallAction struct {
+ App string `yaml:"app"`
+ Action string `yaml:"action"`
+ Args map[string]interface{} `yaml:"args"`
+}
+
+type PostInstall struct {
+ CallAction []CallAction `yaml:"callAction"`
+}
+
+type Init struct {
+ PostInstall PostInstall `yaml:"postInstall"`
+}
+
+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/core/appmanager/chart/Chart.yaml b/core/appmanager/chart/Chart.yaml
new file mode 100644
index 0000000..579f098
--- /dev/null
+++ b/core/appmanager/chart/Chart.yaml
@@ -0,0 +1,10 @@
+apiVersion: v2
+name: pcloud-app-manager
+version: 0.0.1
+description: PCloud Application Manager
+type: application
+sources:
+ - https://github.com/giolekva/pcloud/tree/master/appmanager
+mainteners:
+ - name: Giorgi Lekveishvili
+ url: https://github.com/giolekva
diff --git a/core/appmanager/chart/README.md b/core/appmanager/chart/README.md
new file mode 100644
index 0000000..cd71449
--- /dev/null
+++ b/core/appmanager/chart/README.md
@@ -0,0 +1,3 @@
+# PCloud Application Manager
+
+Provides backend and web frontend for installing application on PCloud environment.
\ No newline at end of file
diff --git a/core/appmanager/chart/templates/cluster-role-binding.yaml b/core/appmanager/chart/templates/cluster-role-binding.yaml
new file mode 100644
index 0000000..c156933
--- /dev/null
+++ b/core/appmanager/chart/templates/cluster-role-binding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: deploy-apps-to-sa
+subjects:
+ - kind: ServiceAccount
+ name: default
+ namespace: {{ .Release.Namespace }}
+roleRef:
+ kind: ClusterRole
+ name: deploy-apps
+ apiGroup: rbac.authorization.k8s.io
diff --git a/core/appmanager/chart/templates/cluster-role.yaml b/core/appmanager/chart/templates/cluster-role.yaml
new file mode 100644
index 0000000..efe4d3c
--- /dev/null
+++ b/core/appmanager/chart/templates/cluster-role.yaml
@@ -0,0 +1,17 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: deploy-apps
+rules:
+ - apiGroups: [""]
+ resources: ["namespaces", "services", "pods", "secrets", "serviceaccounts", "configmaps", "persistentvolumeclaims"]
+ verbs: ["*"]
+ - apiGroups: ["apps"]
+ resources: ["deployments", "statefulsets"]
+ verbs: ["*"]
+ - apiGroups: ["traefik.containo.us"]
+ resources: ["ingressroutes"]
+ verbs: ["*"]
+ - apiGroups: ["rbac.authorization.k8s.io"]
+ resources: ["roles", "rolebindings"]
+ verbs: ["*"]
diff --git a/core/appmanager/chart/templates/ingress.yaml b/core/appmanager/chart/templates/ingress.yaml
new file mode 100644
index 0000000..c37ada0
--- /dev/null
+++ b/core/appmanager/chart/templates/ingress.yaml
@@ -0,0 +1,17 @@
+apiVersion: traefik.containo.us/v1alpha1
+kind: IngressRoute
+metadata:
+ name: app-manager-ingress
+ namespace: {{ .Release.Namespace }}
+spec:
+ entryPoints:
+ - web
+ routes:
+ - kind: Rule
+ match: PathPrefix(`{{ .Values.ingressPathPrefix }}`)
+ services:
+ - kind: Service
+ name: {{ .Values.serviceName }}
+ namespace: {{ .Release.Namespace }}
+ passHostHeader: true
+ port: {{ .Values.servicePort }}
diff --git a/core/appmanager/chart/templates/service.yaml b/core/appmanager/chart/templates/service.yaml
new file mode 100644
index 0000000..010a915
--- /dev/null
+++ b/core/appmanager/chart/templates/service.yaml
@@ -0,0 +1,13 @@
+kind: Service
+apiVersion: v1
+metadata:
+ name: {{ .Values.serviceName }}
+ namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: app-manager
+ ports:
+ - nodePort:
+ port: {{ .Values.servicePort }}
+ targetPort: {{ .Values.containerPort }}
diff --git a/core/appmanager/chart/templates/statefulset.yaml b/core/appmanager/chart/templates/statefulset.yaml
new file mode 100644
index 0000000..474d486
--- /dev/null
+++ b/core/appmanager/chart/templates/statefulset.yaml
@@ -0,0 +1,36 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: app-manager
+ namespace: {{ .Release.Namespace }}
+spec:
+ selector:
+ matchLabels:
+ app: app-manager
+ serviceName: {{ .Values.serviceName }}
+ replicas: {{ .Values.replicas }}
+ template:
+ metadata:
+ labels:
+ app: app-manager
+ spec:
+ serviceAccountName: default
+ containers:
+ - name: app-manager
+ image: {{ .Values.image.name }}:{{ .Values.image.tag }}
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ volumeMounts:
+ - name: state
+ mountPath: /pcloud/app-manager
+ ports:
+ - containerPort: {{ .Values.containerPort }}
+ command: ["app-manager", "--logtostderr", "--port={{ .Values.containerPort }}", "--api_addr=http://api.pcloud.svc:1111/add_schema", "--helm_bin=/usr/bin/helm", "--manager_store_file=/pcloud/app-manager/manager-state"]
+ volumeClaimTemplates:
+ - metadata:
+ name: state
+ spec:
+ accessModes: [ "ReadWriteOnce" ]
+ storageClassName: {{ .Values.storage.className }}
+ resources:
+ requests:
+ storage: {{ .Values.storage.size }}
diff --git a/core/appmanager/chart/values.yaml b/core/appmanager/chart/values.yaml
new file mode 100644
index 0000000..8883eba
--- /dev/null
+++ b/core/appmanager/chart/values.yaml
@@ -0,0 +1,12 @@
+serviceName: app-manager
+replicas: 1
+image:
+ name: giolekva/pcloud-app-manager
+ tag: latest
+ pullPolicy: Always
+servicePort: 80
+containerPort: 1234
+ingressPathPrefix: /app-manager
+storage:
+ size: 10Mi
+ className: "" # local-path
\ No newline at end of file
diff --git a/core/appmanager/cmd/main.go b/core/appmanager/cmd/main.go
new file mode 100644
index 0000000..8d6695d
--- /dev/null
+++ b/core/appmanager/cmd/main.go
@@ -0,0 +1,250 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "syscall"
+
+ "github.com/golang/glog"
+ "github.com/google/uuid"
+ apiv1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+
+ app "github.com/giolekva/pcloud/core/appmanager"
+)
+
+var kubeconfig = flag.String("kubeconfig", "", "Absolute path to the kubeconfig file.")
+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>
+<head>
+ <title>Upload Helm chart</title>
+</head>
+<body>
+<form enctype="multipart/form-data" method="post">
+ <input type="file" name="chartfile" />
+ <input type="submit" value="upload" />
+</form>
+</body>
+</html>
+`
+
+type handler struct {
+ client *kubernetes.Clientset
+ manager *app.Manager
+ launcher app.Launcher
+}
+
+func (hn *handler) handleInstall(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "GET" {
+ _, err := io.WriteString(w, helmUploadPage)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ } else if r.Method == "POST" {
+ r.ParseMultipartForm(1000000)
+ file, handler, err := r.FormFile("chartfile")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer file.Close()
+ tmp := uuid.New().String()
+ if tmp == "" {
+ http.Error(w, "Could not generate temp dir", http.StatusInternalServerError)
+ return
+ }
+ p := "/tmp/" + tmp
+ // TODO(giolekva): defer rmdir
+ if err := syscall.Mkdir(p, 0777); err != nil {
+ http.Error(w, "Could not create temp dir", http.StatusInternalServerError)
+ return
+ }
+ p += "/" + handler.Filename
+ f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE, 0666)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ defer f.Close()
+ _, err = io.Copy(f, file)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err = hn.installHelmChart(p); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("Installed"))
+ }
+}
+
+type trigger struct {
+ App string `json:"app"`
+ Action string `json:"action"`
+}
+
+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 {
+ for _, t := range a.Triggers.Triggers {
+ if t.TriggerOn.Type == triggerOnType && t.TriggerOn.Event == triggerOnEvent {
+ triggers = append(triggers, trigger{a.Name, t.Action})
+ }
+ }
+ }
+ 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
+ }
+}
+
+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
+ }
+ if err := hn.launchAction(req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+}
+
+func (hn *handler) launchAction(req actionReq) error {
+ for _, a := range hn.manager.Apps {
+ if a.Name != req.App {
+ continue
+ }
+ for _, action := range a.Actions.Actions {
+ if action.Name == req.Action {
+ return hn.launcher.Launch(a.Namespace, action.Template, req.Args)
+ }
+ }
+ }
+ return fmt.Errorf("Action not found: %s %s", req.App, req.Action)
+}
+
+func (hn *handler) installHelmChart(path string) error {
+ h, err := app.HelmChartFromTar(path)
+ if err != nil {
+ return err
+ }
+ if err := h.Render(
+ *helmBin,
+ map[string]string{}); err != nil {
+ return err
+ }
+ glog.Info("Rendered templates")
+ if err = app.InstallSchema(h.Schema, *apiAddr); err != nil {
+ return err
+ }
+ glog.Infof("Installed schema: %s", h.Schema)
+ err = createNamespace(hn.client.CoreV1().Namespaces(), h.Namespace)
+ if err != nil {
+ return err
+ }
+ glog.Infof("Created namespaces: %s", h.Namespace)
+ if h.Type == "application" {
+ if err := h.Install(*helmBin); err != nil {
+ return err
+ }
+ glog.Info("Deployed")
+ } else {
+ glog.Info("Skipping deployment as we got library chart.")
+ }
+ hn.manager.Apps[h.Name] = app.App{h.Name, h.Namespace, h.Triggers, h.Actions}
+ app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
+ for _, a := range h.Init.PostInstall.CallAction {
+ if err := hn.launchAction(actionReq{a.App, a.Action, a.Args}); err != nil {
+ return err
+ }
+ }
+ glog.Info("Installed")
+ return nil
+}
+
+func createNamespace(nsClient corev1.NamespaceInterface, name string) error {
+ _, err := nsClient.Create(
+ context.TODO(),
+ &apiv1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name}},
+ metav1.CreateOptions{})
+ return err
+}
+
+func getKubeConfig() (*rest.Config, error) {
+ if *kubeconfig != "" {
+ return clientcmd.BuildConfigFromFlags("", *kubeconfig)
+ } else {
+ return rest.InClusterConfig()
+ }
+}
+
+func main() {
+ flag.Parse()
+ config, err := getKubeConfig()
+ if err != nil {
+ glog.Fatalf("Could not initialize Kubeconfig: %v", err)
+ }
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ glog.Fatalf("Could not create Kubernetes API client: %v", err)
+ }
+ manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
+ if err != nil {
+ glog.Fatalf("Could ot initialize manager: %v", err)
+ }
+ glog.Info(manager)
+ 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/core/appmanager/go.mod b/core/appmanager/go.mod
new file mode 100644
index 0000000..96a749b
--- /dev/null
+++ b/core/appmanager/go.mod
@@ -0,0 +1,12 @@
+module github.com/giolekva/pcloud/core/appmanager
+
+go 1.14
+
+require (
+ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
+ gopkg.in/yaml.v2 v2.2.8
+ k8s.io/api v0.18.2
+ k8s.io/apimachinery v0.18.2
+ k8s.io/client-go v0.18.2
+
+)
diff --git a/core/appmanager/go.sum b/core/appmanager/go.sum
new file mode 100644
index 0000000..ba5ed77
--- /dev/null
+++ b/core/appmanager/go.sum
@@ -0,0 +1,227 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI=
+github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
+github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
+golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
+gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8=
+k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
+k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA=
+k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
+k8s.io/client-go v0.18.2 h1:aLB0iaD4nmwh7arT2wIn+lMnAq7OswjaejkQ8p9bBYE=
+k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU=
+k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E=
+k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
+k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
+k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+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 v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
+k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
+k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
+k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 h1:Ly1Oxdu5p5ZFmiVT71LFgeZETvMfZ1iBIGeOenT2JeM=
+k8s.io/utils v0.0.0-20200414100711-2df71ebbae66/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/core/appmanager/helm.go b/core/appmanager/helm.go
new file mode 100644
index 0000000..894c96c
--- /dev/null
+++ b/core/appmanager/helm.go
@@ -0,0 +1,141 @@
+package appmanager
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/golang/glog"
+)
+
+type Chart struct {
+ Name string `yaml:"name"`
+ Type string `yaml:"type"`
+}
+
+type HelmChart struct {
+ Chart
+ Dir string
+ Namespace string
+ Schema Schema
+ Triggers Triggers
+ Actions Actions
+ Init Init
+ Yamls []string
+}
+
+func HelmChartFromDir(dir string) (*HelmChart, error) {
+ var chart HelmChart
+ chart.Dir = dir
+ err := FromYamlFile(path.Join(dir, "Chart.yaml"), &chart.Chart)
+ if err != nil {
+ return nil, err
+ }
+ return &chart, nil
+}
+
+func (chart *HelmChart) Render(
+ helmBin string,
+ values map[string]string) error {
+ chart.Namespace = fmt.Sprintf("app-%s", chart.Name)
+ renderDir := path.Join(chart.Dir, "__render")
+ if err := chart.renderTemplates(helmBin, values, renderDir); err != nil {
+ return err
+ }
+ if err := os.RemoveAll(path.Join(chart.Dir, "templates")); err != nil {
+ return err
+ }
+ if err := os.Rename(
+ path.Join(renderDir, chart.Name, "templates"),
+ path.Join(chart.Dir, "templates")); err != nil {
+ return err
+ }
+ pcloudDir := path.Join(chart.Dir, "templates/pcloud")
+ err := FromYamlFile(path.Join(pcloudDir, "Schema.yaml"), &chart.Schema)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ err = FromYamlFile(path.Join(pcloudDir, "Triggers.yaml"), &chart.Triggers)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ err = FromYamlFile(path.Join(pcloudDir, "Actions.yaml"), &chart.Actions)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ err = FromYamlFile(path.Join(pcloudDir, "Init.yaml"), &chart.Init)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err := os.RemoveAll(pcloudDir); err != nil {
+ return err
+ }
+ return nil
+}
+
+func HelmChartFromTar(chartTar string) (*HelmChart, error) {
+ if !strings.HasSuffix(chartTar, ".tar.gz") {
+ return nil, errors.New("Expected .tar.gz file")
+ }
+ dir := filepath.Dir(chartTar)
+ archive := filepath.Base(chartTar)
+ if err := os.Chdir(dir); err != nil {
+ return nil, err
+ }
+ cmd := exec.Command("tar", "-xvf", archive)
+ var stdout strings.Builder
+ var stderr strings.Builder
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ if err := cmd.Run(); err != nil {
+ return nil, errors.New(stderr.String())
+ }
+ glog.Info(stdout.String())
+ glog.Info(dir)
+ return HelmChartFromDir(dir)
+}
+
+func (chart *HelmChart) Install(
+ helmBin string) error {
+ cmd := exec.Command(helmBin)
+ cmd.Args = append(cmd.Args, "install")
+ cmd.Args = append(cmd.Args, fmt.Sprintf("--namespace=%s", chart.Namespace))
+ cmd.Args = append(cmd.Args, chart.Name)
+ cmd.Args = append(cmd.Args, fmt.Sprintf("%s", chart.Dir))
+ return runCmd(cmd)
+}
+
+func (chart *HelmChart) renderTemplates(
+ helmBin string,
+ values map[string]string,
+ outputDir string) error {
+ cmd := exec.Command(helmBin)
+ cmd.Args = append(cmd.Args, "template")
+ cmd.Args = append(cmd.Args, fmt.Sprintf("--output-dir=%s", outputDir))
+ cmd.Args = append(cmd.Args, fmt.Sprintf("--namespace=%s", chart.Namespace))
+ cmd.Args = append(cmd.Args, chart.Name)
+ cmd.Args = append(cmd.Args, chart.Dir)
+ // TODO(giolekva): validate values
+ for key, value := range values {
+ cmd.Args = append(cmd.Args, fmt.Sprintf("--set=%s=%s", key, value))
+ }
+ return runCmd(cmd)
+}
+
+func runCmd(cmd *exec.Cmd) error {
+ glog.Info(cmd.String())
+ var stdout strings.Builder
+ var stderr strings.Builder
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ err := cmd.Run()
+ if err != nil {
+ return errors.New(stderr.String())
+ }
+ glog.Info(stdout.String())
+ return nil
+}
diff --git a/core/appmanager/installer.go b/core/appmanager/installer.go
new file mode 100644
index 0000000..7628686
--- /dev/null
+++ b/core/appmanager/installer.go
@@ -0,0 +1,23 @@
+package appmanager
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+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))
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ body, _ := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("Failed request with status code: %d %s", resp.StatusCode, string(body))
+ }
+ return nil
+}
diff --git a/core/appmanager/launcher.go b/core/appmanager/launcher.go
new file mode 100644
index 0000000..e1cec0b
--- /dev/null
+++ b/core/appmanager/launcher.go
@@ -0,0 +1,60 @@
+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"
+)
+
+var leftDelim = "{-{"
+var rightDelim = "}-}"
+
+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").Delims(leftDelim, rightDelim).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/core/appmanager/manager.go b/core/appmanager/manager.go
new file mode 100644
index 0000000..fa12c33
--- /dev/null
+++ b/core/appmanager/manager.go
@@ -0,0 +1,52 @@
+package appmanager
+
+import (
+ "encoding/gob"
+ "os"
+)
+
+type App struct {
+ Name string
+ Namespace string
+ Triggers Triggers
+ Actions Actions
+}
+
+// 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/core/appmanager/schema.go b/core/appmanager/schema.go
new file mode 100644
index 0000000..a5774e1
--- /dev/null
+++ b/core/appmanager/schema.go
@@ -0,0 +1,5 @@
+package appmanager
+
+type Schema struct {
+ Schema string `yaml:"schema"`
+}
diff --git a/core/appmanager/triggers.go b/core/appmanager/triggers.go
new file mode 100644
index 0000000..f0611d5
--- /dev/null
+++ b/core/appmanager/triggers.go
@@ -0,0 +1,16 @@
+package appmanager
+
+type TriggerOn struct {
+ Type string `yaml:"type"`
+ Event string `yaml:"event"`
+}
+
+type Trigger struct {
+ Name string `yaml:"name"`
+ TriggerOn TriggerOn `yaml:"triggerOn"`
+ Action string `yaml:"action"`
+}
+
+type Triggers struct {
+ Triggers []Trigger `yaml:"triggers"`
+}
diff --git a/core/appmanager/triggers_test.go b/core/appmanager/triggers_test.go
new file mode 100644
index 0000000..e23e399
--- /dev/null
+++ b/core/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)
+}