309 lines
6.2 KiB
Go
309 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"text/template"
|
|
|
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
|
"github.com/cloudflare/cfssl/csr"
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
"novit.nc/direktil/pkg/config"
|
|
"novit.nc/direktil/pkg/localconfig"
|
|
)
|
|
|
|
type renderContext struct {
|
|
Host *localconfig.Host
|
|
SSLConfig string
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var prevSSLConfig = "-"
|
|
|
|
func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
|
|
if prevSSLConfig != cfg.SSLConfig {
|
|
var sslCfg *cfsslconfig.Config
|
|
|
|
if len(cfg.SSLConfig) == 0 {
|
|
sslCfg = &cfsslconfig.Config{}
|
|
} else {
|
|
sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
err = loadSecretData(sslCfg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
prevSSLConfig = cfg.SSLConfig
|
|
}
|
|
|
|
return &renderContext{
|
|
SSLConfig: cfg.SSLConfig,
|
|
Host: host,
|
|
}, nil
|
|
}
|
|
|
|
func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
|
|
tmpl, err := template.New(ctx.Host.Name + "/config").
|
|
Funcs(ctx.templateFuncs()).
|
|
Parse(ctx.Host.Config)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
|
if err = tmpl.Execute(buf, nil); 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) templateFuncs() 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([]byte(reqJson), certReq)
|
|
if err != nil {
|
|
log.Print("CSR unmarshal failed on: ", reqJson)
|
|
return
|
|
}
|
|
|
|
return secretData.KeyCert(cluster, caName, name, profile, 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{}{
|
|
"password": func(cluster, name string) (password string, err error) {
|
|
password = secretData.Password(cluster, name)
|
|
if len(password) == 0 {
|
|
err = fmt.Errorf("password %q not defined for cluster %q", name, cluster)
|
|
}
|
|
return
|
|
},
|
|
|
|
"token": func(cluster, name string) (s string, err error) {
|
|
return secretData.Token(cluster, name)
|
|
},
|
|
|
|
"ca_key": func(cluster, name string) (s string, err error) {
|
|
ca, err := secretData.CA(cluster, name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s = string(ca.Key)
|
|
return
|
|
},
|
|
|
|
"ca_crt": func(cluster, name string) (s string, err error) {
|
|
ca, err := secretData.CA(cluster, name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s = string(ca.Cert)
|
|
return
|
|
},
|
|
|
|
"ca_dir": func(cluster, name string) (s string, err error) {
|
|
ca, err := secretData.CA(cluster, name)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
dir := "/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(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
|
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s = string(kc.Key)
|
|
return
|
|
},
|
|
|
|
"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
|
|
}
|
|
|
|
s = string(kc.Cert)
|
|
return
|
|
},
|
|
|
|
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
|
ca, err := secretData.CA(cluster, caName)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
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),
|
|
},
|
|
})
|
|
},
|
|
|
|
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
|
pairs, err := secretData.SSHKeyPairs(cluster, host)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
files := make([]config.FileDef, 0, len(pairs)*2)
|
|
|
|
for _, pair := range pairs {
|
|
basePath := path.Join(dir, "ssh_host_"+pair.Type+"_key")
|
|
files = append(files, []config.FileDef{
|
|
{
|
|
Path: basePath,
|
|
Mode: 0600,
|
|
Content: pair.Private,
|
|
},
|
|
{
|
|
Path: basePath + ".pub",
|
|
Mode: 0644,
|
|
Content: pair.Public,
|
|
},
|
|
}...)
|
|
}
|
|
|
|
return asYaml(files)
|
|
},
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|