diff --git a/Dockerfile b/Dockerfile index cbd0b6b..113e82c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,10 @@ from golang:1.11.2 as build env pkg novit.nc/direktil/local-server -run go get github.com/gobuffalo/packr/packr copy vendor /go/src/${pkg}/vendor copy cmd /go/src/${pkg}/cmd -copy assets /go/src/${pkg}/cmd/dkl-local-server/assets workdir /go/src/${pkg} -run packr -i /go/src/${pkg}/cmd/dkl-local-server \ - && go test ./... \ +run go test ./... \ && go install ./cmd/... # ------------------------------------------------------------------------ diff --git a/cmd/dkl-dir2config/http.go b/cmd/dkl-dir2config/http.go new file mode 100644 index 0000000..357146d --- /dev/null +++ b/cmd/dkl-dir2config/http.go @@ -0,0 +1,293 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "log" + "net" + "net/http" + "regexp" + "strings" + + yaml "gopkg.in/yaml.v2" + "novit.nc/direktil/pkg/localconfig" +) + +var ( + hostsToken = flag.String("hosts-token", "", "Token to give to access /hosts (open is none)") + + reHost = regexp.MustCompile("^/hosts/([^/]+)/([^/]+)$") + + trustXFF = flag.Bool("trust-xff", true, "Trust the X-Forwarded-For header") +) + +func authorizeHosts(r *http.Request) bool { + if *hostsToken == "" { + // access is open + return true + } + + reqToken := r.Header.Get("Authorization") + + return reqToken == "Bearer "+*hostsToken +} + +func forbidden(w http.ResponseWriter, r *http.Request) { + log.Printf("denied access to %s from %s", r.RequestURI, r.RemoteAddr) + http.Error(w, "Forbidden", http.StatusForbidden) +} + +func serveHostByIP(w http.ResponseWriter, r *http.Request) { + host, cfg := hostByIP(w, r) + if host == nil { + return + } + + what := strings.TrimLeft(r.URL.Path, "/") + + renderHost(w, r, what, host, cfg) +} + +func hostByIP(w http.ResponseWriter, r *http.Request) (*localconfig.Host, *localconfig.Config) { + remoteAddr := r.RemoteAddr + + if *trustXFF { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + remoteAddr = strings.Split(xff, ",")[0] + } + } + + hostIP, _, err := net.SplitHostPort(remoteAddr) + + if err != nil { + hostIP = remoteAddr + } + + cfg, err := readConfig() + if err != nil { + http.Error(w, "", http.StatusServiceUnavailable) + return nil, nil + } + + host := cfg.HostByIP(hostIP) + + if host == nil { + log.Print("no host found for IP ", hostIP) + http.NotFound(w, r) + return nil, nil + } + + return host, cfg +} + +func serveHosts(w http.ResponseWriter, r *http.Request) { + if !authorizeHosts(r) { + forbidden(w, r) + return + } + + cfg, err := readConfig() + if err != nil { + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + renderJSON(w, cfg.Hosts) +} + +func serveHost(w http.ResponseWriter, r *http.Request) { + if !authorizeHosts(r) { + forbidden(w, r) + return + } + + match := reHost.FindStringSubmatch(r.URL.Path) + if match == nil { + http.NotFound(w, r) + return + } + + hostName, what := match[1], match[2] + + cfg, err := readConfig() + if err != nil { + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + host := cfg.Host(hostName) + + if host == nil { + host = cfg.HostByMAC(hostName) + } + + if host == nil { + log.Printf("no host with name or MAC %q", hostName) + http.NotFound(w, r) + return + } + + renderHost(w, r, what, host, cfg) +} + +func renderHost(w http.ResponseWriter, r *http.Request, what string, host *localconfig.Host, cfg *localconfig.Config) { + ctx, err := newRenderContext(host, cfg) + if err != nil { + log.Printf("host %s: %s: failed to render: %v", what, host.Name, err) + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + switch what { + case "ipxe": + w.Header().Set("Content-Type", "text/x-ipxe") + case "config": + w.Header().Set("Content-Type", "text/vnd.yaml") + default: + w.Header().Set("Content-Type", "application/octet-stream") + } + + switch what { + case "ipxe": + err = renderIPXE(w, ctx) + + case "kernel": + err = renderKernel(w, r, ctx) + + case "initrd": + err = renderCtx(w, r, ctx, what, buildInitrd) + + case "boot.iso": + err = renderCtx(w, r, ctx, what, buildBootISO) + + case "boot.tar": + err = renderCtx(w, r, ctx, what, buildBootTar) + + case "boot.img": + err = renderCtx(w, r, ctx, what, buildBootImg) + + case "boot.img.gz": + err = renderCtx(w, r, ctx, what, buildBootImgGZ) + + case "boot.img.lz4": + err = renderCtx(w, r, ctx, what, buildBootImgLZ4) + + case "config": + err = renderConfig(w, r, ctx) + + default: + http.NotFound(w, r) + } + + if err != nil { + if isNotFound(err) { + log.Printf("host %s: %s: %v", what, host.Name, err) + http.NotFound(w, r) + } else { + log.Printf("host %s: %s: failed to render: %v", what, host.Name, err) + http.Error(w, "", http.StatusServiceUnavailable) + } + } +} + +func renderJSON(w http.ResponseWriter, v interface{}) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(v) +} + +func serveClusters(w http.ResponseWriter, r *http.Request) { + cfg, err := readConfig() + if err != nil { + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + clusterNames := make([]string, len(cfg.Clusters)) + for i, cluster := range cfg.Clusters { + clusterNames[i] = cluster.Name + } + + renderJSON(w, clusterNames) +} + +func serveCluster(w http.ResponseWriter, r *http.Request) { + // "/clusters//" split => "", "clusters", "", "" + p := strings.Split(r.URL.Path, "/") + + if len(p) != 4 { + http.NotFound(w, r) + return + } + + clusterName := p[2] + + p = strings.SplitN(p[3], ".", 2) + what := p[0] + format := "" + if len(p) > 1 { + format = p[1] + } + + cfg, err := readConfig() + if err != nil { + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + cluster := cfg.Cluster(clusterName) + if cluster == nil { + http.NotFound(w, r) + return + } + + switch what { + case "addons": + if len(cluster.Addons) == 0 { + log.Printf("cluster %q has no addons defined", clusterName) + http.NotFound(w, r) + return + } + + addons := cluster.Addons + if addons == nil { + log.Printf("cluster %q: no addons with name %q", clusterName, cluster.Addons) + http.NotFound(w, r) + return + } + + clusterAsMap := asMap(cluster) + clusterAsMap["kubernetes_svc_ip"] = cluster.KubernetesSvcIP().String() + clusterAsMap["dns_svc_ip"] = cluster.DNSSvcIP().String() + + cm := newConfigMap("cluster-addons") + + for _, addon := range addons { + buf := &bytes.Buffer{} + err := addon.Execute(buf, clusterAsMap, nil) + + if err != nil { + log.Printf("cluster %q: addons %q: failed to render %q: %v", + clusterName, cluster.Addons, addon.Name, err) + http.Error(w, "", http.StatusServiceUnavailable) + return + } + + cm.Data[addon.Name] = buf.String() + } + + switch format { + case "yaml": + for name, data := range cm.Data { + w.Write([]byte("\n# addon: " + name + "\n---\n\n")) + w.Write([]byte(data)) + } + + default: + yaml.NewEncoder(w).Encode(cm) + } + + default: + http.NotFound(w, r) + } +} diff --git a/cmd/dkl-dir2config/main.go b/cmd/dkl-dir2config/main.go new file mode 100644 index 0000000..50e8d8d --- /dev/null +++ b/cmd/dkl-dir2config/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("vim-go") +} diff --git a/cmd/dkl-dir2config/render-context.go b/cmd/dkl-dir2config/render-context.go new file mode 100644 index 0000000..764d054 --- /dev/null +++ b/cmd/dkl-dir2config/render-context.go @@ -0,0 +1,402 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log" + "path" + "path/filepath" + + cfsslconfig "github.com/cloudflare/cfssl/config" + "github.com/cloudflare/cfssl/csr" + yaml "gopkg.in/yaml.v2" + + "novit.nc/direktil/pkg/clustersconfig" + "novit.nc/direktil/pkg/config" +) + +type renderContext struct { + Host *clustersconfig.Host + Group *clustersconfig.Group + Cluster *clustersconfig.Cluster + Vars map[string]interface{} + ConfigTemplate *clustersconfig.Template + StaticPodsTemplate *clustersconfig.Template + + clusterConfig *clustersconfig.Config +} + +func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (ctx *renderContext, err error) { + cluster := cfg.Cluster(host.Cluster) + if cluster == nil { + err = fmt.Errorf("no cluster named %q", host.Cluster) + return + } + + group := cfg.Group(host.Group) + if group == nil { + err = fmt.Errorf("no group named %q", host.Group) + return + } + + vars := make(map[string]interface{}) + + for _, oVars := range []map[string]interface{}{ + cluster.Vars, + group.Vars, + host.Vars, + } { + for k, v := range oVars { + vars[k] = v + } + } + + return &renderContext{ + Host: host, + Group: group, + Cluster: cluster, + Vars: vars, + ConfigTemplate: cfg.ConfigTemplate(group.Config), + StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods), + + clusterConfig: cfg, + }, nil +} + +func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { + if ctx.ConfigTemplate == nil { + err = notFoundError{fmt.Sprintf("config %q", ctx.Group.Config)} + return + } + + ctxMap := ctx.asMap() + + secretData, err := ctx.secretData() + if err != nil { + return + } + + templateFuncs := ctx.templateFuncs(secretData, ctxMap) + + render := func(what string, t *clustersconfig.Template) (s string, err error) { + buf := &bytes.Buffer{} + err = t.Execute(buf, ctxMap, templateFuncs) + if err != nil { + log.Printf("host %s: failed to render %s [%q]: %v", ctx.Host.Name, what, t.Name, err) + return + } + + s = buf.String() + return + } + + extraFuncs := ctx.templateFuncs(secretData, ctxMap) + + extraFuncs["static_pods"] = func(name string) (string, error) { + t := ctx.clusterConfig.StaticPodsTemplate(name) + if t == nil { + return "", fmt.Errorf("no static pods template named %q", name) + } + + return render("static pods", t) + } + + buf := bytes.NewBuffer(make([]byte, 0, 4096)) + if err = ctx.ConfigTemplate.Execute(buf, ctxMap, extraFuncs); err != nil { + return + } + + if secretData.Changed() { + err = secretData.Save() + if err != nil { + return + } + } + + ba = buf.Bytes() + + cfg = &config.Config{} + + if err = yaml.Unmarshal(buf.Bytes(), cfg); err != nil { + return + } + + return +} + +func (ctx *renderContext) secretData() (data *SecretData, err error) { + var sslCfg *cfsslconfig.Config + + if ctx.clusterConfig.SSLConfig == "" { + sslCfg = &cfsslconfig.Config{} + } else { + sslCfg, err = cfsslconfig.LoadConfig([]byte(ctx.clusterConfig.SSLConfig)) + if err != nil { + return + } + } + + data, err = loadSecretData(sslCfg) + return +} + +func (ctx *renderContext) StaticPods() (ba []byte, err error) { + secretData, err := ctx.secretData() + if err != nil { + return + } + + if ctx.StaticPodsTemplate == nil { + err = notFoundError{fmt.Sprintf("static-pods %q", ctx.Group.StaticPods)} + return + } + + ctxMap := ctx.asMap() + + buf := bytes.NewBuffer(make([]byte, 0, 4096)) + if err = ctx.StaticPodsTemplate.Execute(buf, ctxMap, ctx.templateFuncs(secretData, ctxMap)); err != nil { + return + } + + if secretData.Changed() { + err = secretData.Save() + if err != nil { + return + } + } + + ba = buf.Bytes() + return +} + +func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[string]interface{}) map[string]interface{} { + cluster := ctx.Cluster.Name + + getKeyCert := func(name string) (kc *KeyCert, err error) { + req := ctx.clusterConfig.CSR(name) + if req == nil { + err = errors.New("no such certificate request") + return + } + + if req.CA == "" { + err = errors.New("CA not defined") + return + } + + buf := &bytes.Buffer{} + err = req.Execute(buf, ctxMap, nil) + if err != nil { + return + } + + certReq := &csr.CertificateRequest{ + KeyRequest: csr.NewBasicKeyRequest(), + } + + err = json.Unmarshal(buf.Bytes(), certReq) + if err != nil { + log.Print("unmarshal failed on: ", buf) + return + } + + if req.PerHost { + name = name + "/" + ctx.Host.Name + } + + return secretData.KeyCert(cluster, req.CA, name, req.Profile, req.Label, certReq) + } + + asYaml := func(v interface{}) (string, error) { + ba, err := yaml.Marshal(v) + if err != nil { + return "", err + } + + return string(ba), nil + } + + return map[string]interface{}{ + "token": func(name string) (s string, err error) { + return secretData.Token(cluster, name) + }, + + "ca_key": func(name string) (s string, err error) { + ca, err := secretData.CA(cluster, name) + if err != nil { + return + } + + s = string(ca.Key) + return + }, + + "ca_crt": func(name string) (s string, err error) { + ca, err := secretData.CA(cluster, name) + if err != nil { + return + } + + s = string(ca.Cert) + return + }, + + "ca_dir": func(name string) (s string, err error) { + ca, err := secretData.CA(cluster, name) + if err != nil { + return + } + + dir := "/" + path.Join("etc", "tls-ca", name) + + return asYaml([]config.FileDef{ + { + Path: path.Join(dir, "ca.crt"), + Mode: 0644, + Content: string(ca.Cert), + }, + { + Path: path.Join(dir, "ca.key"), + Mode: 0600, + Content: string(ca.Key), + }, + }) + }, + + "tls_key": func(name string) (s string, err error) { + kc, err := getKeyCert(name) + if err != nil { + return + } + + s = string(kc.Key) + return + }, + + "tls_crt": func(name string) (s string, err error) { + kc, err := getKeyCert(name) + if err != nil { + return + } + + s = string(kc.Cert) + return + }, + + "tls_dir": func(name string) (s string, err error) { + csr := ctx.clusterConfig.CSR(name) + if csr == nil { + err = fmt.Errorf("no CSR named %q", name) + return + } + + ca, err := secretData.CA(cluster, csr.CA) + if err != nil { + return + } + + kc, err := getKeyCert(name) + if err != nil { + return + } + + dir := "/" + path.Join("etc", "tls", name) + + return asYaml([]config.FileDef{ + { + Path: path.Join(dir, "ca.crt"), + Mode: 0644, + Content: string(ca.Cert), + }, + { + Path: path.Join(dir, "tls.crt"), + Mode: 0644, + Content: string(kc.Cert), + }, + { + Path: path.Join(dir, "tls.key"), + Mode: 0600, + Content: string(kc.Key), + }, + }) + }, + + "hosts_of_group": func() (hosts []interface{}) { + hosts = make([]interface{}, 0) + + for _, host := range ctx.clusterConfig.Hosts { + if host.Group != ctx.Host.Group { + continue + } + + hosts = append(hosts, asMap(host)) + } + + return hosts + }, + + "hosts_of_group_count": func() (count int) { + for _, host := range ctx.clusterConfig.Hosts { + if host.Group != ctx.Host.Group { + continue + } + + count++ + } + return + }, + } +} + +func (ctx *renderContext) distFilePath(path ...string) string { + return filepath.Join(append([]string{*dataDir, "dist"}, path...)...) +} + +func (ctx *renderContext) Tag() (string, error) { + h := sha256.New() + + _, cfg, err := ctx.Config() + if err != nil { + return "", err + } + + enc := yaml.NewEncoder(h) + + for _, o := range []interface{}{cfg, ctx} { + if err := enc.Encode(o); err != nil { + return "", err + } + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +func (ctx *renderContext) asMap() map[string]interface{} { + result := asMap(ctx) + + // also expand cluster: + cluster := result["cluster"].(map[interface{}]interface{}) + cluster["kubernetes_svc_ip"] = ctx.Cluster.KubernetesSvcIP().String() + cluster["dns_svc_ip"] = ctx.Cluster.DNSSvcIP().String() + + return result +} + +func asMap(v interface{}) map[string]interface{} { + ba, err := yaml.Marshal(v) + if err != nil { + panic(err) // shouldn't happen + } + + result := make(map[string]interface{}) + + if err := yaml.Unmarshal(ba, result); err != nil { + panic(err) // shouldn't happen + } + + return result +} diff --git a/cmd/dkl-local-server/boot-iso.go b/cmd/dkl-local-server/boot-iso.go index 1d03308..1298440 100644 --- a/cmd/dkl-local-server/boot-iso.go +++ b/cmd/dkl-local-server/boot-iso.go @@ -132,13 +132,13 @@ menuentry "Direktil" { } copies := []distCopy{ - {Src: []string{"kernels", ctx.Group.Kernel}, Dst: "vmlinuz"}, - {Src: []string{"initrd", ctx.Group.Initrd}, Dst: "initrd"}, + {Src: []string{"kernels", ctx.Host.Kernel}, Dst: "vmlinuz"}, + {Src: []string{"initrd", ctx.Host.Initrd}, Dst: "initrd"}, } // layers for _, layer := range cfg.Layers { - layerVersion := ctx.Group.Versions[layer] + layerVersion := ctx.Host.Versions[layer] if layerVersion == "" { return fmt.Errorf("layer %q not mapped to a version", layer) } diff --git a/cmd/dkl-local-server/boot-tar.go b/cmd/dkl-local-server/boot-tar.go index d10b392..8ab5f25 100644 --- a/cmd/dkl-local-server/boot-tar.go +++ b/cmd/dkl-local-server/boot-tar.go @@ -4,7 +4,6 @@ import ( "archive/tar" "fmt" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -18,18 +17,6 @@ func rmTempFile(f *os.File) { } func buildBootTar(out io.Writer, ctx *renderContext) (err error) { - grubCfg, err := ioutil.TempFile(os.TempDir(), "grub.cfg-") - if err != nil { - return - } - defer rmTempFile(grubCfg) - - _, err = grubCfg.Write(asset("grub.cfg")) - if err != nil { - return - } - grubCfg.Close() - arch := tar.NewWriter(out) defer arch.Close() @@ -58,13 +45,13 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) { // kernel and initrd copies := []distCopy{ - {Src: []string{"kernels", ctx.Group.Kernel}, Dst: "current/vmlinuz"}, - {Src: []string{"initrd", ctx.Group.Initrd}, Dst: "current/initrd"}, + {Src: []string{"kernels", ctx.Host.Kernel}, Dst: "current/vmlinuz"}, + {Src: []string{"initrd", ctx.Host.Initrd}, Dst: "current/initrd"}, } // layers for _, layer := range cfg.Layers { - layerVersion := ctx.Group.Versions[layer] + layerVersion := ctx.Host.Versions[layer] if layerVersion == "" { return fmt.Errorf("layer %q not mapped to a version", layer) } diff --git a/cmd/dkl-local-server/data.go b/cmd/dkl-local-server/data.go index a1b6c05..187688d 100644 --- a/cmd/dkl-local-server/data.go +++ b/cmd/dkl-local-server/data.go @@ -2,10 +2,9 @@ package main import ( "flag" - "log" "path/filepath" - "novit.nc/direktil/pkg/clustersconfig" + "novit.nc/direktil/pkg/localconfig" ) var ( @@ -13,22 +12,8 @@ var ( configFromDir = flag.String("config-from-dir", "", "Build configuration from this directory") ) -func readConfig() (config *clustersconfig.Config, err error) { - configFile := filepath.Join(*dataDir, "current-config.yaml") +func readConfig() (config *localconfig.Config, err error) { + configFile := filepath.Join(*dataDir, "config.yaml") - if *configFromDir != "" { - config, err = clustersconfig.FromDir(*configFromDir) - if err != nil { - log.Print("failed to load config: ", err) - return nil, err - } - - if err = config.SaveTo(configFile); err != nil { - return nil, err - } - - return - } - - return clustersconfig.FromFile(configFile) + return localconfig.FromFile(configFile) } diff --git a/cmd/dkl-local-server/http.go b/cmd/dkl-local-server/http.go index 530eb36..6b94676 100644 --- a/cmd/dkl-local-server/http.go +++ b/cmd/dkl-local-server/http.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "flag" "log" @@ -10,8 +9,7 @@ import ( "regexp" "strings" - yaml "gopkg.in/yaml.v2" - "novit.nc/direktil/pkg/clustersconfig" + "novit.nc/direktil/pkg/localconfig" ) var ( @@ -49,7 +47,7 @@ func serveHostByIP(w http.ResponseWriter, r *http.Request) { renderHost(w, r, what, host, cfg) } -func hostByIP(w http.ResponseWriter, r *http.Request) (*clustersconfig.Host, *clustersconfig.Config) { +func hostByIP(w http.ResponseWriter, r *http.Request) (*localconfig.Host, *localconfig.Config) { remoteAddr := r.RemoteAddr if *trustXFF { @@ -131,7 +129,7 @@ func serveHost(w http.ResponseWriter, r *http.Request) { renderHost(w, r, what, host, cfg) } -func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clustersconfig.Host, cfg *clustersconfig.Config) { +func renderHost(w http.ResponseWriter, r *http.Request, what string, host *localconfig.Host, cfg *localconfig.Config) { ctx, err := newRenderContext(host, cfg) if err != nil { log.Printf("host %s: %s: failed to render: %v", what, host.Name, err) @@ -156,13 +154,13 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clust err = renderKernel(w, r, ctx) case "initrd": - err = renderCtx(w, r, ctx, "initrd", buildInitrd) + err = renderCtx(w, r, ctx, what, buildInitrd) case "boot.iso": - err = renderCtx(w, r, ctx, "boot.iso", buildBootISO) + err = renderCtx(w, r, ctx, what, buildBootISO) case "boot.tar": - err = renderCtx(w, r, ctx, "boot.tar", buildBootTar) + err = renderCtx(w, r, ctx, what, buildBootTar) case "boot.img": err = renderCtx(w, r, ctx, what, buildBootImg) @@ -176,13 +174,6 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *clust case "config": err = renderConfig(w, r, ctx) - case "static-pods": - if ctx.Group.StaticPods == "" { - w.WriteHeader(http.StatusNoContent) - return - } - err = renderStaticPods(w, r, ctx) - default: http.NotFound(w, r) } @@ -228,16 +219,11 @@ func serveCluster(w http.ResponseWriter, r *http.Request) { } clusterName := p[2] - - p = strings.SplitN(p[3], ".", 2) - what := p[0] - format := "" - if len(p) > 1 { - format = p[1] - } + what := p[3] cfg, err := readConfig() if err != nil { + log.Print("failed to read config: ", err) http.Error(w, "", http.StatusServiceUnavailable) return } @@ -250,49 +236,13 @@ func serveCluster(w http.ResponseWriter, r *http.Request) { switch what { case "addons": - if cluster.Addons == "" { + if len(cluster.Addons) == 0 { log.Printf("cluster %q has no addons defined", clusterName) http.NotFound(w, r) return } - addons := cfg.Addons[cluster.Addons] - if addons == nil { - log.Printf("cluster %q: no addons with name %q", clusterName, cluster.Addons) - http.NotFound(w, r) - return - } - - clusterAsMap := asMap(cluster) - clusterAsMap["kubernetes_svc_ip"] = cluster.KubernetesSvcIP().String() - clusterAsMap["dns_svc_ip"] = cluster.DNSSvcIP().String() - - cm := newConfigMap("cluster-addons") - - for _, addon := range addons { - buf := &bytes.Buffer{} - err := addon.Execute(buf, clusterAsMap, nil) - - if err != nil { - log.Printf("cluster %q: addons %q: failed to render %q: %v", - clusterName, cluster.Addons, addon.Name, err) - http.Error(w, "", http.StatusServiceUnavailable) - return - } - - cm.Data[addon.Name] = buf.String() - } - - switch format { - case "yaml": - for name, data := range cm.Data { - w.Write([]byte("\n# addon: " + name + "\n---\n\n")) - w.Write([]byte(data)) - } - - default: - yaml.NewEncoder(w).Encode(cm) - } + w.Write([]byte(cluster.Addons)) default: http.NotFound(w, r) diff --git a/cmd/dkl-local-server/initrd.go b/cmd/dkl-local-server/initrd.go index 838daa9..3679a89 100644 --- a/cmd/dkl-local-server/initrd.go +++ b/cmd/dkl-local-server/initrd.go @@ -32,45 +32,6 @@ func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext) er return nil } -func renderStaticPods(w http.ResponseWriter, r *http.Request, ctx *renderContext) error { - log.Printf("sending static-pods for %q", ctx.Host.Name) - - ba, err := ctx.StaticPods() - if err != nil { - return err - } - - w.Header().Set("Content-Type", "application/yaml") // XXX can also be JSON - http.ServeContent(w, r, "static-pods", time.Unix(0, 0), bytes.NewReader(ba)) - - return nil -} - -// TODO move somewhere logical -func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string, - create func(out io.Writer, ctx *renderContext) error) error { - log.Printf("sending %s for %q", what, ctx.Host.Name) - - tag, err := ctx.Tag() - if err != nil { - return err - } - - // get it or create it - content, meta, err := casStore.GetOrCreate(tag, what, func(out io.Writer) error { - log.Printf("building %s for %q", what, ctx.Host.Name) - return create(out, ctx) - }) - - if err != nil { - return err - } - - // serve it - http.ServeContent(w, r, what, meta.ModTime(), content) - return nil -} - func buildInitrd(out io.Writer, ctx *renderContext) error { _, cfg, err := ctx.Config() @@ -79,7 +40,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) error { } // send initrd basis - initrdPath, err := ctx.distFetch("initrd", ctx.Group.Initrd) + initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd) if err != nil { return err } @@ -106,7 +67,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) error { // - the layers for _, layer := range cfg.Layers { - layerVersion := ctx.Group.Versions[layer] + layerVersion := ctx.Host.Versions[layer] if layerVersion == "" { return fmt.Errorf("layer %q not mapped to a version", layer) } diff --git a/cmd/dkl-local-server/ipxe.go b/cmd/dkl-local-server/ipxe.go index 59735a8..65abbc6 100644 --- a/cmd/dkl-local-server/ipxe.go +++ b/cmd/dkl-local-server/ipxe.go @@ -1,25 +1,13 @@ package main import ( - "bytes" "io" "log" - "text/template" ) func renderIPXE(out io.Writer, ctx *renderContext) error { log.Printf("sending IPXE code for %q", ctx.Host.Name) - tmpl, err := template.New("ipxe").Parse(ctx.Group.IPXE) - if err != nil { - return err - } - - buf := bytes.NewBuffer(make([]byte, 0, 4096)) - if err := tmpl.Execute(buf, ctx.asMap()); err != nil { - return err - } - - _, err = buf.WriteTo(out) + _, err := out.Write([]byte(ctx.Host.IPXE)) return err } diff --git a/cmd/dkl-local-server/kernel.go b/cmd/dkl-local-server/kernel.go index c61b44f..b7c01bb 100644 --- a/cmd/dkl-local-server/kernel.go +++ b/cmd/dkl-local-server/kernel.go @@ -6,12 +6,12 @@ import ( ) func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error { - path, err := ctx.distFetch("kernels", ctx.Group.Kernel) + path, err := ctx.distFetch("kernels", ctx.Host.Kernel) if err != nil { return err } - log.Printf("sending kernel %s for %q", ctx.Group.Kernel, ctx.Host.Name) + log.Printf("sending kernel %s for %q", ctx.Host.Kernel, ctx.Host.Name) http.ServeFile(w, r, path) return nil } diff --git a/cmd/dkl-local-server/render-context.go b/cmd/dkl-local-server/render-context.go index 764d054..e4e2612 100644 --- a/cmd/dkl-local-server/render-context.go +++ b/cmd/dkl-local-server/render-context.go @@ -5,108 +5,73 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" - "fmt" + "io" "log" + "net/http" "path" "path/filepath" cfsslconfig "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" + "github.com/golang/go/src/pkg/html/template" yaml "gopkg.in/yaml.v2" - "novit.nc/direktil/pkg/clustersconfig" "novit.nc/direktil/pkg/config" + "novit.nc/direktil/pkg/localconfig" ) type renderContext struct { - Host *clustersconfig.Host - Group *clustersconfig.Group - Cluster *clustersconfig.Cluster - Vars map[string]interface{} - ConfigTemplate *clustersconfig.Template - StaticPodsTemplate *clustersconfig.Template - - clusterConfig *clustersconfig.Config + Host *localconfig.Host + SSLConfig string } -func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (ctx *renderContext, err error) { - cluster := cfg.Cluster(host.Cluster) - if cluster == nil { - err = fmt.Errorf("no cluster named %q", host.Cluster) - return +func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string, + create func(out io.Writer, ctx *renderContext) error) error { + log.Printf("sending %s for %q", what, ctx.Host.Name) + + tag, err := ctx.Tag() + if err != nil { + return err } - group := cfg.Group(host.Group) - if group == nil { - err = fmt.Errorf("no group named %q", host.Group) - return + // get it or create it + content, meta, err := casStore.GetOrCreate(tag, what, func(out io.Writer) error { + log.Printf("building %s for %q", what, ctx.Host.Name) + return create(out, ctx) + }) + + if err != nil { + return err } - vars := make(map[string]interface{}) - - for _, oVars := range []map[string]interface{}{ - cluster.Vars, - group.Vars, - host.Vars, - } { - for k, v := range oVars { - vars[k] = v - } - } + // serve it + http.ServeContent(w, r, what, meta.ModTime(), content) + return nil +} +func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) { return &renderContext{ - Host: host, - Group: group, - Cluster: cluster, - Vars: vars, - ConfigTemplate: cfg.ConfigTemplate(group.Config), - StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods), - - clusterConfig: cfg, + SSLConfig: cfg.SSLConfig, + Host: host, }, nil } func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { - if ctx.ConfigTemplate == nil { - err = notFoundError{fmt.Sprintf("config %q", ctx.Group.Config)} - return - } - - ctxMap := ctx.asMap() - secretData, err := ctx.secretData() if err != nil { return } - templateFuncs := ctx.templateFuncs(secretData, ctxMap) + tmpl, err := template.New(ctx.Host.Name + "/config"). + Funcs(ctx.templateFuncs(secretData)). + Parse(ctx.Host.Config) - render := func(what string, t *clustersconfig.Template) (s string, err error) { - buf := &bytes.Buffer{} - err = t.Execute(buf, ctxMap, templateFuncs) - if err != nil { - log.Printf("host %s: failed to render %s [%q]: %v", ctx.Host.Name, what, t.Name, err) - return - } - - s = buf.String() + if err != nil { return } - extraFuncs := ctx.templateFuncs(secretData, ctxMap) - - extraFuncs["static_pods"] = func(name string) (string, error) { - t := ctx.clusterConfig.StaticPodsTemplate(name) - if t == nil { - return "", fmt.Errorf("no static pods template named %q", name) - } - - return render("static pods", t) - } - buf := bytes.NewBuffer(make([]byte, 0, 4096)) - if err = ctx.ConfigTemplate.Execute(buf, ctxMap, extraFuncs); err != nil { + if err = tmpl.Execute(buf, nil); err != nil { return } @@ -131,10 +96,10 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { func (ctx *renderContext) secretData() (data *SecretData, err error) { var sslCfg *cfsslconfig.Config - if ctx.clusterConfig.SSLConfig == "" { + if len(ctx.SSLConfig) == 0 { sslCfg = &cfsslconfig.Config{} } else { - sslCfg, err = cfsslconfig.LoadConfig([]byte(ctx.clusterConfig.SSLConfig)) + sslCfg, err = cfsslconfig.LoadConfig([]byte(ctx.SSLConfig)) if err != nil { return } @@ -144,71 +109,19 @@ func (ctx *renderContext) secretData() (data *SecretData, err error) { return } -func (ctx *renderContext) StaticPods() (ba []byte, err error) { - secretData, err := ctx.secretData() - if err != nil { - return - } - - if ctx.StaticPodsTemplate == nil { - err = notFoundError{fmt.Sprintf("static-pods %q", ctx.Group.StaticPods)} - return - } - - ctxMap := ctx.asMap() - - buf := bytes.NewBuffer(make([]byte, 0, 4096)) - if err = ctx.StaticPodsTemplate.Execute(buf, ctxMap, ctx.templateFuncs(secretData, ctxMap)); err != nil { - return - } - - if secretData.Changed() { - err = secretData.Save() - if err != nil { - return - } - } - - ba = buf.Bytes() - return -} - -func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[string]interface{}) map[string]interface{} { - cluster := ctx.Cluster.Name - - getKeyCert := func(name string) (kc *KeyCert, err error) { - req := ctx.clusterConfig.CSR(name) - if req == nil { - err = errors.New("no such certificate request") - return - } - - if req.CA == "" { - err = errors.New("CA not defined") - return - } - - buf := &bytes.Buffer{} - err = req.Execute(buf, ctxMap, nil) - if err != nil { - return - } - +func (ctx *renderContext) templateFuncs(secretData *SecretData) map[string]interface{} { + getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) { certReq := &csr.CertificateRequest{ KeyRequest: csr.NewBasicKeyRequest(), } - err = json.Unmarshal(buf.Bytes(), certReq) + err = json.Unmarshal([]byte(reqJson), certReq) if err != nil { - log.Print("unmarshal failed on: ", buf) + log.Print("CSR unmarshal failed on: ", reqJson) return } - if req.PerHost { - name = name + "/" + ctx.Host.Name - } - - return secretData.KeyCert(cluster, req.CA, name, req.Profile, req.Label, certReq) + return secretData.KeyCert(cluster, caName, name, profile, label, certReq) } asYaml := func(v interface{}) (string, error) { @@ -221,11 +134,11 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin } return map[string]interface{}{ - "token": func(name string) (s string, err error) { + "token": func(cluster, name string) (s string, err error) { return secretData.Token(cluster, name) }, - "ca_key": func(name string) (s string, err error) { + "ca_key": func(cluster, name string) (s string, err error) { ca, err := secretData.CA(cluster, name) if err != nil { return @@ -235,7 +148,7 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin return }, - "ca_crt": func(name string) (s string, err error) { + "ca_crt": func(cluster, name string) (s string, err error) { ca, err := secretData.CA(cluster, name) if err != nil { return @@ -245,13 +158,13 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin return }, - "ca_dir": func(name string) (s string, err error) { + "ca_dir": func(cluster, name string) (s string, err error) { ca, err := secretData.CA(cluster, name) if err != nil { return } - dir := "/" + path.Join("etc", "tls-ca", name) + dir := "/etc/tls-ca/" + name return asYaml([]config.FileDef{ { @@ -267,8 +180,8 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin }) }, - "tls_key": func(name string) (s string, err error) { - kc, err := getKeyCert(name) + "tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) if err != nil { return } @@ -277,8 +190,8 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin return }, - "tls_crt": func(name string) (s string, err error) { - kc, err := getKeyCert(name) + "tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) if err != nil { return } @@ -287,24 +200,18 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin return }, - "tls_dir": func(name string) (s string, err error) { - csr := ctx.clusterConfig.CSR(name) - if csr == nil { - err = fmt.Errorf("no CSR named %q", name) - return - } - - ca, err := secretData.CA(cluster, csr.CA) + "tls_dir": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) { + ca, err := secretData.CA(cluster, caName) if err != nil { return } - kc, err := getKeyCert(name) + kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson) if err != nil { return } - dir := "/" + path.Join("etc", "tls", name) + dir := "/etc/tls/" + name return asYaml([]config.FileDef{ { @@ -324,31 +231,6 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin }, }) }, - - "hosts_of_group": func() (hosts []interface{}) { - hosts = make([]interface{}, 0) - - for _, host := range ctx.clusterConfig.Hosts { - if host.Group != ctx.Host.Group { - continue - } - - hosts = append(hosts, asMap(host)) - } - - return hosts - }, - - "hosts_of_group_count": func() (count int) { - for _, host := range ctx.clusterConfig.Hosts { - if host.Group != ctx.Host.Group { - continue - } - - count++ - } - return - }, } } @@ -375,17 +257,6 @@ func (ctx *renderContext) Tag() (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } -func (ctx *renderContext) asMap() map[string]interface{} { - result := asMap(ctx) - - // also expand cluster: - cluster := result["cluster"].(map[interface{}]interface{}) - cluster["kubernetes_svc_ip"] = ctx.Cluster.KubernetesSvcIP().String() - cluster["dns_svc_ip"] = ctx.Cluster.DNSSvcIP().String() - - return result -} - func asMap(v interface{}) map[string]interface{} { ba, err := yaml.Marshal(v) if err != nil { diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..a5a4610 --- /dev/null +++ b/modd.conf @@ -0,0 +1,7 @@ +**/*.go Dockerfile { + #prep: go test ./... + #prep: go install ./cmd/... + prep: go install ./cmd/dkl-local-server + #prep: docker build -t dls . + daemon +sigterm: /var/lib/direktil/test-run +}