403 lines
8.0 KiB
Go
403 lines
8.0 KiB
Go
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
|
|
}
|