blob: 9bf0b4edd079d3a883ffa9814660892b8fef9457 [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 {
100 Namespace string `json:"namespace"`
101 Template string `json:"template"`
102}
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 {
120 triggers = append(triggers, trigger{a.Namespace, t.Template})
121 }
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 }
152 for _, a := range hn.manager.Apps {
153 if a.Name != req.App {
154 continue
155 }
156 for _, action := range a.Actions.Actions {
157 if action.Name != req.Action {
158 continue
159 }
160 err := hn.launcher.Launch(a.Namespace, action.Template, req.Args)
161 if err != nil {
162 http.Error(w, err.Error(), http.StatusInternalServerError)
163 }
164 return
165 }
166 }
167 http.Error(
168 w,
169 fmt.Sprintf("Application action not found: %s %s", req.App, req.Action),
170 http.StatusBadRequest)
171}
172
giolekva6d464592020-05-13 20:12:18 +0400173func (hn *handler) installHelmChart(path string) error {
giolekvaebea5822020-05-12 20:52:19 +0400174 h, err := app.HelmChartFromTar(path)
giolekvaa4a153b2020-05-12 11:49:53 +0400175 if err != nil {
176 return err
177 }
giolekvaebea5822020-05-12 20:52:19 +0400178 if err = app.InstallSchema(h.Schema, *apiAddr); err != nil {
giolekvafe0765f2020-05-12 14:09:09 +0400179 return err
180 }
181 glog.Infof("Installed schema: %s", h.Schema)
giolekvaebea5822020-05-12 20:52:19 +0400182 namespace := fmt.Sprintf("app-%s", h.Name)
giolekvab1f19ee2020-05-16 11:31:20 +0400183 err = createNamespace(hn.client.CoreV1().Namespaces(), namespace)
184 if err != nil {
giolekvaebea5822020-05-12 20:52:19 +0400185 return err
186 }
187 glog.Infof("Created namespaces: %s", namespace)
giolekva30036e72020-05-13 22:00:40 +0400188 if h.Type == "application" {
189 if err = h.Install(
190 *helmBin,
191 map[string]string{}); err != nil {
192 return err
193 }
194 glog.Info("Deployed")
195 } else {
196 glog.Info("Skipping deployment as we got library chart.")
giolekvaebea5822020-05-12 20:52:19 +0400197 }
giolekvab1f19ee2020-05-16 11:31:20 +0400198 hn.manager.Apps[h.Name] = app.App{h.Name, namespace, h.Triggers, h.Actions}
giolekva6d464592020-05-13 20:12:18 +0400199 app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
giolekvaebea5822020-05-12 20:52:19 +0400200 glog.Info("Installed")
201 return nil
202}
203
204func createNamespace(nsClient corev1.NamespaceInterface, name string) error {
205 _, err := nsClient.Create(
206 context.TODO(),
207 &apiv1.Namespace{
208 ObjectMeta: metav1.ObjectMeta{
209 Name: name}},
210 metav1.CreateOptions{})
giolekvaa4a153b2020-05-12 11:49:53 +0400211 return err
212}
213
giolekvaebea5822020-05-12 20:52:19 +0400214func getKubeConfig() (*rest.Config, error) {
215 if *kubeconfig != "" {
216 return clientcmd.BuildConfigFromFlags("", *kubeconfig)
217 } else {
218 return rest.InClusterConfig()
219 }
220}
221
giolekvaa4a153b2020-05-12 11:49:53 +0400222func main() {
223 flag.Parse()
giolekvaebea5822020-05-12 20:52:19 +0400224 config, err := getKubeConfig()
225 if err != nil {
226 glog.Fatalf("Could not initialize Kubeconfig: %v", err)
227 }
228 clientset, err := kubernetes.NewForConfig(config)
229 if err != nil {
230 glog.Fatalf("Could not create Kubernetes API client: %v", err)
231 }
giolekva6d464592020-05-13 20:12:18 +0400232 manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
233 if err != nil {
234 glog.Fatalf("Could ot initialize manager: %v", err)
235 }
giolekvab1f19ee2020-05-16 11:31:20 +0400236 h := handler{clientset, manager, app.NewK8sLauncher(clientset)}
giolekva6d464592020-05-13 20:12:18 +0400237 http.HandleFunc("/triggers", h.handleTriggers)
giolekvab1f19ee2020-05-16 11:31:20 +0400238 http.HandleFunc("/launch_action", h.handleLaunchAction)
giolekva6d464592020-05-13 20:12:18 +0400239 http.HandleFunc("/", h.handleInstall)
giolekvaa4a153b2020-05-12 11:49:53 +0400240 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
241
giolekvad5a58c42020-05-07 22:18:35 +0400242}