blob: 9ac89daf790b4e0ff18a843cd322898708581ee2 [file] [log] [blame]
giof6ad2982024-08-23 17:42:49 +04001package installer
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "text/template"
16
17 "github.com/giolekva/pcloud/core/installer/soft"
18
19 "sigs.k8s.io/yaml"
20)
21
22type ClusterNetworkConfigurator interface {
23 AddCluster(name string, ingressIP net.IP) error
24 RemoveCluster(name string, ingressIP net.IP) error
25 AddProxy(src, dst string) error
26 RemoveProxy(src, dst string) error
27}
28
29type NginxProxyConfigurator struct {
30 PrivateSubdomain string
31 DNSAPIAddr string
32 Repo soft.RepoIO
33 NginxConfigPath string
34}
35
36type createARecordReq struct {
37 Entry string `json:"entry"`
38 IP net.IP `json:"text"`
39}
40
41func (c *NginxProxyConfigurator) AddCluster(name string, ingressIP net.IP) error {
42 req := createARecordReq{
43 Entry: fmt.Sprintf("*.%s.cluster.%s", name, c.PrivateSubdomain),
44 IP: ingressIP,
45 }
46 var buf bytes.Buffer
47 if err := json.NewEncoder(&buf).Encode(req); err != nil {
48 return err
49 }
50 resp, err := http.Post(fmt.Sprintf("%s/create-a-record", c.DNSAPIAddr), "application/json", &buf)
51 if err != nil {
52 return err
53 }
54 if resp.StatusCode != http.StatusOK {
55 var buf bytes.Buffer
56 io.Copy(&buf, resp.Body)
57 return fmt.Errorf(buf.String())
58 }
59 return nil
60}
61
62func (c *NginxProxyConfigurator) RemoveCluster(name string, ingressIP net.IP) error {
63 req := createARecordReq{
64 Entry: fmt.Sprintf("*.%s.cluster.%s", name, c.PrivateSubdomain),
65 IP: ingressIP,
66 }
67 var buf bytes.Buffer
68 if err := json.NewEncoder(&buf).Encode(req); err != nil {
69 return err
70 }
71 resp, err := http.Post(fmt.Sprintf("%s/delete-a-record", c.DNSAPIAddr), "application/json", &buf)
72 if err != nil {
73 return err
74 }
75 if resp.StatusCode != http.StatusOK {
76 var buf bytes.Buffer
77 io.Copy(&buf, resp.Body)
78 return fmt.Errorf(buf.String())
79 }
80 return nil
81}
82
83func (c *NginxProxyConfigurator) AddProxy(src, dst string) error {
84 _, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
85 cfg, err := func() (NginxProxyConfig, error) {
86 r, err := fs.Reader(c.NginxConfigPath)
87 if err != nil {
88 return NginxProxyConfig{}, err
89 }
90 defer r.Close()
91 return ParseNginxProxyConfig(r)
92 }()
93 if err != nil {
94 return "", err
95 }
giof15b9da2024-09-19 06:59:16 +040096 if v, ok := cfg.Proxies[src]; ok && v != dst {
97 return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
giof6ad2982024-08-23 17:42:49 +040098 }
99 cfg.Proxies[src] = dst
100 w, err := fs.Writer(c.NginxConfigPath)
101 if err != nil {
102 return "", err
103 }
104 defer w.Close()
105 h := sha256.New()
106 o := io.MultiWriter(w, h)
107 if err := cfg.Render(o); err != nil {
108 return "", err
109 }
110 hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
111 nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
112 nginx, err := func() (map[string]any, error) {
113 r, err := fs.Reader(nginxPath)
114 if err != nil {
115 return nil, err
116 }
117 defer r.Close()
118 var buf bytes.Buffer
119 if _, err := io.Copy(&buf, r); err != nil {
120 return nil, err
121 }
122 ret := map[string]any{}
123 if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
124 return nil, err
125 }
126 return ret, nil
127 }()
128 if err != nil {
129 return "", err
130 }
131 cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
132 var annotations map[string]any
133 if a, ok := cv["podAnnotations"]; ok {
134 annotations = a.(map[string]any)
135 } else {
136 annotations = map[string]any{}
137 cv["podAnnotations"] = annotations
138 }
139 annotations["dodo.cloud/hash"] = string(hash)
140 buf, err := yaml.Marshal(nginx)
141 if err != nil {
142 return "", err
143 }
144 w, err = fs.Writer(nginxPath)
145 if err != nil {
146 return "", err
147 }
148 defer w.Close()
149 if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
150 return "", err
151 }
152 return fmt.Sprintf("add proxy mapping: %s %s", src, dst), nil
153 })
154 return err
155}
156
157func (c *NginxProxyConfigurator) RemoveProxy(src, dst string) error {
158 _, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
159 cfg, err := func() (NginxProxyConfig, error) {
160 r, err := fs.Reader(c.NginxConfigPath)
161 if err != nil {
162 return NginxProxyConfig{}, err
163 }
164 defer r.Close()
165 return ParseNginxProxyConfig(r)
166 }()
167 if err != nil {
168 return "", err
169 }
giof15b9da2024-09-19 06:59:16 +0400170 if v, ok := cfg.Proxies[src]; ok || v != dst {
171 return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
giof6ad2982024-08-23 17:42:49 +0400172 }
173 delete(cfg.Proxies, src)
174 w, err := fs.Writer(c.NginxConfigPath)
175 if err != nil {
176 return "", err
177 }
178 defer w.Close()
179 h := sha256.New()
180 o := io.MultiWriter(w, h)
181 if err := cfg.Render(o); err != nil {
182 return "", err
183 }
184 hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
185 nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
186 nginx, err := func() (map[string]any, error) {
187 r, err := fs.Reader(nginxPath)
188 if err != nil {
189 return nil, err
190 }
191 defer r.Close()
192 var buf bytes.Buffer
193 if _, err := io.Copy(&buf, r); err != nil {
194 return nil, err
195 }
196 ret := map[string]any{}
197 if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
198 return nil, err
199 }
200 return ret, nil
201 }()
202 if err != nil {
203 return "", err
204 }
205 cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
206 var annotations map[string]any
207 if a, ok := cv["podAnnotations"]; ok {
208 annotations = a.(map[string]any)
209 } else {
210 annotations = map[string]any{}
211 cv["podAnnotations"] = annotations
212 }
213 annotations["dodo.cloud/hash"] = string(hash)
214 buf, err := yaml.Marshal(nginx)
215 if err != nil {
216 return "", err
217 }
218 w, err = fs.Writer(nginxPath)
219 if err != nil {
220 return "", err
221 }
222 defer w.Close()
223 if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
224 return "", err
225 }
226 return fmt.Sprintf("remove proxy mapping: %s %s", src, dst), nil
227 })
228 return err
229}
230
231type NginxProxyConfig struct {
232 Port int
233 Resolvers []net.IP
234 Proxies map[string]string
235 PreConf []string
236}
237
238func ParseNginxProxyConfig(r io.Reader) (NginxProxyConfig, error) {
239 var buf strings.Builder
240 if _, err := io.Copy(&buf, r); err != nil {
241 return NginxProxyConfig{}, err
242 }
243 ret := NginxProxyConfig{
244 Port: -1,
245 Resolvers: nil,
246 Proxies: make(map[string]string),
247 }
248 lines := strings.Split(buf.String(), "\n")
249 insideConf := true
250 insideMap := false
251 for _, l := range lines {
252 items := strings.Fields(strings.TrimSuffix(l, ";"))
253 if len(items) == 0 {
254 continue
255 }
256 if strings.Contains(l, "nginx.conf") {
257 ret.PreConf = append(ret.PreConf, l)
258 insideConf = false
259 } else if insideConf {
260 ret.PreConf = append(ret.PreConf, l)
261 } else if strings.Contains(l, "listen") {
262 if len(items) < 2 {
263 return NginxProxyConfig{}, fmt.Errorf("invalid listen: %s\n", l)
264 }
265 port, err := strconv.Atoi(items[1])
266 if err != nil {
267 return NginxProxyConfig{}, err
268 }
269 ret.Port = port
270 } else if strings.Contains(l, "resolver") {
271 if len(items) < 2 {
272 return NginxProxyConfig{}, fmt.Errorf("invalid resolver: %s", l)
273 }
274 ip := net.ParseIP(items[1])
275 if ip == nil {
276 return NginxProxyConfig{}, fmt.Errorf("invalid resolver ip: %s", l)
277 }
278 ret.Resolvers = append(ret.Resolvers, ip)
279 } else if insideMap {
280 if items[0] == "}" {
281 insideMap = false
282 continue
283 }
284 if len(items) < 2 {
285 return NginxProxyConfig{}, fmt.Errorf("invalid map: %s", l)
286 }
287 ret.Proxies[items[0]] = items[1]
288 } else if items[0] == "map" {
289 insideMap = true
290 }
291 }
292 return ret, nil
293}
294
295func (c NginxProxyConfig) Render(w io.Writer) error {
296 for _, l := range c.PreConf {
297 fmt.Fprintln(w, l)
298 }
299 tmpl, err := template.New("nginx.conf").Parse(nginxConfigTmpl)
300 if err != nil {
301 return err
302 }
303 return tmpl.Execute(w, c)
304}
305
306const nginxConfigTmpl = ` worker_processes 1;
307 worker_rlimit_nofile 8192;
308 events {
309 worker_connections 1024;
310 }
311 http {
312 map $http_host $backend {
313 {{- range $from, $to := .Proxies }}
314 {{ $from }} {{ $to }};
315 {{- end }}
316 }
317 server {
318 listen {{ .Port }};
319 location / {
320 {{- range .Resolvers }}
321 resolver {{ . }};
322 {{- end }}
323 proxy_pass http://$backend;
324 }
325 }
326 }`