host templates
This commit is contained in:
		| @ -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", "") | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
|  | ||||
							
								
								
									
										108
									
								
								cmd/dkl-local-server/ws-hosts-from-templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								cmd/dkl-local-server/ws-hosts-from-templates.go
									
									
									
									
									
										Normal 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() | ||||
| } | ||||
| @ -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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user