host templates

This commit is contained in:
Mikaël Cluseau
2024-04-15 15:32:43 +02:00
parent d4dbe709e0
commit 699b8e71a6
16 changed files with 315 additions and 46 deletions

View File

@ -96,7 +96,7 @@ func main() {
ctx.Host.Versions["modules"] = ctx.Host.Kernel
}
dst.Hosts = append(dst.Hosts, &localconfig.Host{
renderedHost := &localconfig.Host{
Name: host.Name,
ClusterName: ctx.Cluster.Name,
@ -115,7 +115,13 @@ func main() {
BootstrapConfig: ctx.BootstrapConfig(),
Config: ctx.Config(),
})
}
if host.Template {
dst.HostTemplates = append(dst.HostTemplates, renderedHost)
} else {
dst.Hosts = append(dst.Hosts, renderedHost)
}
}
// ----------------------------------------------------------------------

View File

@ -2,8 +2,6 @@ package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"log"
@ -180,9 +178,20 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
return string(ba), err
}
extraFuncs["host_ip"] = func() string {
if ctx.Host.Template {
return "{{ host_ip }}"
}
return ctx.Host.IP
}
extraFuncs["host_name"] = func() string {
if ctx.Host.Template {
return "{{ host_name }}"
}
return ctx.Host.Name
}
extraFuncs["machine_id"] = func() string {
ba := sha1.Sum([]byte(ctx.Cluster.Name + "/" + ctx.Host.Name)) // TODO: check semantics of machine-id
return hex.EncodeToString(ba[:])
return "{{ machine_id }}"
}
extraFuncs["version"] = func() string { return Version }

View File

