blob: 9ac89daf790b4e0ff18a843cd322898708581ee2 [file] [log] [blame]
package installer
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/giolekva/pcloud/core/installer/soft"
"sigs.k8s.io/yaml"
)
type ClusterNetworkConfigurator interface {
AddCluster(name string, ingressIP net.IP) error
RemoveCluster(name string, ingressIP net.IP) error
AddProxy(src, dst string) error
RemoveProxy(src, dst string) error
}
type NginxProxyConfigurator struct {
PrivateSubdomain string
DNSAPIAddr string
Repo soft.RepoIO
NginxConfigPath string
}
type createARecordReq struct {
Entry string `json:"entry"`
IP net.IP `json:"text"`
}
func (c *NginxProxyConfigurator) AddCluster(name string, ingressIP net.IP) error {
req := createARecordReq{
Entry: fmt.Sprintf("*.%s.cluster.%s", name, c.PrivateSubdomain),
IP: ingressIP,
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return err
}
resp, err := http.Post(fmt.Sprintf("%s/create-a-record", c.DNSAPIAddr), "application/json", &buf)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
return fmt.Errorf(buf.String())
}
return nil
}
func (c *NginxProxyConfigurator) RemoveCluster(name string, ingressIP net.IP) error {
req := createARecordReq{
Entry: fmt.Sprintf("*.%s.cluster.%s", name, c.PrivateSubdomain),
IP: ingressIP,
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return err
}
resp, err := http.Post(fmt.Sprintf("%s/delete-a-record", c.DNSAPIAddr), "application/json", &buf)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
return fmt.Errorf(buf.String())
}
return nil
}
func (c *NginxProxyConfigurator) AddProxy(src, dst string) error {
_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
cfg, err := func() (NginxProxyConfig, error) {
r, err := fs.Reader(c.NginxConfigPath)
if err != nil {
return NginxProxyConfig{}, err
}
defer r.Close()
return ParseNginxProxyConfig(r)
}()
if err != nil {
return "", err
}
if v, ok := cfg.Proxies[src]; ok && v != dst {
return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
}
cfg.Proxies[src] = dst
w, err := fs.Writer(c.NginxConfigPath)
if err != nil {
return "", err
}
defer w.Close()
h := sha256.New()
o := io.MultiWriter(w, h)
if err := cfg.Render(o); err != nil {
return "", err
}
hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
nginx, err := func() (map[string]any, error) {
r, err := fs.Reader(nginxPath)
if err != nil {
return nil, err
}
defer r.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
return nil, err
}
ret := map[string]any{}
if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
return nil, err
}
return ret, nil
}()
if err != nil {
return "", err
}
cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
var annotations map[string]any
if a, ok := cv["podAnnotations"]; ok {
annotations = a.(map[string]any)
} else {
annotations = map[string]any{}
cv["podAnnotations"] = annotations
}
annotations["dodo.cloud/hash"] = string(hash)
buf, err := yaml.Marshal(nginx)
if err != nil {
return "", err
}
w, err = fs.Writer(nginxPath)
if err != nil {
return "", err
}
defer w.Close()
if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
return "", err
}
return fmt.Sprintf("add proxy mapping: %s %s", src, dst), nil
})
return err
}
func (c *NginxProxyConfigurator) RemoveProxy(src, dst string) error {
_, err := c.Repo.Do(func(fs soft.RepoFS) (string, error) {
cfg, err := func() (NginxProxyConfig, error) {
r, err := fs.Reader(c.NginxConfigPath)
if err != nil {
return NginxProxyConfig{}, err
}
defer r.Close()
return ParseNginxProxyConfig(r)
}()
if err != nil {
return "", err
}
if v, ok := cfg.Proxies[src]; ok || v != dst {
return "", fmt.Errorf("wrong mapping %s already exists (%s)", src, v)
}
delete(cfg.Proxies, src)
w, err := fs.Writer(c.NginxConfigPath)
if err != nil {
return "", err
}
defer w.Close()
h := sha256.New()
o := io.MultiWriter(w, h)
if err := cfg.Render(o); err != nil {
return "", err
}
hash := base64.StdEncoding.EncodeToString(h.Sum(nil))
nginxPath := filepath.Join(filepath.Dir(c.NginxConfigPath), "ingress-nginx.yaml")
nginx, err := func() (map[string]any, error) {
r, err := fs.Reader(nginxPath)
if err != nil {
return nil, err
}
defer r.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
return nil, err
}
ret := map[string]any{}
if err := yaml.Unmarshal(buf.Bytes(), &ret); err != nil {
return nil, err
}
return ret, nil
}()
if err != nil {
return "", err
}
cv := nginx["spec"].(map[string]any)["values"].(map[string]any)["controller"].(map[string]any)
var annotations map[string]any
if a, ok := cv["podAnnotations"]; ok {
annotations = a.(map[string]any)
} else {
annotations = map[string]any{}
cv["podAnnotations"] = annotations
}
annotations["dodo.cloud/hash"] = string(hash)
buf, err := yaml.Marshal(nginx)
if err != nil {
return "", err
}
w, err = fs.Writer(nginxPath)
if err != nil {
return "", err
}
defer w.Close()
if _, err := io.Copy(w, bytes.NewReader(buf)); err != nil {
return "", err
}
return fmt.Sprintf("remove proxy mapping: %s %s", src, dst), nil
})
return err
}
type NginxProxyConfig struct {
Port int
Resolvers []net.IP
Proxies map[string]string
PreConf []string
}
func ParseNginxProxyConfig(r io.Reader) (NginxProxyConfig, error) {
var buf strings.Builder
if _, err := io.Copy(&buf, r); err != nil {
return NginxProxyConfig{}, err
}
ret := NginxProxyConfig{
Port: -1,
Resolvers: nil,
Proxies: make(map[string]string),
}
lines := strings.Split(buf.String(), "\n")
insideConf := true
insideMap := false
for _, l := range lines {
items := strings.Fields(strings.TrimSuffix(l, ";"))
if len(items) == 0 {
continue
}
if strings.Contains(l, "nginx.conf") {
ret.PreConf = append(ret.PreConf, l)
insideConf = false
} else if insideConf {
ret.PreConf = append(ret.PreConf, l)
} else if strings.Contains(l, "listen") {
if len(items) < 2 {
return NginxProxyConfig{}, fmt.Errorf("invalid listen: %s\n", l)
}
port, err := strconv.Atoi(items[1])
if err != nil {
return NginxProxyConfig{}, err
}
ret.Port = port
} else if strings.Contains(l, "resolver") {
if len(items) < 2 {
return NginxProxyConfig{}, fmt.Errorf("invalid resolver: %s", l)
}
ip := net.ParseIP(items[1])
if ip == nil {
return NginxProxyConfig{}, fmt.Errorf("invalid resolver ip: %s", l)
}
ret.Resolvers = append(ret.Resolvers, ip)
} else if insideMap {
if items[0] == "}" {
insideMap = false
continue
}
if len(items) < 2 {
return NginxProxyConfig{}, fmt.Errorf("invalid map: %s", l)
}
ret.Proxies[items[0]] = items[1]
} else if items[0] == "map" {
insideMap = true
}
}
return ret, nil
}
func (c NginxProxyConfig) Render(w io.Writer) error {
for _, l := range c.PreConf {
fmt.Fprintln(w, l)
}
tmpl, err := template.New("nginx.conf").Parse(nginxConfigTmpl)
if err != nil {
return err
}
return tmpl.Execute(w, c)
}
const nginxConfigTmpl = ` worker_processes 1;
worker_rlimit_nofile 8192;
events {
worker_connections 1024;
}
http {
map $http_host $backend {
{{- range $from, $to := .Proxies }}
{{ $from }} {{ $to }};
{{- end }}
}
server {
listen {{ .Port }};
location / {
{{- range .Resolvers }}
resolver {{ . }};
{{- end }}
proxy_pass http://$backend;
}
}
}`