blob: 8d6695d713bdef333365f61873cb4047ffa51b96 [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
giolekva9fb5b0a2020-12-16 11:43:46 +040025 app "github.com/giolekva/pcloud/core/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 }
giolekvadaa85592020-05-16 23:32:02 +0400177 if err := h.Render(
178 *helmBin,
179 map[string]string{}); err != nil {
180 return err
181 }
182 glog.Info("Rendered templates")
giolekvaebea5822020-05-12 20:52:19 +0400183 if err = app.InstallSchema(h.Schema, *apiAddr); err != nil {
giolekvafe0765f2020-05-12 14:09:09 +0400184 return err
185 }
186 glog.Infof("Installed schema: %s", h.Schema)
giolekvadaa85592020-05-16 23:32:02 +0400187 err = createNamespace(hn.client.CoreV1().Namespaces(), h.Namespace)
giolekvab1f19ee2020-05-16 11:31:20 +0400188 if err != nil {
giolekvaebea5822020-05-12 20:52:19 +0400189 return err
190 }
giolekvadaa85592020-05-16 23:32:02 +0400191 glog.Infof("Created namespaces: %s", h.Namespace)
giolekva30036e72020-05-13 22:00:40 +0400192 if h.Type == "application" {
giolekvadaa85592020-05-16 23:32:02 +0400193 if err := h.Install(*helmBin); err != nil {
giolekva30036e72020-05-13 22:00:40 +0400194 return err
195 }
196 glog.Info("Deployed")
197 } else {
198 glog.Info("Skipping deployment as we got library chart.")
giolekvaebea5822020-05-12 20:52:19 +0400199 }
giolekvadaa85592020-05-16 23:32:02 +0400200 hn.manager.Apps[h.Name] = app.App{h.Name, h.Namespace, h.Triggers, h.Actions}
giolekva6d464592020-05-13 20:12:18 +0400201 app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
giolekva1c0372c2020-05-16 21:18:59 +0400202 for _, a := range h.Init.PostInstall.CallAction {
203 if err := hn.launchAction(actionReq{a.App, a.Action, a.Args}); err != nil {
204 return err
205 }
206 }
giolekvaebea5822020-05-12 20:52:19 +0400207 glog.Info("Installed")
208 return nil
209}
210
211func createNamespace(nsClient corev1.NamespaceInterface, name string) error {
212 _, err := nsClient.Create(
213 context.TODO(),
214 &apiv1.Namespace{
215 ObjectMeta: metav1.ObjectMeta{
216 Name: name}},
217 metav1.CreateOptions{})
giolekvaa4a153b2020-05-12 11:49:53 +0400218 return err
219}
220
giolekvaebea5822020-05-12 20:52:19 +0400221func getKubeConfig() (*rest.Config, error) {
222 if *kubeconfig != "" {
223 return clientcmd.BuildConfigFromFlags("", *kubeconfig)
224 } else {
225 return rest.InClusterConfig()
226 }
227}
228
giolekvaa4a153b2020-05-12 11:49:53 +0400229func main() {
230 flag.Parse()
giolekvaebea5822020-05-12 20:52:19 +0400231 config, err := getKubeConfig()
232 if err != nil {
233 glog.Fatalf("Could not initialize Kubeconfig: %v", err)
234 }
235 clientset, err := kubernetes.NewForConfig(config)
236 if err != nil {
237 glog.Fatalf("Could not create Kubernetes API client: %v", err)
238 }
giolekva6d464592020-05-13 20:12:18 +0400239 manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
240 if err != nil {
241 glog.Fatalf("Could ot initialize manager: %v", err)
242 }
giolekvad6cbd8f2020-05-16 13:52:01 +0400243 glog.Info(manager)
giolekvab1f19ee2020-05-16 11:31:20 +0400244 h := handler{clientset, manager, app.NewK8sLauncher(clientset)}
giolekva6d464592020-05-13 20:12:18 +0400245 http.HandleFunc("/triggers", h.handleTriggers)
giolekvab1f19ee2020-05-16 11:31:20 +0400246 http.HandleFunc("/launch_action", h.handleLaunchAction)
giolekva6d464592020-05-13 20:12:18 +0400247 http.HandleFunc("/", h.handleInstall)
giolekvaa4a153b2020-05-12 11:49:53 +0400248 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
249
giolekvad5a58c42020-05-07 22:18:35 +0400250}