@ -63,7 +63,7 @@ func (ctx *renderContext) renderHostTemplates(setName string,
log.Print("rendering host templates in ", setName)
buf := &bytes.Buffer{}
buf := bytes.NewBuffer(make([]byte, 0, 16<<10))
for _, t := range templates {
log.Print("- template: ", setName, ": ", t.Name)

View File

@ -62,7 +62,7 @@ func main() {
log.Fatal(err)
}
log.Print("store auto-unlocked, token is ", adminToken)
log.Print("store auto-unlocked")
}
os.Setenv("DLS_AUTO_UNLOCK", "")

View File

@ -2,6 +2,7 @@ package main
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
@ -175,6 +176,17 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
funcs := templateFuncs(ctx.SSLConfig)
for name, method := range map[string]any{
"host_ip": func() (s string) {
return ctx.Host.IPs[0]
},
"host_name": func() (s string) {
return ctx.Host.Name
},
"machine_id": func() (s string) {
ba := sha1.Sum([]byte(ctx.Host.ClusterName + "/" + ctx.Host.Name))
return hex.EncodeToString(ba[:])
},
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
if host == "" {
host = ctx.Host.Name

View File

@ -247,6 +247,31 @@ func (s KVSecrets[T]) Data() (kvs map[string]T, err error) {
return
}
type KV[T any] struct {
K string
V T
}
func (s KVSecrets[T]) List(prefix string) (list []KV[T], err error) {
data, err := s.Data()
if err != nil {
return
}
list = make([]KV[T], 0, len(data))
for k, v := range data {
if !strings.HasPrefix(k, prefix) {
continue
}
list = append(list, KV[T]{k, v})
}
sort.Slice(list, func(i, j int) bool { return list[i].K < list[j].K })
return
}
func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
kvs, err := s.Data()
if err != nil {
@ -291,6 +316,20 @@ func (s KVSecrets[T]) Put(key string, v T) (err error) {
return
}
func (s KVSecrets[T]) Del(key string) (err error) {
secL.Lock()
defer secL.Unlock()
kvs, err := s.Data()
if err != nil {
return
}
delete(kvs, key)
err = writeSecret(s.Name, kvs)
return
}
func (s KVSecrets[T]) GetOrCreate(key string, create func() (T, error)) (v T, err error) {
v, found, err := s.Get(key)
if err != nil {
@ -348,3 +387,11 @@ func (s KVSecrets[T]) WsPut(req *restful.Request, resp *restful.Response, key st
return
}
}
func (s KVSecrets[T]) WsDel(req *restful.Request, resp *restful.Response, key string) {
err := s.Del(key)
if err != nil {
wsError(resp, err)
return
}
}

View File

@ -31,6 +31,8 @@ type State struct {
Config *localconfig.Config
Downloads map[string]DownloadSpec
HostTemplates []string
}
type ClusterState struct {
@ -45,6 +47,8 @@ type HostState struct {
Name string
Cluster string
IPs []string
Template string `json:",omitempty"`
}
type CAState struct {
@ -117,22 +121,44 @@ func updateState() {
clusters = append(clusters, c)
}
hosts := make([]HostState, 0, len(cfg.Hosts))
hfts, err := hostsFromTemplate.List("")
if err != nil {
log.Print("failed to read hosts from template: ", err)
}
hosts := make([]HostState, 0, len(cfg.Hosts)+len(hfts))
for _, host := range cfg.Hosts {
h := HostState{
Name: host.Name,
Cluster: host.ClusterName,
IPs: host.IPs,
}
hosts = append(hosts, h)
}
for _, kv := range hfts {
name, hft := kv.K, kv.V
h := HostState{
Name: name,
Cluster: hft.ClusterName(cfg),
IPs: []string{hft.IP},
Template: hft.Template,
}
hosts = append(hosts, h)
}
hostTemplates := make([]string, 0, len(cfg.HostTemplates))
for _, ht := range cfg.HostTemplates {
hostTemplates = append(hostTemplates, ht.Name)
}
// done
wState.Change(func(v *State) {
v.HasConfig = true
v.Store.KeyNames = keyNames
v.Clusters = clusters
v.Hosts = hosts
v.HostTemplates = hostTemplates
})
}

View File

@ -130,7 +130,7 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
}
case "host":
host := cfg.Host(spec.Name)
host := hostOrTemplate(cfg, spec.Name)
if host == nil {
wsNotFound(resp)
return

View File

@ -114,12 +114,12 @@ func (ws wsHost) host(req *restful.Request, resp *restful.Response) (host *local
return
}
host = cfg.Host(hostname)
host = hostOrTemplate(cfg, hostname)
if host == nil {
log.Print("no host named ", hostname)
wsNotFound(resp)
return
}
return
}

View File

@ -0,0 +1,108 @@
package main
import (
"log"
"github.com/emicklei/go-restful"
"novit.tech/direktil/pkg/localconfig"
)
var hostsFromTemplate = KVSecrets[HostFromTemplate]{"hosts-from-template"}
type HostFromTemplate struct {
Template string
IP string
}
func (hft HostFromTemplate) ClusterName(cfg *localconfig.Config) string {
for _, ht := range cfg.HostTemplates {
if ht.Name == hft.Template {
return ht.ClusterName
}
}
return ""
}
func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Host) {
host = cfg.Host(name)
if host != nil {
log.Print("no host named ", name)
return
}
hft, found, err := hostsFromTemplate.Get(name)
if err != nil {
log.Print("failed to read store: ", err)
return
}
if !found {
log.Print("no host from template named ", name)
return
}
ht := cfg.HostTemplate(hft.Template)
if ht == nil {
log.Print("no host template named ", name)
return
}
host = &localconfig.Host{}
*host = *ht
host.Name = name
host.IPs = []string{hft.IP}
return
}
func wsHostsFromTemplateSet(req *restful.Request, resp *restful.Response) {
name := req.PathParameter("name")
cfg, err := readConfig()
if err != nil {
wsError(resp, err)
return
}
v := HostFromTemplate{}
if err := req.ReadEntity(&v); err != nil {
wsBadRequest(resp, err.Error())
return
}
if v.Template == "" {
wsBadRequest(resp, "template is required")
return
}
if v.IP == "" {
wsBadRequest(resp, "ip is required")
return
}
found := false
for _, ht := range cfg.HostTemplates {
if ht.Name != v.Template {
continue
}
found = true
break
}
if !found {
wsBadRequest(resp, "no host template with this name")
return
}
if err := hostsFromTemplate.Put(name, v); err != nil {
wsError(resp, err)
return
}
updateState()
}
func wsHostsFromTemplateDelete(req *restful.Request, resp *restful.Response) {
name := req.PathParameter("name")
hostsFromTemplate.WsDel(req, resp, name)
updateState()
}

View File

@ -76,6 +76,13 @@ func registerWS(rest *restful.Container) {
ws.Route(ws.GET("/clusters").To(wsListClusters).
Doc("List clusters"))
ws.Route(ws.POST("/hosts-from-template/{name}").To(wsHostsFromTemplateSet).
Reads(HostFromTemplate{}).
Doc("Create or update a host template instance"))
ws.Route(ws.DELETE("/hosts-from-template/{name}").To(wsHostsFromTemplateDelete).
Reads(HostFromTemplate{}).
Doc("Delete a host template instance"))
const (
GET = http.MethodGet
PUT = http.MethodPut