blob: b4ef21a46b2c052452b2a8368bceb5d1701e3891 [file] [log] [blame]
giolekvad5a58c42020-05-07 22:18:35 +04001package main
2
3import (
giolekva6d464592020-05-13 20:12:18 +04004 "bytes"
giolekvaebea5822020-05-12 20:52:19 +04005 "context"
giolekva6d464592020-05-13 20:12:18 +04006 "encoding/json"
giolekvaa4a153b2020-05-12 11:49:53 +04007 "flag"
8 "fmt"
9 "io"
giolekvab1f19ee2020-05-16 11:31:20 +040010 "io/ioutil"
giolekvaa4a153b2020-05-12 11:49:53 +040011 "log"
12 "net/http"
13 "os"
giolekvaebea5822020-05-12 20:52:19 +040014 "syscall"
giolekvad5a58c42020-05-07 22:18:35 +040015
giolekvafe0765f2020-05-12 14:09:09 +040016 "github.com/golang/glog"
giolekvaebea5822020-05-12 20:52:19 +040017 "github.com/google/uuid"
18 apiv1 "k8s.io/api/core/v1"
19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20 "k8s.io/client-go/kubernetes"
21 corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
22 "k8s.io/client-go/rest"
23 "k8s.io/client-go/tools/clientcmd"
giolekvaa4a153b2020-05-12 11:49:53 +040024
25 app "github.com/giolekva/pcloud/appmanager"
giolekvad5a58c42020-05-07 22:18:35 +040026)
27
giolekvaebea5822020-05-12 20:52:19 +040028var kubeconfig = flag.String("kubeconfig", "", "Absolute path to the kubeconfig file.")
giolekvab22550e2020-05-12 22:09:03 +040029var helmBin = flag.String("helm_bin", "/usr/local/bin/helm", "Path to the Helm binary.")
giolekvaa4a153b2020-05-12 11:49:53 +040030var port = flag.Int("port", 1234, "Port to listen on.")
giolekvafe0765f2020-05-12 14:09:09 +040031var apiAddr = flag.String("api_addr", "", "PCloud API service address.")
giolekva6d464592020-05-13 20:12:18 +040032var managerStoreFile = flag.String("manager_store_file", "", "Persistent file containing installed application information.")
giolekvaa4a153b2020-05-12 11:49:53 +040033
34var helmUploadPage = `
35<html>
36<head>
37 <title>Upload Helm chart</title>
38</head>
39<body>
giolekvab22550e2020-05-12 22:09:03 +040040<form enctype="multipart/form-data" method="post">
giolekvaa4a153b2020-05-12 11:49:53 +040041 <input type="file" name="chartfile" />
42 <input type="submit" value="upload" />
43</form>
44</body>
45</html>
46`
47
giolekvaebea5822020-05-12 20:52:19 +040048type handler struct {
giolekvab1f19ee2020-05-16 11:31:20 +040049 client *kubernetes.Clientset
giolekva6d464592020-05-13 20:12:18 +040050 manager *app.Manager
giolekvab1f19ee2020-05-16 11:31:20 +040051 launcher app.Launcher
giolekvaebea5822020-05-12 20:52:19 +040052}
53
giolekva6d464592020-05-13 20:12:18 +040054func (hn *handler) handleInstall(w http.ResponseWriter, r *http.Request) {
giolekvaa4a153b2020-05-12 11:49:53 +040055 if r.Method == "GET" {
56 _, err := io.WriteString(w, helmUploadPage)
57 if err != nil {
58 http.Error(w, err.Error(), http.StatusInternalServerError)
59 }
60 } else if r.Method == "POST" {
61 r.ParseMultipartForm(1000000)
62 file, handler, err := r.FormFile("chartfile")
63 if err != nil {
64 http.Error(w, err.Error(), http.StatusInternalServerError)
65 return
66 }
67 defer file.Close()
giolekvaebea5822020-05-12 20:52:19 +040068 tmp := uuid.New().String()
69 if tmp == "" {
70 http.Error(w, "Could not generate temp dir", http.StatusInternalServerError)
71 return
72 }
73 p := "/tmp/" + tmp
74 // TODO(giolekva): defer rmdir
75 if err := syscall.Mkdir(p, 0777); err != nil {
76 http.Error(w, "Could not create temp dir", http.StatusInternalServerError)
77 return
78 }
79 p += "/" + handler.Filename
giolekvaa4a153b2020-05-12 11:49:53 +040080 f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE, 0666)
81 if err != nil {
82 fmt.Println(err)
83 return
84 }
85 defer f.Close()
86 _, err = io.Copy(f, file)
87 if err != nil {
88 http.Error(w, err.Error(), http.StatusInternalServerError)
89 return
90 }
giolekva6d464592020-05-13 20:12:18 +040091 if err = hn.installHelmChart(p); err != nil {
giolekvaa4a153b2020-05-12 11:49:53 +040092 http.Error(w, err.Error(), http.StatusInternalServerError)
93 return
94 }
95 w.Write([]byte("Installed"))
giolekvad5a58c42020-05-07 22:18:35 +040096 }
giolekvaa4a153b2020-05-12 11:49:53 +040097}
98
giolekva6d464592020-05-13 20:12:18 +040099type trigger struct {
giolekvad6cbd8f2020-05-16 13:52:01 +0400100 App string `json:"app"`
101 Action string `json:"action"`
giolekva6d464592020-05-13 20:12:18 +0400102}
103
104func (hn *handler) handleTriggers(w http.ResponseWriter, r *http.Request) {
105 if r.Method != "GET" {
106 http.Error(w, "Only GET method is supported on /triggers", http.StatusBadRequest)
107 return
108 }
109 if err := r.ParseForm(); err != nil {
110 http.Error(w, err.Error(), http.StatusBadRequest)
111 return
112 }
113 // TODO(giolekva): check if exists
114 triggerOnType := r.Form["trigger_on_type"][0]
115 triggerOnEvent := r.Form["trigger_on_event"][0]
116 var triggers []trigger
117 for _, a := range hn.manager.Apps {
giolekva6d464592020-05-13 20:12:18 +0400118 for _, t := range a.Triggers.Triggers {
119 if t.TriggerOn.Type == triggerOnType && t.TriggerOn.Event == triggerOnEvent {
giolekvad6cbd8f2020-05-16 13:52:01 +0400120 triggers = append(triggers, trigger{a.Name, t.Action})
giolekva6d464592020-05-13 20:12:18 +0400121 }
122 }
123 }
124 respBody, err := json.Marshal(triggers)
125 if err != nil {
126 http.Error(w, err.Error(), http.StatusInternalServerError)
127 return
128 }
129 if _, err := io.Copy(w, bytes.NewReader(respBody)); err != nil {
130 http.Error(w, err.Error(), http.StatusInternalServerError)
131 return
132 }
133}
134
giolekvab1f19ee2020-05-16 11:31:20 +0400135type actionReq struct {
136 App string `json:"app"`
137 Action string `json:"action"`
138 Args map[string]interface{} `json:"args"`
139}
140
141func (hn *handler) handleLaunchAction(w http.ResponseWriter, r *http.Request) {
142 actionStr, err := ioutil.ReadAll(r.Body)
143 if err != nil {
144 http.Error(w, err.Error(), http.StatusInternalServerError)
145 return
146 }
147 var req actionReq
148 if err := json.Unmarshal(actionStr, &req); err != nil {
149 http.Error(w, err.Error(), http.StatusBadRequest)
150 return
151 }
giolekva1c0372c2020-05-16 21:18:59 +0400152 if err := hn.launchAction(req); err != nil {
153 http.Error(w, err.Error(), http.StatusBadRequest)
154 return
155 }
156}
157
158func (hn *handler) launchAction(req actionReq) error {
giolekvab1f19ee2020-05-16 11:31:20 +0400159 for _, a := range hn.manager.Apps {
160 if a.Name != req.App {
161 continue
162 }
163 for _, action := range a.Actions.Actions {
giolekva1c0372c2020-05-16 21:18:59 +0400164 if action.Name == req.Action {
165 return hn.launcher.Launch(a.Namespace, action.Template, req.Args)
giolekvab1f19ee2020-05-16 11:31:20 +0400166 }
giolekvab1f19ee2020-05-16 11:31:20 +0400167 }
168 }
giolekva1c0372c2020-05-16 21:18:59 +0400169 return fmt.Errorf("Action not found: %s %s", req.App, req.Action)
giolekvab1f19ee2020-05-16 11:31:20 +0400170}
171
giolekva6d464592020-05-13 20:12:18 +0400172func (hn *handler) installHelmChart(path string) error {
giolekvaebea5822020-05-12 20:52:19 +0400173 h, err := app.HelmChartFromTar(path)
giolekvaa4a153b2020-05-12 11:49:53 +0400174 if err != nil {
175 return err
176 }
giolekvaebea5822020-05-12 20:52:19 +0400177 if err = app.InstallSchema(h.Schema, *apiAddr); err != nil {
giolekvafe0765f2020-05-12 14:09:09 +0400178 return err
179 }
180 glog.Infof("Installed schema: %s", h.Schema)
giolekvaebea5822020-05-12 20:52:19 +0400181 namespace := fmt.Sprintf("app-%s", h.Name)
giolekvab1f19ee2020-05-16 11:31:20 +0400182 err = createNamespace(hn.client.CoreV1().Namespaces(), namespace)
183 if err != nil {
giolekvaebea5822020-05-12 20:52:19 +0400184 return err
185 }
186 glog.Infof("Created namespaces: %s", namespace)
giolekva30036e72020-05-13 22:00:40 +0400187 if h.Type == "application" {
188 if err = h.Install(
189 *helmBin,
190 map[string]string{}); err != nil {
191 return err
192 }
193 glog.Info("Deployed")
194 } else {
195 glog.Info("Skipping deployment as we got library chart.")
giolekvaebea5822020-05-12 20:52:19 +0400196 }
giolekvab1f19ee2020-05-16 11:31:20 +0400197 hn.manager.Apps[h.Name] = app.App{h.Name, namespace, h.Triggers, h.Actions}
giolekva6d464592020-05-13 20:12:18 +0400198 app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
giolekva1c0372c2020-05-16 21:18:59 +0400199 for _, a := range h.Init.PostInstall.CallAction {
200 if err := hn.launchAction(actionReq{a.App, a.Action, a.Args}); err != nil {
201 return err
202 }
203 }
giolekvaebea5822020-05-12 20:52:19 +0400204 glog.Info("Installed")
205 return nil
206}
207
208func createNamespace(nsClient corev1.NamespaceInterface, name string) error {
209 _, err := nsClient.Create(
210 context.TODO(),
211 &apiv1.Namespace{
212 ObjectMeta: metav1.ObjectMeta{
213 Name: name}},
214 metav1.CreateOptions{})
giolekvaa4a153b2020-05-12 11:49:53 +0400215 return err
216}
217
giolekvaebea5822020-05-12 20:52:19 +0400218func getKubeConfig() (*rest.Config, error) {
219 if *kubeconfig != "" {
220 return clientcmd.BuildConfigFromFlags("", *kubeconfig)
221 } else {
222 return rest.InClusterConfig()
223 }
224}
225
giolekvaa4a153b2020-05-12 11:49:53 +0400226func main() {
227 flag.Parse()
giolekvaebea5822020-05-12 20:52:19 +0400228 config, err := getKubeConfig()
229 if err != nil {
230 glog.Fatalf("Could not initialize Kubeconfig: %v", err)
231 }
232 clientset, err := kubernetes.NewForConfig(config)
233 if err != nil {
234 glog.Fatalf("Could not create Kubernetes API client: %v", err)
235 }
giolekva6d464592020-05-13 20:12:18 +0400236 manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
237 if err != nil {
238 glog.Fatalf("Could ot initialize manager: %v", err)
239 }
giolekvad6cbd8f2020-05-16 13:52:01 +0400240 glog.Info(manager)
giolekvab1f19ee2020-05-16 11:31:20 +0400241 h := handler{clientset, manager, app.NewK8sLauncher(clientset)}
giolekva6d464592020-05-13 20:12:18 +0400242 http.HandleFunc("/triggers", h.handleTriggers)
giolekvab1f19ee2020-05-16 11:31:20 +0400243 http.HandleFunc("/launch_action", h.handleLaunchAction)
giolekva6d464592020-05-13 20:12:18 +0400244 http.HandleFunc("/", h.handleInstall)
giolekvaa4a153b2020-05-12 11:49:53 +0400245 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
246
giolekvad5a58c42020-05-07 22:18:35 +0400247}