blob: 4ad45cce7cc25b63fc30222005158413f0687cc3 [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"
10 "log"
11 "net/http"
12 "os"
giolekvaebea5822020-05-12 20:52:19 +040013 "syscall"
giolekvad5a58c42020-05-07 22:18:35 +040014
giolekvafe0765f2020-05-12 14:09:09 +040015 "github.com/golang/glog"
giolekvaebea5822020-05-12 20:52:19 +040016 "github.com/google/uuid"
17 apiv1 "k8s.io/api/core/v1"
18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19 "k8s.io/client-go/kubernetes"
20 corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
21 "k8s.io/client-go/rest"
22 "k8s.io/client-go/tools/clientcmd"
giolekvaa4a153b2020-05-12 11:49:53 +040023
24 app "github.com/giolekva/pcloud/appmanager"
giolekvad5a58c42020-05-07 22:18:35 +040025)
26
giolekvaebea5822020-05-12 20:52:19 +040027var kubeconfig = flag.String("kubeconfig", "", "Absolute path to the kubeconfig file.")
giolekvab22550e2020-05-12 22:09:03 +040028var helmBin = flag.String("helm_bin", "/usr/local/bin/helm", "Path to the Helm binary.")
giolekvaa4a153b2020-05-12 11:49:53 +040029var port = flag.Int("port", 1234, "Port to listen on.")
giolekvafe0765f2020-05-12 14:09:09 +040030var apiAddr = flag.String("api_addr", "", "PCloud API service address.")
giolekva6d464592020-05-13 20:12:18 +040031var managerStoreFile = flag.String("manager_store_file", "", "Persistent file containing installed application information.")
giolekvaa4a153b2020-05-12 11:49:53 +040032
33var helmUploadPage = `
34<html>
35<head>
36 <title>Upload Helm chart</title>
37</head>
38<body>
giolekvab22550e2020-05-12 22:09:03 +040039<form enctype="multipart/form-data" method="post">
giolekvaa4a153b2020-05-12 11:49:53 +040040 <input type="file" name="chartfile" />
41 <input type="submit" value="upload" />
42</form>
43</body>
44</html>
45`
46
giolekvaebea5822020-05-12 20:52:19 +040047type handler struct {
giolekva6d464592020-05-13 20:12:18 +040048 manager *app.Manager
giolekvaebea5822020-05-12 20:52:19 +040049 nsClient corev1.NamespaceInterface
50}
51
giolekva6d464592020-05-13 20:12:18 +040052func (hn *handler) handleInstall(w http.ResponseWriter, r *http.Request) {
giolekvaa4a153b2020-05-12 11:49:53 +040053 if r.Method == "GET" {
54 _, err := io.WriteString(w, helmUploadPage)
55 if err != nil {
56 http.Error(w, err.Error(), http.StatusInternalServerError)
57 }
58 } else if r.Method == "POST" {
59 r.ParseMultipartForm(1000000)
60 file, handler, err := r.FormFile("chartfile")
61 if err != nil {
62 http.Error(w, err.Error(), http.StatusInternalServerError)
63 return
64 }
65 defer file.Close()
giolekvaebea5822020-05-12 20:52:19 +040066 tmp := uuid.New().String()
67 if tmp == "" {
68 http.Error(w, "Could not generate temp dir", http.StatusInternalServerError)
69 return
70 }
71 p := "/tmp/" + tmp
72 // TODO(giolekva): defer rmdir
73 if err := syscall.Mkdir(p, 0777); err != nil {
74 http.Error(w, "Could not create temp dir", http.StatusInternalServerError)
75 return
76 }
77 p += "/" + handler.Filename
giolekvaa4a153b2020-05-12 11:49:53 +040078 f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE, 0666)
79 if err != nil {
80 fmt.Println(err)
81 return
82 }
83 defer f.Close()
84 _, err = io.Copy(f, file)
85 if err != nil {
86 http.Error(w, err.Error(), http.StatusInternalServerError)
87 return
88 }
giolekva6d464592020-05-13 20:12:18 +040089 if err = hn.installHelmChart(p); err != nil {
giolekvaa4a153b2020-05-12 11:49:53 +040090 http.Error(w, err.Error(), http.StatusInternalServerError)
91 return
92 }
93 w.Write([]byte("Installed"))
giolekvad5a58c42020-05-07 22:18:35 +040094 }
giolekvaa4a153b2020-05-12 11:49:53 +040095}
96
giolekva6d464592020-05-13 20:12:18 +040097type trigger struct {
98 Namespace string `json:"namespace"`
99 Template string `json:"template"`
100}
101
102func (hn *handler) handleTriggers(w http.ResponseWriter, r *http.Request) {
103 if r.Method != "GET" {
104 http.Error(w, "Only GET method is supported on /triggers", http.StatusBadRequest)
105 return
106 }
107 if err := r.ParseForm(); err != nil {
108 http.Error(w, err.Error(), http.StatusBadRequest)
109 return
110 }
111 // TODO(giolekva): check if exists
112 triggerOnType := r.Form["trigger_on_type"][0]
113 triggerOnEvent := r.Form["trigger_on_event"][0]
114 var triggers []trigger
115 for _, a := range hn.manager.Apps {
116 if a.Triggers == nil {
117 continue
118 }
119 for _, t := range a.Triggers.Triggers {
120 if t.TriggerOn.Type == triggerOnType && t.TriggerOn.Event == triggerOnEvent {
121 triggers = append(triggers, trigger{a.Namespace, t.Template})
122 }
123 }
124 }
125 respBody, err := json.Marshal(triggers)
126 if err != nil {
127 http.Error(w, err.Error(), http.StatusInternalServerError)
128 return
129 }
130 if _, err := io.Copy(w, bytes.NewReader(respBody)); err != nil {
131 http.Error(w, err.Error(), http.StatusInternalServerError)
132 return
133 }
134}
135
136func (hn *handler) installHelmChart(path string) error {
giolekvaebea5822020-05-12 20:52:19 +0400137 h, err := app.HelmChartFromTar(path)
giolekvaa4a153b2020-05-12 11:49:53 +0400138 if err != nil {
139 return err
140 }
giolekvaebea5822020-05-12 20:52:19 +0400141 if err = app.InstallSchema(h.Schema, *apiAddr); err != nil {
giolekvafe0765f2020-05-12 14:09:09 +0400142 return err
143 }
144 glog.Infof("Installed schema: %s", h.Schema)
giolekvaebea5822020-05-12 20:52:19 +0400145 namespace := fmt.Sprintf("app-%s", h.Name)
giolekva6d464592020-05-13 20:12:18 +0400146 if err = createNamespace(hn.nsClient, namespace); err != nil {
giolekvaebea5822020-05-12 20:52:19 +0400147 return err
148 }
149 glog.Infof("Created namespaces: %s", namespace)
150 if err = h.Install(
giolekvab22550e2020-05-12 22:09:03 +0400151 *helmBin,
giolekvaebea5822020-05-12 20:52:19 +0400152 map[string]string{}); err != nil {
153 return err
154 }
giolekva6d464592020-05-13 20:12:18 +0400155 glog.Info("Deployed")
156 hn.manager.Apps[h.Name] = app.App{namespace, h.Triggers}
157 app.StoreManagerStateToFile(hn.manager, *managerStoreFile)
giolekvaebea5822020-05-12 20:52:19 +0400158 glog.Info("Installed")
159 return nil
160}
161
162func createNamespace(nsClient corev1.NamespaceInterface, name string) error {
163 _, err := nsClient.Create(
164 context.TODO(),
165 &apiv1.Namespace{
166 ObjectMeta: metav1.ObjectMeta{
167 Name: name}},
168 metav1.CreateOptions{})
giolekvaa4a153b2020-05-12 11:49:53 +0400169 return err
170}
171
giolekvaebea5822020-05-12 20:52:19 +0400172func getKubeConfig() (*rest.Config, error) {
173 if *kubeconfig != "" {
174 return clientcmd.BuildConfigFromFlags("", *kubeconfig)
175 } else {
176 return rest.InClusterConfig()
177 }
178}
179
giolekvaa4a153b2020-05-12 11:49:53 +0400180func main() {
181 flag.Parse()
giolekvaebea5822020-05-12 20:52:19 +0400182 config, err := getKubeConfig()
183 if err != nil {
184 glog.Fatalf("Could not initialize Kubeconfig: %v", err)
185 }
186 clientset, err := kubernetes.NewForConfig(config)
187 if err != nil {
188 glog.Fatalf("Could not create Kubernetes API client: %v", err)
189 }
190 namespaces := clientset.CoreV1().Namespaces()
giolekva6d464592020-05-13 20:12:18 +0400191 manager, err := app.LoadManagerStateFromFile(*managerStoreFile)
192 if err != nil {
193 glog.Fatalf("Could ot initialize manager: %v", err)
194 }
195 h := handler{manager, namespaces}
196 http.HandleFunc("/triggers", h.handleTriggers)
197 http.HandleFunc("/", h.handleInstall)
giolekvaa4a153b2020-05-12 11:49:53 +0400198 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
199
giolekvad5a58c42020-05-07 22:18:35 +0400200}