This commit is contained in:
Mikaël Cluseau 2018-12-10 21:59:24 +11:00
parent 63f991bbd2
commit 26b6efd54c
13 changed files with 788 additions and 340 deletions

View File

@ -2,13 +2,10 @@
from golang:1.11.2 as build from golang:1.11.2 as build
env pkg novit.nc/direktil/local-server env pkg novit.nc/direktil/local-server
run go get github.com/gobuffalo/packr/packr
copy vendor /go/src/${pkg}/vendor copy vendor /go/src/${pkg}/vendor
copy cmd /go/src/${pkg}/cmd copy cmd /go/src/${pkg}/cmd
copy assets /go/src/${pkg}/cmd/dkl-local-server/assets
workdir /go/src/${pkg} workdir /go/src/${pkg}
run packr -i /go/src/${pkg}/cmd/dkl-local-server \ run go test ./... \
&& go test ./... \
&& go install ./cmd/... && go install ./cmd/...
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------

293
cmd/dkl-dir2config/http.go Normal file
View File

@ -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/<name>/<what>" split => "", "clusters", "<name>", "<what>"
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)
}
}

View File

@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("vim-go")
}

View File

@ -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
}

View File

@ -132,13 +132,13 @@ menuentry "Direktil" {
} }
copies := []distCopy{ copies := []distCopy{
{Src: []string{"kernels", ctx.Group.Kernel}, Dst: "vmlinuz"}, {Src: []string{"kernels", ctx.Host.Kernel}, Dst: "vmlinuz"},
{Src: []string{"initrd", ctx.Group.Initrd}, Dst: "initrd"}, {Src: []string{"initrd", ctx.Host.Initrd}, Dst: "initrd"},
} }
// layers // layers
for _, layer := range cfg.Layers { for _, layer := range cfg.Layers {
layerVersion := ctx.Group.Versions[layer] layerVersion := ctx.Host.Versions[layer]
if layerVersion == "" { if layerVersion == "" {
return fmt.Errorf("layer %q not mapped to a version", layer) return fmt.Errorf("layer %q not mapped to a version", layer)
} }

View File

@ -4,7 +4,6 @@ import (
"archive/tar" "archive/tar"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -18,18 +17,6 @@ func rmTempFile(f *os.File) {
} }
func buildBootTar(out io.Writer, ctx *renderContext) (err error) { 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) arch := tar.NewWriter(out)
defer arch.Close() defer arch.Close()
@ -58,13 +45,13 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
// kernel and initrd // kernel and initrd
copies := []distCopy{ copies := []distCopy{
{Src: []string{"kernels", ctx.Group.Kernel}, Dst: "current/vmlinuz"}, {Src: []string{"kernels", ctx.Host.Kernel}, Dst: "current/vmlinuz"},
{Src: []string{"initrd", ctx.Group.Initrd}, Dst: "current/initrd"}, {Src: []string{"initrd", ctx.Host.Initrd}, Dst: "current/initrd"},
} }
// layers // layers
for _, layer := range cfg.Layers { for _, layer := range cfg.Layers {
layerVersion := ctx.Group.Versions[layer] layerVersion := ctx.Host.Versions[layer]
if layerVersion == "" { if layerVersion == "" {
return fmt.Errorf("layer %q not mapped to a version", layer) return fmt.Errorf("layer %q not mapped to a version", layer)
} }

View File

@ -2,10 +2,9 @@ package main
import ( import (
"flag" "flag"
"log"
"path/filepath" "path/filepath"
"novit.nc/direktil/pkg/clustersconfig" "novit.nc/direktil/pkg/localconfig"
) )
var ( var (
@ -13,22 +12,8 @@ var (
configFromDir = flag.String("config-from-dir", "", "Build configuration from this directory") configFromDir = flag.String("config-from-dir", "", "Build configuration from this directory")
) )
func readConfig() (config *clustersconfig.Config, err error) { func readConfig() (config *localconfig.Config, err error) {
configFile := filepath.Join(*dataDir, "current-config.yaml") configFile := filepath.Join(*dataDir, "config.yaml")
if *configFromDir != "" { return localconfig.FromFile(configFile)
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)
} }

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"flag" "flag"
"log" "log"
@ -10,8 +9,7 @@ import (
"regexp" "regexp"
"strings" "strings"
yaml "gopkg.in/yaml.v2" "novit.nc/direktil/pkg/localconfig"
"novit.nc/direktil/pkg/clustersconfig"
) )
var ( var (
@ -49,7 +47,7 @@ func serveHostByIP(w http.ResponseWriter, r *http.Request) {
renderHost(w, r, what, host, cfg) 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 remoteAddr := r.RemoteAddr
if *trustXFF { if *trustXFF {
@ -131,7 +129,7 @@ func serveHost(w http.ResponseWriter, r *http.Request) {
renderHost(w, r, what, host, cfg) 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) ctx, err := newRenderContext(host, cfg)
if err != nil { if err != nil {
log.Printf("host %s: %s: failed to render: %v", what, host.Name, err) 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) err = renderKernel(w, r, ctx)
case "initrd": case "initrd":
err = renderCtx(w, r, ctx, "initrd", buildInitrd) err = renderCtx(w, r, ctx, what, buildInitrd)
case "boot.iso": case "boot.iso":
err = renderCtx(w, r, ctx, "boot.iso", buildBootISO) err = renderCtx(w, r, ctx, what, buildBootISO)
case "boot.tar": case "boot.tar":
err = renderCtx(w, r, ctx, "boot.tar", buildBootTar) err = renderCtx(w, r, ctx, what, buildBootTar)
case "boot.img": case "boot.img":
err = renderCtx(w, r, ctx, what, buildBootImg) 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": case "config":
err = renderConfig(w, r, ctx) err = renderConfig(w, r, ctx)
case "static-pods":
if ctx.Group.StaticPods == "" {
w.WriteHeader(http.StatusNoContent)
return
}
err = renderStaticPods(w, r, ctx)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@ -228,16 +219,11 @@ func serveCluster(w http.ResponseWriter, r *http.Request) {
} }
clusterName := p[2] clusterName := p[2]
what := p[3]
p = strings.SplitN(p[3], ".", 2)
what := p[0]
format := ""
if len(p) > 1 {
format = p[1]
}
cfg, err := readConfig() cfg, err := readConfig()
if err != nil { if err != nil {
log.Print("failed to read config: ", err)
http.Error(w, "", http.StatusServiceUnavailable) http.Error(w, "", http.StatusServiceUnavailable)
return return
} }
@ -250,49 +236,13 @@ func serveCluster(w http.ResponseWriter, r *http.Request) {
switch what { switch what {
case "addons": case "addons":
if cluster.Addons == "" { if len(cluster.Addons) == 0 {
log.Printf("cluster %q has no addons defined", clusterName) log.Printf("cluster %q has no addons defined", clusterName)
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
addons := cfg.Addons[cluster.Addons] w.Write([]byte(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: default:
http.NotFound(w, r) http.NotFound(w, r)

View File

@ -32,45 +32,6 @@ func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext) er
return nil 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 { func buildInitrd(out io.Writer, ctx *renderContext) error {
_, cfg, err := ctx.Config() _, cfg, err := ctx.Config()
@ -79,7 +40,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) error {
} }
// send initrd basis // send initrd basis
initrdPath, err := ctx.distFetch("initrd", ctx.Group.Initrd) initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd)
if err != nil { if err != nil {
return err return err
} }
@ -106,7 +67,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) error {
// - the layers // - the layers
for _, layer := range cfg.Layers { for _, layer := range cfg.Layers {
layerVersion := ctx.Group.Versions[layer] layerVersion := ctx.Host.Versions[layer]
if layerVersion == "" { if layerVersion == "" {
return fmt.Errorf("layer %q not mapped to a version", layer) return fmt.Errorf("layer %q not mapped to a version", layer)
} }

View File

@ -1,25 +1,13 @@
package main package main
import ( import (
"bytes"
"io" "io"
"log" "log"
"text/template"
) )
func renderIPXE(out io.Writer, ctx *renderContext) error { func renderIPXE(out io.Writer, ctx *renderContext) error {
log.Printf("sending IPXE code for %q", ctx.Host.Name) log.Printf("sending IPXE code for %q", ctx.Host.Name)
tmpl, err := template.New("ipxe").Parse(ctx.Group.IPXE) _, err := out.Write([]byte(ctx.Host.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)
return err return err
} }

View File

@ -6,12 +6,12 @@ import (
) )
func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error { 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 { if err != nil {
return err 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) http.ServeFile(w, r, path)
return nil return nil
} }

View File

@ -5,108 +5,73 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "io"
"fmt"
"log" "log"
"net/http"
"path" "path"
"path/filepath" "path/filepath"
cfsslconfig "github.com/cloudflare/cfssl/config" cfsslconfig "github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/csr"
"github.com/golang/go/src/pkg/html/template"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
"novit.nc/direktil/pkg/clustersconfig"
"novit.nc/direktil/pkg/config" "novit.nc/direktil/pkg/config"
"novit.nc/direktil/pkg/localconfig"
) )
type renderContext struct { type renderContext struct {
Host *clustersconfig.Host Host *localconfig.Host
Group *clustersconfig.Group SSLConfig string
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) { func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string,
cluster := cfg.Cluster(host.Cluster) create func(out io.Writer, ctx *renderContext) error) error {
if cluster == nil { log.Printf("sending %s for %q", what, ctx.Host.Name)
err = fmt.Errorf("no cluster named %q", host.Cluster)
return tag, err := ctx.Tag()
if err != nil {
return err
} }
group := cfg.Group(host.Group) // get it or create it
if group == nil { content, meta, err := casStore.GetOrCreate(tag, what, func(out io.Writer) error {
err = fmt.Errorf("no group named %q", host.Group) log.Printf("building %s for %q", what, ctx.Host.Name)
return return create(out, ctx)
})
if err != nil {
return err
} }
vars := make(map[string]interface{}) // serve it
http.ServeContent(w, r, what, meta.ModTime(), content)
for _, oVars := range []map[string]interface{}{ return nil
cluster.Vars,
group.Vars,
host.Vars,
} {
for k, v := range oVars {
vars[k] = v
}
} }
func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
return &renderContext{ return &renderContext{
SSLConfig: cfg.SSLConfig,
Host: host, Host: host,
Group: group,
Cluster: cluster,
Vars: vars,
ConfigTemplate: cfg.ConfigTemplate(group.Config),
StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods),
clusterConfig: cfg,
}, nil }, nil
} }
func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) { 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() secretData, err := ctx.secretData()
if err != nil { if err != nil {
return 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 { if err != nil {
log.Printf("host %s: failed to render %s [%q]: %v", ctx.Host.Name, what, t.Name, err)
return 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)) 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 return
} }
@ -131,10 +96,10 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
func (ctx *renderContext) secretData() (data *SecretData, err error) { func (ctx *renderContext) secretData() (data *SecretData, err error) {
var sslCfg *cfsslconfig.Config var sslCfg *cfsslconfig.Config
if ctx.clusterConfig.SSLConfig == "" { if len(ctx.SSLConfig) == 0 {
sslCfg = &cfsslconfig.Config{} sslCfg = &cfsslconfig.Config{}
} else { } else {
sslCfg, err = cfsslconfig.LoadConfig([]byte(ctx.clusterConfig.SSLConfig)) sslCfg, err = cfsslconfig.LoadConfig([]byte(ctx.SSLConfig))
if err != nil { if err != nil {
return return
} }
@ -144,71 +109,19 @@ func (ctx *renderContext) secretData() (data *SecretData, err error) {
return return
} }
func (ctx *renderContext) StaticPods() (ba []byte, err error) { func (ctx *renderContext) templateFuncs(secretData *SecretData) map[string]interface{} {
secretData, err := ctx.secretData() getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) {
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{ certReq := &csr.CertificateRequest{
KeyRequest: csr.NewBasicKeyRequest(), KeyRequest: csr.NewBasicKeyRequest(),
} }
err = json.Unmarshal(buf.Bytes(), certReq) err = json.Unmarshal([]byte(reqJson), certReq)
if err != nil { if err != nil {
log.Print("unmarshal failed on: ", buf) log.Print("CSR unmarshal failed on: ", reqJson)
return return
} }
if req.PerHost { return secretData.KeyCert(cluster, caName, name, profile, label, certReq)
name = name + "/" + ctx.Host.Name
}
return secretData.KeyCert(cluster, req.CA, name, req.Profile, req.Label, certReq)
} }
asYaml := func(v interface{}) (string, error) { asYaml := func(v interface{}) (string, error) {
@ -221,11 +134,11 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin
} }
return map[string]interface{}{ 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) 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) ca, err := secretData.CA(cluster, name)
if err != nil { if err != nil {
return return
@ -235,7 +148,7 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin
return 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) ca, err := secretData.CA(cluster, name)
if err != nil { if err != nil {
return return
@ -245,13 +158,13 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin
return 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) ca, err := secretData.CA(cluster, name)
if err != nil { if err != nil {
return return
} }
dir := "/" + path.Join("etc", "tls-ca", name) dir := "/etc/tls-ca/" + name
return asYaml([]config.FileDef{ 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) { "tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
kc, err := getKeyCert(name) kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
if err != nil { if err != nil {
return return
} }
@ -277,8 +190,8 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin
return return
}, },
"tls_crt": func(name string) (s string, err error) { "tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
kc, err := getKeyCert(name) kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
if err != nil { if err != nil {
return return
} }
@ -287,24 +200,18 @@ func (ctx *renderContext) templateFuncs(secretData *SecretData, ctxMap map[strin
return return
}, },
"tls_dir": func(name string) (s string, err error) { "tls_dir": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
csr := ctx.clusterConfig.CSR(name) ca, err := secretData.CA(cluster, caName)
if csr == nil {
err = fmt.Errorf("no CSR named %q", name)
return
}
ca, err := secretData.CA(cluster, csr.CA)
if err != nil { if err != nil {
return return
} }
kc, err := getKeyCert(name) kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
if err != nil { if err != nil {
return return
} }
dir := "/" + path.Join("etc", "tls", name) dir := "/etc/tls/" + name
return asYaml([]config.FileDef{ 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 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{} { func asMap(v interface{}) map[string]interface{} {
ba, err := yaml.Marshal(v) ba, err := yaml.Marshal(v)
if err != nil { if err != nil {

7
modd.conf Normal file
View File

@ -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
}