Compare commits
49 Commits
6197369e04
...
main
Author | SHA1 | Date | |
---|---|---|---|
4b05458cec | |||
84a0e286e7 | |||
58cfaa7d0f | |||
1871eac7bb | |||
b12ce7299f | |||
82f7cbcc92 | |||
ce8b7f01ef | |||
edbe1641fd | |||
aac792c341 | |||
eaeb38b8c2 | |||
e0f755ec42 | |||
bb7c3835bc | |||
7c9334233d | |||
699b8e71a6 | |||
d4dbe709e0 | |||
22a3e0b6c2 | |||
e08bf0e99d | |||
1e904b7361 | |||
8ed0f12fb4 | |||
f59eca6724 | |||
b5b7272603 | |||
4f48866daa | |||
b616b710cb | |||
c02f701c04 | |||
7f429a863d | |||
29ed01a19f | |||
07e9dccd06 | |||
40d08139db | |||
efa6193954 | |||
f7b708ce4b | |||
41897c00b4 | |||
ee5629643c | |||
34afe03818 | |||
25c2d20c19 | |||
c338522b33 | |||
b6fa941fcc | |||
7619998d8f | |||
b6e7c55704 | |||
4ed50e3b78 | |||
dac6613646 | |||
a8ccb6990b | |||
b1cdb30622 | |||
50bb60823f | |||
482d3c83ba | |||
74abbf9eda | |||
76c1861017 | |||
0d0494b825 | |||
c6320049ff | |||
9e56acfc9a |
@ -1 +1,3 @@
|
|||||||
tmp
|
tmp
|
||||||
|
dist
|
||||||
|
test-run
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
*.sw[po]
|
*.sw[po]
|
||||||
modd-local.conf
|
modd-local.conf
|
||||||
/tmp
|
/tmp
|
||||||
|
/test-dir2config
|
||||||
|
/config.yaml
|
||||||
/dist
|
/dist
|
||||||
/go.work
|
/go.work
|
||||||
/go.work.sum
|
/go.work.sum
|
||||||
|
40
Dockerfile
40
Dockerfile
@ -1,23 +1,33 @@
|
|||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from mcluseau/golang-builder:1.20.2 as build
|
from golang:1.24.3-bookworm as build
|
||||||
|
|
||||||
|
run apt-get update && apt-get install -y git
|
||||||
|
|
||||||
|
workdir /src
|
||||||
|
|
||||||
|
copy go.mod go.sum ./
|
||||||
|
run \
|
||||||
|
--mount=type=cache,id=gomod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=gobuild,target=/root/.cache/go-build \
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
arg GIT_TAG
|
||||||
|
|
||||||
|
copy . ./
|
||||||
|
run \
|
||||||
|
--mount=type=cache,id=gomod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=gobuild,target=/root/.cache/go-build \
|
||||||
|
go test ./... && \
|
||||||
|
hack/build ./...
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from debian:stretch
|
from debian:bookworm
|
||||||
entrypoint ["/bin/dkl-local-server"]
|
entrypoint ["/bin/dkl-local-server"]
|
||||||
|
|
||||||
env _uncache 1
|
env _uncache=1
|
||||||
run apt-get update \
|
run apt-get update \
|
||||||
&& apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
&& yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
||||||
|
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils \
|
||||||
&& apt-get clean
|
&& apt-get clean
|
||||||
|
|
||||||
run yes |apt-get install -y grub2 grub-pc-bin grub-efi-amd64-bin \
|
copy --from=build /src/dist/ /bin/
|
||||||
&& apt-get clean
|
|
||||||
|
|
||||||
run apt-get install -y ca-certificates curl openssh-client \
|
|
||||||
&& apt-get clean
|
|
||||||
|
|
||||||
run curl -L https://github.com/vmware/govmomi/releases/download/v0.21.0/govc_linux_amd64.gz | gunzip > /bin/govc && chmod +x /bin/govc
|
|
||||||
|
|
||||||
copy upload-vmware.sh govc.env /var/lib/direktil/
|
|
||||||
|
|
||||||
copy --from=build /go/bin/ /bin/
|
|
||||||
|
155
cmd/dkl-dir2config/assemble.go
Normal file
155
cmd/dkl-dir2config/assemble.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mergeIn(tgt, add map[any]any) {
|
||||||
|
mergeLoop:
|
||||||
|
for k, v := range add {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case map[any]any:
|
||||||
|
if tgtV, ok := tgt[k]; ok {
|
||||||
|
switch tgtV := tgtV.(type) {
|
||||||
|
case map[any]any:
|
||||||
|
mergeIn(tgtV, v)
|
||||||
|
continue mergeLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tgt[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assemble(path string) (yamlBytes []byte, err error) {
|
||||||
|
obj := map[any]any{}
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
log.Printf("assemble %q", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eachFragment(path, searchList, func(r io.Reader) (err error) {
|
||||||
|
m := map[any]any{}
|
||||||
|
err = yaml.NewDecoder(r).Decode(&m)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeIn(obj, m)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("failed to assemble %q: %w", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlBytes, err = yaml.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
log.Printf("assemble %q result:\n%s", path, yamlBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func eachFragment(path string, searchList []FS, walk func(io.Reader) error) (err error) {
|
||||||
|
var r io.ReadCloser
|
||||||
|
|
||||||
|
for len(searchList) != 0 {
|
||||||
|
fs := searchList[0]
|
||||||
|
|
||||||
|
r, err = fs.Open(path + ".yaml")
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
searchList = searchList[1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// found and open
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ba, err := io.ReadAll(r)
|
||||||
|
r.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
log.Print("fragment:\n", string(ba))
|
||||||
|
}
|
||||||
|
|
||||||
|
in := bytes.NewBuffer(ba)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var line string
|
||||||
|
line, err = in.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
genCmd, found := strings.CutPrefix(line, "#!gen ")
|
||||||
|
if found {
|
||||||
|
cmdArgs := strings.Fields(genCmd)
|
||||||
|
if Debug {
|
||||||
|
log.Print("#!gen ", cmdArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := "gen/" + cmdArgs[0]
|
||||||
|
args := cmdArgs[1:]
|
||||||
|
genOutput, err := exec.Command(cmd, args...).Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gen %v: %w", cmdArgs, err)
|
||||||
|
}
|
||||||
|
walk(bytes.NewBuffer(genOutput))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath, found := strings.CutPrefix(line, "#!include ")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath = strings.TrimSpace(includePath)
|
||||||
|
if Debug {
|
||||||
|
log.Print("#!include ", includePath)
|
||||||
|
}
|
||||||
|
err = eachFragment(includePath, searchList, walk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("include %q: %w", includePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in = bytes.NewBuffer(ba)
|
||||||
|
err = walk(in)
|
||||||
|
return
|
||||||
|
}
|
34
cmd/dkl-dir2config/fs.go
Normal file
34
cmd/dkl-dir2config/fs.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
iofs "io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FS interface {
|
||||||
|
Open(path string) (io.ReadCloser, error)
|
||||||
|
List(path string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsFS struct{ iofs.FS }
|
||||||
|
|
||||||
|
func (fs fsFS) Open(path string) (io.ReadCloser, error) {
|
||||||
|
return fs.FS.Open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fsFS) List(path string) (entries []string, err error) {
|
||||||
|
dirEnts, err := iofs.ReadDir(fs.FS, path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = make([]string, 0, len(dirEnts))
|
||||||
|
for _, ent := range dirEnts {
|
||||||
|
if ent.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, ent.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
38
cmd/dkl-dir2config/git.go
Normal file
38
cmd/dkl-dir2config/git.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gitFS struct{ *object.Tree }
|
||||||
|
|
||||||
|
func (fs gitFS) Open(path string) (r io.ReadCloser, err error) {
|
||||||
|
f, err := fs.Tree.File(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return f.Reader()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs gitFS) List(path string) (entries []string, err error) {
|
||||||
|
tree, err := fs.Tree.Tree(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = make([]string, 0, len(tree.Entries))
|
||||||
|
|
||||||
|
for _, ent := range tree.Entries {
|
||||||
|
if !ent.Mode.IsFile() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = append(entries, ent.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(entries)
|
||||||
|
return
|
||||||
|
}
|
@ -2,9 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/localconfig"
|
"novit.tech/direktil/pkg/localconfig"
|
||||||
@ -12,18 +16,27 @@ import (
|
|||||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dir = flag.String("in", ".", "Source directory")
|
Debug = false
|
||||||
outPath = flag.String("out", "config.yaml", "Output file")
|
|
||||||
defaultsPath = flag.String("defaults", "defaults", "Path to the defaults")
|
dir = flag.String("in", ".", "Source directory")
|
||||||
|
outPath = flag.String("out", "config.yaml", "Output file")
|
||||||
|
|
||||||
|
base fs.FS
|
||||||
|
|
||||||
src *clustersconfig.Config
|
src *clustersconfig.Config
|
||||||
dst *localconfig.Config
|
dst *localconfig.Config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&Debug, "debug", Debug, "debug")
|
||||||
|
}
|
||||||
|
|
||||||
func loadSrc() {
|
func loadSrc() {
|
||||||
var err error
|
var err error
|
||||||
src, err = clustersconfig.FromDir(*dir, *defaultsPath)
|
src, err = clustersconfig.FromDir(read, assemble, listBase, listMerged)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("failed to load config from dir: ", err)
|
log.Fatal("failed to load config from dir: ", err)
|
||||||
}
|
}
|
||||||
@ -34,6 +47,11 @@ func main() {
|
|||||||
|
|
||||||
log.SetFlags(log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
log.SetFlags(log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
||||||
|
|
||||||
|
base = os.DirFS(*dir)
|
||||||
|
searchList = append(searchList, fsFS{base})
|
||||||
|
|
||||||
|
openIncludes()
|
||||||
|
|
||||||
loadSrc()
|
loadSrc()
|
||||||
|
|
||||||
dst = &localconfig.Config{
|
dst = &localconfig.Config{
|
||||||
@ -50,8 +68,6 @@ func main() {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
for _, host := range src.Hosts {
|
for _, host := range src.Hosts {
|
||||||
loadSrc() // FIXME ugly fix of some template caching or something
|
|
||||||
|
|
||||||
log.Print("rendering host ", host.Name)
|
log.Print("rendering host ", host.Name)
|
||||||
ctx, err := newRenderContext(host, src)
|
ctx, err := newRenderContext(host, src)
|
||||||
|
|
||||||
@ -70,12 +86,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
ips = append(ips, host.IPs...)
|
ips = append(ips, host.IPs...)
|
||||||
|
|
||||||
if ctx.Group.Versions["modules"] == "" {
|
if ctx.Host.Versions["modules"] == "" {
|
||||||
// default modules' version to kernel's version
|
// default modules' version to kernel's version
|
||||||
ctx.Group.Versions["modules"] = ctx.Group.Kernel
|
ctx.Host.Versions["modules"] = ctx.Host.Kernel
|
||||||
}
|
}
|
||||||
|
|
||||||
dst.Hosts = append(dst.Hosts, &localconfig.Host{
|
renderedHost := &localconfig.Host{
|
||||||
Name: host.Name,
|
Name: host.Name,
|
||||||
|
|
||||||
ClusterName: ctx.Cluster.Name,
|
ClusterName: ctx.Cluster.Name,
|
||||||
@ -86,15 +102,21 @@ func main() {
|
|||||||
MACs: macs,
|
MACs: macs,
|
||||||
IPs: ips,
|
IPs: ips,
|
||||||
|
|
||||||
IPXE: ctx.Group.IPXE, // TODO render
|
IPXE: ctx.Host.IPXE, // TODO render
|
||||||
|
|
||||||
Kernel: ctx.Group.Kernel,
|
Kernel: ctx.Host.Kernel,
|
||||||
Initrd: ctx.Group.Initrd,
|
Initrd: ctx.Host.Initrd,
|
||||||
Versions: ctx.Group.Versions,
|
Versions: ctx.Host.Versions,
|
||||||
|
|
||||||
BootstrapConfig: ctx.BootstrapConfig(),
|
BootstrapConfig: ctx.BootstrapConfig(),
|
||||||
Config: ctx.Config(),
|
Config: ctx.Config(),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if host.Template {
|
||||||
|
dst.HostTemplates = append(dst.HostTemplates, renderedHost)
|
||||||
|
} else {
|
||||||
|
dst.Hosts = append(dst.Hosts, renderedHost)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@ -105,8 +127,83 @@ func main() {
|
|||||||
|
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
|
out.Write([]byte("# dkl-dir2config " + Version + "\n"))
|
||||||
|
|
||||||
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
||||||
log.Fatal("failed to render output: ", err)
|
log.Fatal("failed to render output: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cfgPath(subPath string) string { return filepath.Join(*dir, subPath) }
|
||||||
|
|
||||||
|
func openIncludes() {
|
||||||
|
includesFile, err := base.Open("includes.yaml")
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open includes: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
includes := make([]struct {
|
||||||
|
Path string
|
||||||
|
Branch string
|
||||||
|
Tag string
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
err = yaml.NewDecoder(includesFile).Decode(&includes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to parse includes: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, include := range includes {
|
||||||
|
switch {
|
||||||
|
case include.Branch != "" || include.Tag != "":
|
||||||
|
p := cfgPath(include.Path) // FIXME parse git path to allow remote repos
|
||||||
|
|
||||||
|
var rev plumbing.Revision
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case include.Branch != "":
|
||||||
|
log.Printf("opening include path %q as git, branch %q", p, include.Branch)
|
||||||
|
rev = plumbing.Revision(plumbing.NewBranchReferenceName(include.Branch))
|
||||||
|
|
||||||
|
case include.Tag != "":
|
||||||
|
log.Printf("opening include path %q as git, tag %q", p, include.Branch)
|
||||||
|
rev = plumbing.Revision(plumbing.NewTagReferenceName(include.Branch))
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := git.PlainOpen(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
revH, err := repo.ResolveRevision(rev)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to resolve revision %s: %v", rev, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(" -> resolved to commit ", *revH)
|
||||||
|
|
||||||
|
commit, err := repo.CommitObject(*revH)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to get commit object: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := commit.Tree()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open git tree: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchList = append(searchList, gitFS{tree})
|
||||||
|
|
||||||
|
default:
|
||||||
|
p := cfgPath(include.Path)
|
||||||
|
log.Printf("opening include path %q as raw dir", p)
|
||||||
|
|
||||||
|
searchList = append(searchList, fsFS{os.DirFS(p)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,12 +9,12 @@ import (
|
|||||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]any {
|
||||||
cluster := clusterSpec.Name
|
cluster := clusterSpec.Name
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"password": func(name string) (s string) {
|
"password": func(name, hash string) (s string) {
|
||||||
return fmt.Sprintf("{{ password %q %q }}", cluster, name)
|
return fmt.Sprintf("{{ password %q %q %q | quote }}", cluster, name, hash)
|
||||||
},
|
},
|
||||||
|
|
||||||
"token": func(name string) (s string) {
|
"token": func(name string) (s string) {
|
||||||
@ -36,7 +36,7 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
|||||||
return fmt.Sprintf("{{ ca_dir %q %q }}", cluster, name), nil
|
return fmt.Sprintf("{{ ca_dir %q %q }}", cluster, name), nil
|
||||||
},
|
},
|
||||||
|
|
||||||
"hosts_by_cluster": func(cluster string) (hosts []interface{}) {
|
"hosts_by_cluster": func(cluster string) (hosts []any) {
|
||||||
for _, host := range src.Hosts {
|
for _, host := range src.Hosts {
|
||||||
if host.Cluster == cluster {
|
if host.Cluster == cluster {
|
||||||
hosts = append(hosts, asMap(host))
|
hosts = append(hosts, asMap(host))
|
||||||
@ -50,9 +50,9 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
|||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
"hosts_by_group": func(group string) (hosts []interface{}) {
|
"hosts_by_group": func(group string) (hosts []any) {
|
||||||
for _, host := range src.Hosts {
|
for _, host := range src.Hosts {
|
||||||
if host.Group == group {
|
if host.Cluster == cluster && host.Group == group {
|
||||||
hosts = append(hosts, asMap(host))
|
hosts = append(hosts, asMap(host))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,6 +63,26 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
|||||||
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"host_ip_from": func(hostName, net string) string {
|
||||||
|
host := src.Host(hostName)
|
||||||
|
if host == nil {
|
||||||
|
log.Printf("WARNING: no host named %q", hostName)
|
||||||
|
return "<no value>"
|
||||||
|
}
|
||||||
|
|
||||||
|
ipFrom := host.IPFrom
|
||||||
|
if ipFrom == nil {
|
||||||
|
ipFrom = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, ok := ipFrom[net]
|
||||||
|
if !ok {
|
||||||
|
ip = host.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,12 +121,18 @@ func renderAddons(cluster *clustersconfig.Cluster) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
addons := src.Addons[cluster.Addons]
|
buf := new(bytes.Buffer)
|
||||||
if addons == nil {
|
|
||||||
log.Fatalf("cluster %q: no addons with name %q", cluster.Name, cluster.Addons)
|
for _, addonSet := range cluster.Addons {
|
||||||
|
addons := src.Addons[addonSet]
|
||||||
|
if addons == nil {
|
||||||
|
log.Fatalf("cluster %q: no addons with name %q", cluster.Name, addonSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Write(renderClusterTemplates(cluster, "addons", addons))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(renderClusterTemplates(cluster, "addons", addons))
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type namePod struct {
|
type namePod struct {
|
||||||
|
@ -2,15 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cespare/xxhash"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/config"
|
"novit.tech/direktil/pkg/config"
|
||||||
@ -23,9 +23,8 @@ type renderContext struct {
|
|||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
|
|
||||||
Host *clustersconfig.Host
|
Host *clustersconfig.Host
|
||||||
Group *clustersconfig.Group
|
|
||||||
Cluster *clustersconfig.Cluster
|
Cluster *clustersconfig.Cluster
|
||||||
Vars map[string]interface{}
|
Vars map[string]any
|
||||||
|
|
||||||
BootstrapConfigTemplate *clustersconfig.Template
|
BootstrapConfigTemplate *clustersconfig.Template
|
||||||
ConfigTemplate *clustersconfig.Template
|
ConfigTemplate *clustersconfig.Template
|
||||||
@ -41,34 +40,25 @@ func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (ct
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group := cfg.Group(host.Group)
|
vars := make(map[string]any)
|
||||||
if group == nil {
|
|
||||||
err = fmt.Errorf("no group named %q", host.Group)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := make(map[string]interface{})
|
for _, oVars := range []map[string]any{
|
||||||
|
|
||||||
for _, oVars := range []map[string]interface{}{
|
|
||||||
cluster.Vars,
|
cluster.Vars,
|
||||||
group.Vars,
|
|
||||||
host.Vars,
|
host.Vars,
|
||||||
} {
|
} {
|
||||||
mapMerge(vars, oVars)
|
mapMerge(vars, oVars)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &renderContext{
|
return &renderContext{
|
||||||
Labels: mergeLabels(cluster.Labels, group.Labels, host.Labels),
|
Labels: mergeLabels(cluster.Labels, host.Labels),
|
||||||
Annotations: mergeLabels(cluster.Annotations, group.Annotations, host.Annotations),
|
Annotations: mergeLabels(cluster.Annotations, host.Annotations),
|
||||||
|
|
||||||
Host: host,
|
Host: host,
|
||||||
Group: group,
|
|
||||||
Cluster: cluster,
|
Cluster: cluster,
|
||||||
Vars: vars,
|
Vars: vars,
|
||||||
|
|
||||||
BootstrapConfigTemplate: cfg.ConfigTemplate(group.BootstrapConfig),
|
BootstrapConfigTemplate: cfg.ConfigTemplate(host.BootstrapConfig),
|
||||||
ConfigTemplate: cfg.ConfigTemplate(group.Config),
|
ConfigTemplate: cfg.ConfigTemplate(host.Config),
|
||||||
StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods),
|
|
||||||
|
|
||||||
clusterConfig: cfg,
|
clusterConfig: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
@ -132,8 +122,6 @@ func (ctx *renderContext) Name() string {
|
|||||||
switch {
|
switch {
|
||||||
case ctx.Host != nil:
|
case ctx.Host != nil:
|
||||||
return "host:" + ctx.Host.Name
|
return "host:" + ctx.Host.Name
|
||||||
case ctx.Group != nil:
|
|
||||||
return "group:" + ctx.Group.Name
|
|
||||||
case ctx.Cluster != nil:
|
case ctx.Cluster != nil:
|
||||||
return "cluster:" + ctx.Cluster.Name
|
return "cluster:" + ctx.Cluster.Name
|
||||||
default:
|
default:
|
||||||
@ -143,14 +131,14 @@ func (ctx *renderContext) Name() string {
|
|||||||
|
|
||||||
func (ctx *renderContext) BootstrapConfig() string {
|
func (ctx *renderContext) BootstrapConfig() string {
|
||||||
if ctx.BootstrapConfigTemplate == nil {
|
if ctx.BootstrapConfigTemplate == nil {
|
||||||
log.Fatalf("no such (bootstrap) config: %q", ctx.Group.BootstrapConfig)
|
log.Fatalf("no such (bootstrap) config: %q", ctx.Host.BootstrapConfig)
|
||||||
}
|
}
|
||||||
return ctx.renderConfig(ctx.BootstrapConfigTemplate)
|
return ctx.renderConfig(ctx.BootstrapConfigTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *renderContext) Config() string {
|
func (ctx *renderContext) Config() string {
|
||||||
if ctx.ConfigTemplate == nil {
|
if ctx.ConfigTemplate == nil {
|
||||||
log.Fatalf("no such config: %q", ctx.Group.Config)
|
log.Fatalf("no such config: %q", ctx.Host.Config)
|
||||||
}
|
}
|
||||||
return ctx.renderConfig(ctx.ConfigTemplate)
|
return ctx.renderConfig(ctx.ConfigTemplate)
|
||||||
}
|
}
|
||||||
@ -166,38 +154,10 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
|||||||
|
|
||||||
ctxMap := ctx.asMap()
|
ctxMap := ctx.asMap()
|
||||||
|
|
||||||
templateFuncs := ctx.templateFuncs(ctxMap)
|
|
||||||
|
|
||||||
render := func(what string, t *clustersconfig.Template) (s string, err error) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
err = t.Execute(ctxName, what, 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(ctxMap)
|
extraFuncs := ctx.templateFuncs(ctxMap)
|
||||||
|
|
||||||
extraFuncs["static_pods"] = func() (string, error) {
|
extraFuncs["static_pods_files"] = func(dir string) (string, error) {
|
||||||
name := ctx.Group.StaticPods
|
namePods := ctx.renderStaticPods()
|
||||||
if len(name) == 0 {
|
|
||||||
return "", fmt.Errorf("group %q has no static pods defined", ctx.Group.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := ctx.clusterConfig.StaticPodsTemplate(name)
|
|
||||||
if t == nil {
|
|
||||||
return "", fmt.Errorf("no static pods template named %q", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return render("static-pods", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraFuncs["bootstrap_pods_files"] = func(dir string) (string, error) {
|
|
||||||
namePods := ctx.renderBootstrapPods()
|
|
||||||
|
|
||||||
defs := make([]config.FileDef, 0)
|
defs := make([]config.FileDef, 0)
|
||||||
|
|
||||||
@ -206,7 +166,7 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
|||||||
|
|
||||||
ba, err := yaml.Marshal(namePod.Pod)
|
ba, err := yaml.Marshal(namePod.Pod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("bootstrap pod %s: failed to render: %v", name, err)
|
return "", fmt.Errorf("static pod %s: failed to render: %v", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defs = append(defs, config.FileDef{
|
defs = append(defs, config.FileDef{
|
||||||
@ -220,33 +180,30 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
|||||||
return string(ba), err
|
return string(ba), err
|
||||||
}
|
}
|
||||||
|
|
||||||
extraFuncs["machine_id"] = func() string {
|
extraFuncs["host_ip"] = func() string {
|
||||||
ba := sha1.Sum([]byte(ctx.Cluster.Name + "/" + ctx.Host.Name)) // TODO: check semantics of machine-id
|
if ctx.Host.Template {
|
||||||
return hex.EncodeToString(ba[:])
|
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 {
|
||||||
|
return "{{ machine_id }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
extraFuncs["version"] = func() string { return Version }
|
||||||
|
|
||||||
if err := configTemplate.Execute(ctxName, "config", buf, ctxMap, extraFuncs); err != nil {
|
if err := configTemplate.Execute(ctxName, "config", buf, ctxMap, extraFuncs); err != nil {
|
||||||
log.Fatalf("failed to render config %q for host %q: %v", ctx.Group.Config, ctx.Host.Name, err)
|
log.Fatalf("failed to render config %q for host %q: %v", ctx.Host.Config, ctx.Host.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *renderContext) StaticPods() (ba []byte, err error) {
|
func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]interface{} {
|
||||||
if ctx.StaticPodsTemplate == nil {
|
|
||||||
log.Fatalf("no such static-pods: %q", ctx.Group.StaticPods)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxMap := ctx.asMap()
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
|
||||||
if err = ctx.StaticPodsTemplate.Execute(ctx.Name(), "static-pods", buf, ctxMap, ctx.templateFuncs(ctxMap)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ba = buf.Bytes()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[string]interface{} {
|
|
||||||
cluster := ctx.Cluster.Name
|
cluster := ctx.Cluster.Name
|
||||||
|
|
||||||
getKeyCert := func(name, funcName string) (s string, err error) {
|
getKeyCert := func(name, funcName string) (s string, err error) {
|
||||||
@ -287,7 +244,25 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
funcs := clusterFuncs(ctx.Cluster)
|
funcs := clusterFuncs(ctx.Cluster)
|
||||||
for k, v := range map[string]interface{}{
|
for k, v := range map[string]any{
|
||||||
|
"default": func(value, defaultValue any) any {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
if v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64:
|
||||||
|
if v != 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
},
|
||||||
"tls_key": func(name string) (string, error) {
|
"tls_key": func(name string) (string, error) {
|
||||||
return getKeyCert(name, "tls_key")
|
return getKeyCert(name, "tls_key")
|
||||||
},
|
},
|
||||||
@ -308,11 +283,11 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
|||||||
return "{{ host_download_token }}"
|
return "{{ host_download_token }}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"hosts_of_group": func() (hosts []interface{}) {
|
"hosts_of_group": func() (hosts []any) {
|
||||||
hosts = make([]interface{}, 0)
|
hosts = make([]any, 0)
|
||||||
|
|
||||||
for _, host := range ctx.clusterConfig.Hosts {
|
for _, host := range ctx.clusterConfig.Hosts {
|
||||||
if host.Group != ctx.Host.Group {
|
if host.Cluster == ctx.Cluster.Name && host.Group != ctx.Host.Group {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,12 +299,31 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
|||||||
|
|
||||||
"hosts_of_group_count": func() (count int) {
|
"hosts_of_group_count": func() (count int) {
|
||||||
for _, host := range ctx.clusterConfig.Hosts {
|
for _, host := range ctx.clusterConfig.Hosts {
|
||||||
if host.Group == ctx.Host.Group {
|
if host.Cluster == ctx.Cluster.Name && host.Group == ctx.Host.Group {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"shuffled_hosts_by_group": func(group string) (hosts []any) {
|
||||||
|
for _, host := range src.Hosts {
|
||||||
|
if host.Cluster == ctx.Cluster.Name && host.Group == group {
|
||||||
|
hosts = append(hosts, asMap(host))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
log.Printf("WARNING: no hosts in group %q", group)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seed := xxhash.Sum64String(ctx.Host.Name)
|
||||||
|
rng := rand.New(rand.NewSource(int64(seed)))
|
||||||
|
rng.Shuffle(len(hosts), func(i, j int) { hosts[i], hosts[j] = hosts[j], hosts[i] })
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
funcs[k] = v
|
funcs[k] = v
|
||||||
}
|
}
|
||||||
|
@ -10,18 +10,18 @@ import (
|
|||||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
func (ctx *renderContext) renderStaticPods() (pods []namePod) {
|
||||||
if ctx.Cluster.BootstrapPods == "" {
|
if ctx.Host.StaticPods == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapPods, ok := src.BootstrapPods[ctx.Cluster.BootstrapPods]
|
staticPods, ok := src.StaticPods[ctx.Host.StaticPods]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Fatalf("no bootstrap pods template named %q", ctx.Cluster.BootstrapPods)
|
log.Fatalf("no static pods template named %q", ctx.Host.StaticPods)
|
||||||
}
|
}
|
||||||
|
|
||||||
// render bootstrap pods
|
// render static pods
|
||||||
parts := bytes.Split(ctx.renderHostTemplates("bootstrap-pods", bootstrapPods), []byte("\n---\n"))
|
parts := bytes.Split(ctx.renderHostTemplates("static-pods", staticPods), []byte("\n---\n"))
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
buf := bytes.NewBuffer(part)
|
buf := bytes.NewBuffer(part)
|
||||||
dec := yaml.NewDecoder(buf)
|
dec := yaml.NewDecoder(buf)
|
||||||
@ -35,7 +35,7 @@ func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatalf("bootstrap pod %d: failed to parse: %v\n%s", n, err, str)
|
log.Fatalf("static pod %d: failed to parse: %v\n%s", n, err, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(podMap) == 0 {
|
if len(podMap) == 0 {
|
||||||
@ -43,7 +43,7 @@ func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if podMap["metadata"] == nil {
|
if podMap["metadata"] == nil {
|
||||||
log.Fatalf("bootstrap pod %d: no metadata\n%s", n, buf.String())
|
log.Fatalf("static pod %d: no metadata\n%s", n, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
md := podMap["metadata"].(map[interface{}]interface{})
|
md := podMap["metadata"].(map[interface{}]interface{})
|
||||||
@ -63,7 +63,7 @@ func (ctx *renderContext) renderHostTemplates(setName string,
|
|||||||
|
|
||||||
log.Print("rendering host templates in ", setName)
|
log.Print("rendering host templates in ", setName)
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := bytes.NewBuffer(make([]byte, 0, 16<<10))
|
||||||
|
|
||||||
for _, t := range templates {
|
for _, t := range templates {
|
||||||
log.Print("- template: ", setName, ": ", t.Name)
|
log.Print("- template: ", setName, ": ", t.Name)
|
||||||
|
59
cmd/dkl-dir2config/search-list.go
Normal file
59
cmd/dkl-dir2config/search-list.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
var searchList = make([]FS, 0)
|
||||||
|
|
||||||
|
// read the first file matching path in the search list
|
||||||
|
func read(path string) (ba []byte, err error) {
|
||||||
|
for _, fs := range searchList {
|
||||||
|
var r io.ReadCloser
|
||||||
|
r, err = fs.Open(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Close()
|
||||||
|
return io.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBase(path string) ([]string, error) {
|
||||||
|
return fsFS{base}.List(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMerged(path string) (entries []string, err error) {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, fs := range searchList {
|
||||||
|
var fsEnts []string
|
||||||
|
fsEnts, err = fs.List(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ent := range fsEnts {
|
||||||
|
if !seen[ent] {
|
||||||
|
entries = append(entries, ent)
|
||||||
|
seen[ent] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(entries)
|
||||||
|
return
|
||||||
|
}
|
@ -1,28 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var adminToken string
|
||||||
hostsToken = flag.String("hosts-token", "", "Token to give to access /hosts (open is none)")
|
|
||||||
adminToken = flag.String("admin-token", "", "Token to give to access to admin actions (open is none)")
|
|
||||||
)
|
|
||||||
|
|
||||||
func authorizeHosts(r *http.Request) bool {
|
|
||||||
return authorizeToken(r, *hostsToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizeAdmin(r *http.Request) bool {
|
func authorizeAdmin(r *http.Request) bool {
|
||||||
return authorizeToken(r, *adminToken)
|
return authorizeToken(r, adminToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorizeToken(r *http.Request, token string) bool {
|
func authorizeToken(r *http.Request, token string) bool {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
// access is open
|
return false
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reqToken := r.Header.Get("Authorization")
|
reqToken := r.Header.Get("Authorization")
|
||||||
@ -34,13 +25,13 @@ func authorizeToken(r *http.Request, token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("denied access to %s from %s", r.RequestURI, r.RemoteAddr)
|
log.Printf("denied access to %s from %s", r.URL.Path, r.RemoteAddr)
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireToken(token string, handler http.Handler) http.Handler {
|
func requireToken(token *string, handler http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
if !authorizeToken(req, token) {
|
if !authorizeToken(req, *token) {
|
||||||
forbidden(w, req)
|
forbidden(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,9 +40,5 @@ func requireToken(token string, handler http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requireAdmin(handler http.Handler) http.Handler {
|
func requireAdmin(handler http.Handler) http.Handler {
|
||||||
return requireToken(*adminToken, handler)
|
return requireToken(&adminToken, handler)
|
||||||
}
|
|
||||||
|
|
||||||
func requireHosts(handler http.Handler) http.Handler {
|
|
||||||
return requireToken(*hostsToken, handler)
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -18,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
||||||
bootImg, err := ioutil.TempFile(os.TempDir(), "boot.img-")
|
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -30,7 +29,7 @@ func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send the result
|
// send the result
|
||||||
bootImg.Seek(0, os.SEEK_SET)
|
bootImg.Seek(0, io.SeekStart)
|
||||||
io.Copy(out, bootImg)
|
io.Copy(out, bootImg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -57,7 +56,55 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var grubSupportVersion = flag.String("grub-support", "1.0.1", "GRUB support version")
|
func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (err error) {
|
||||||
|
imgPath, err := func() (imgPath string, err error) {
|
||||||
|
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rmTempFile(bootImg)
|
||||||
|
|
||||||
|
err = setupBootImage(bootImg, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bootImg.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imgPath = bootImg.Name() + "." + format
|
||||||
|
|
||||||
|
err = run("qemu-img", "convert", "-f", "raw", "-O", format, bootImg.Name(), imgPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer os.Remove(imgPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the result
|
||||||
|
img, err := os.Open(imgPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
io.Copy(out, img)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func qemuImgBootImg(format string) func(out io.Writer, ctx *renderContext) (err error) {
|
||||||
|
return func(out io.Writer, ctx *renderContext) (err error) {
|
||||||
|
return buildBootImgQemuConvert(out, ctx, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version")
|
||||||
|
|
||||||
func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||||
path, err := ctx.distFetch("grub-support", *grubSupportVersion)
|
path, err := ctx.distFetch("grub-support", *grubSupportVersion)
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -13,189 +12,8 @@ import (
|
|||||||
"github.com/cespare/xxhash"
|
"github.com/cespare/xxhash"
|
||||||
)
|
)
|
||||||
|
|
||||||
// deprecated
|
func buildBootISO(out io.Writer, ctx *renderContext) (err error) {
|
||||||
func buildBootISO(out io.Writer, ctx *renderContext) error {
|
tempDir, err := os.MkdirTemp("/tmp", "iso-v2-")
|
||||||
tempDir, err := ioutil.TempDir("/tmp", "iso-")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
cp := func(src, dst string) error {
|
|
||||||
log.Printf("iso: adding %s as %s", src, dst)
|
|
||||||
in, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
outPath := filepath.Join(tempDir, dst)
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.Create(outPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = func() error {
|
|
||||||
// grub
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(tempDir, "grub"), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
|
||||||
search --set=root --file /config.yaml
|
|
||||||
|
|
||||||
insmod all_video
|
|
||||||
set timeout=3
|
|
||||||
|
|
||||||
menuentry "Direktil" {
|
|
||||||
linux /vmlinuz direktil.boot=DEVNAME=sr0 direktil.boot.fs=iso9660 `+ctx.CmdLine+`
|
|
||||||
initrd /initrd
|
|
||||||
}
|
|
||||||
`), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
coreImgPath := filepath.Join(tempDir, "grub", "core.img")
|
|
||||||
grubCfgPath := filepath.Join(tempDir, "grub", "grub.cfg")
|
|
||||||
|
|
||||||
cmd := exec.Command("grub-mkstandalone",
|
|
||||||
"--format=i386-pc",
|
|
||||||
"--output="+coreImgPath,
|
|
||||||
"--install-modules=linux normal iso9660 biosdisk memdisk search tar ls",
|
|
||||||
"--modules=linux normal iso9660 biosdisk search",
|
|
||||||
"--locales=",
|
|
||||||
"--fonts=",
|
|
||||||
"boot/grub/grub.cfg="+grubCfgPath,
|
|
||||||
)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.Remove(coreImgPath)
|
|
||||||
defer os.Remove(grubCfgPath)
|
|
||||||
|
|
||||||
out, err := os.Create(filepath.Join(tempDir, "grub", "bios.img"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := out.Write(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err = ioutil.ReadFile(coreImgPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := out.Write(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// config
|
|
||||||
cfgBytes, cfg, err := ctx.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ioutil.WriteFile(filepath.Join(tempDir, "config.yaml"), cfgBytes, 0600)
|
|
||||||
|
|
||||||
// kernel and initrd
|
|
||||||
type distCopy struct {
|
|
||||||
Src []string
|
|
||||||
Dst string
|
|
||||||
}
|
|
||||||
|
|
||||||
copies := []distCopy{
|
|
||||||
{Src: []string{"kernels", ctx.Host.Kernel}, Dst: "vmlinuz"},
|
|
||||||
{Src: []string{"initrd", ctx.Host.Initrd}, Dst: "initrd"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// layers
|
|
||||||
for _, layer := range cfg.Layers {
|
|
||||||
layerVersion := ctx.Host.Versions[layer]
|
|
||||||
if layerVersion == "" {
|
|
||||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
copies = append(copies,
|
|
||||||
distCopy{
|
|
||||||
Src: []string{"layers", layer, layerVersion},
|
|
||||||
Dst: filepath.Join("current", "layers", layer+".fs"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, copy := range copies {
|
|
||||||
outPath, err := ctx.distFetch(copy.Src...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cp(outPath, copy.Dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the ISO
|
|
||||||
mkisofs, err := exec.LookPath("genisoimage")
|
|
||||||
if err != nil {
|
|
||||||
mkisofs, err = exec.LookPath("mkisofs")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(mkisofs,
|
|
||||||
"-quiet",
|
|
||||||
"-joliet",
|
|
||||||
"-joliet-long",
|
|
||||||
"-rock",
|
|
||||||
"-translation-table",
|
|
||||||
"-no-emul-boot",
|
|
||||||
"-boot-load-size", "4",
|
|
||||||
"-boot-info-table",
|
|
||||||
"-eltorito-boot", "grub/bios.img",
|
|
||||||
"-eltorito-catalog", "grub/boot.cat",
|
|
||||||
tempDir,
|
|
||||||
)
|
|
||||||
cmd.Stdout = out
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBootISOv2(out io.Writer, ctx *renderContext) (err error) {
|
|
||||||
tempDir, err := ioutil.TempDir("/tmp", "iso-v2-")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -251,7 +69,7 @@ func buildBootISOv2(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
f.Write([]byte("direktil marker file\n"))
|
f.Write([]byte("direktil marker file\n"))
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
err = ioutil.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
err = os.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||||
search --set=root --file /`+tag+`
|
search --set=root --file /`+tag+`
|
||||||
|
|
||||||
insmod all_video
|
insmod all_video
|
||||||
@ -294,7 +112,7 @@ menuentry "Direktil" {
|
|||||||
|
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
b, err := ioutil.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
b, err := os.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -303,7 +121,7 @@ menuentry "Direktil" {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err = ioutil.ReadFile(coreImgPath)
|
b, err = os.ReadFile(coreImgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -320,7 +138,7 @@ menuentry "Direktil" {
|
|||||||
|
|
||||||
// kernel and initrd
|
// kernel and initrd
|
||||||
buildRes(fetchKernel, "vmlinuz")
|
buildRes(fetchKernel, "vmlinuz")
|
||||||
buildRes(buildInitrdV2, "initrd")
|
buildRes(buildInitrd, "initrd")
|
||||||
|
|
||||||
// build the ISO
|
// build the ISO
|
||||||
mkisofs, err := exec.LookPath("genisoimage")
|
mkisofs, err := exec.LookPath("genisoimage")
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kernelBytes, err := ioutil.ReadFile(kernelPath)
|
kernelBytes, err := os.ReadFile(kernelPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,7 +48,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
|
|
||||||
// initrd
|
// initrd
|
||||||
initrd := new(bytes.Buffer)
|
initrd := new(bytes.Buffer)
|
||||||
err = buildInitrdV2(initrd, ctx)
|
err = buildInitrd(initrd, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,7 +97,7 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kernelBytes, err := ioutil.ReadFile(kernelPath)
|
kernelBytes, err := os.ReadFile(kernelPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -110,7 +109,7 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
|
|
||||||
// initrd
|
// initrd
|
||||||
initrd := new(bytes.Buffer)
|
initrd := new(bytes.Buffer)
|
||||||
err = buildInitrdV2(initrd, ctx)
|
err = buildInitrd(initrd, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -15,8 +14,6 @@ import (
|
|||||||
"novit.tech/direktil/pkg/cpiocat"
|
"novit.tech/direktil/pkg/cpiocat"
|
||||||
)
|
)
|
||||||
|
|
||||||
var initrdV2 = flag.String("initrd-v2", "2.1.0", "initrd V2 version (temporary flag)") // FIXME
|
|
||||||
|
|
||||||
func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
||||||
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
||||||
|
|
||||||
@ -34,7 +31,7 @@ func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderCo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
||||||
_, cfg, err := ctx.Config()
|
_, cfg, err := ctx.Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -43,7 +40,7 @@ func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
cat := cpiocat.New(out)
|
cat := cpiocat.New(out)
|
||||||
|
|
||||||
// initrd
|
// initrd
|
||||||
initrdPath, err := ctx.distFetch("initrd", *initrdV2)
|
initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -73,7 +70,9 @@ func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
|
|
||||||
// ssh keys
|
// ssh keys
|
||||||
// FIXME we want a bootstrap-stage key instead of the real host key
|
// FIXME we want a bootstrap-stage key instead of the real host key
|
||||||
cat.AppendBytes(cfg.FileContent("/etc/ssh/ssh_host_rsa_key"), "id_rsa", 0600)
|
for _, format := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
|
||||||
|
cat.AppendBytes(cfg.FileContent("/etc/ssh/ssh_host_"+format+"_key"), "id_"+format, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
return cat.Close()
|
return cat.Close()
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||||
"github.com/cloudflare/cfssl/csr"
|
"github.com/cloudflare/cfssl/csr"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"novit.tech/direktil/pkg/bootstrapconfig"
|
||||||
"novit.tech/direktil/pkg/config"
|
"novit.tech/direktil/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,16 +33,50 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
|||||||
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg)
|
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hash := func(plain, seed []byte, hashAlg string) (hashed string, err error) {
|
||||||
|
switch hashAlg {
|
||||||
|
case "sha512crypt":
|
||||||
|
return sha512crypt(plain, seed)
|
||||||
|
|
||||||
|
case "bootstrap":
|
||||||
|
return bootstrapconfig.JoinSeedAndHash(seed, bootstrapconfig.PasswordHashFromSeed(seed, plain)), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown hash alg: %q", hashAlg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"password": func(cluster, name string) (password string, err error) {
|
"quote": strconv.Quote,
|
||||||
password, _, err = clusterPasswords.Get(cluster + "/" + name)
|
|
||||||
|
"password": func(cluster, name, hashAlg string) (password string, err error) {
|
||||||
|
key := cluster + "/" + name
|
||||||
|
|
||||||
|
seed, err := seeds.GetOrCreate(key, func() (seed []byte, err error) {
|
||||||
|
seed = make([]byte, 16)
|
||||||
|
_, err = rand.Read(seed)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get seed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err = clusterPasswords.GetOrCreate(key, func() (password string, err error) {
|
||||||
|
raw := make([]byte, 10)
|
||||||
|
_, err = rand.Read(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password = strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(raw))
|
||||||
|
return
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(password) == 0 {
|
|
||||||
err = fmt.Errorf("password %q not defined for cluster %q", name, cluster)
|
return hash([]byte(password), seed, hashAlg)
|
||||||
}
|
|
||||||
return
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"token": getOrCreateClusterToken,
|
"token": getOrCreateClusterToken,
|
||||||
|
@ -7,8 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = httperr.StdStatus(http.StatusNotFound)
|
ErrNotFound = httperr.NotFound
|
||||||
ErrUnauthorized = httperr.StdStatus(http.StatusUnauthorized)
|
ErrUnauthorized = httperr.StdStatus(http.StatusUnauthorized)
|
||||||
ErrForbidden = httperr.StdStatus(http.StatusForbidden)
|
ErrForbidden = httperr.StdStatus(http.StatusForbidden)
|
||||||
|
ErrInternal = httperr.StdStatus(http.StatusInternalServerError)
|
||||||
ErrInvalidToken = httperr.NewStd(1000, http.StatusForbidden, "invalid token")
|
ErrInvalidToken = httperr.NewStd(1000, http.StatusForbidden, "invalid token")
|
||||||
|
ErrStoreLocked = httperr.NewStd(1001, http.StatusServiceUnavailable, "store is locked")
|
||||||
)
|
)
|
||||||
|
@ -2,13 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
cpio "github.com/cavaliergopher/cpio"
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,84 +24,3 @@ func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, as
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildInitrd(out io.Writer, ctx *renderContext) error {
|
|
||||||
_, cfg, err := ctx.Config()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// send initrd basis
|
|
||||||
initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeFile(out, initrdPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// and our extra archive
|
|
||||||
archive := cpio.NewWriter(out)
|
|
||||||
|
|
||||||
// - required dirs
|
|
||||||
for _, dir := range []string{
|
|
||||||
"boot",
|
|
||||||
"boot/current",
|
|
||||||
"boot/current/layers",
|
|
||||||
} {
|
|
||||||
archive.WriteHeader(&cpio.Header{
|
|
||||||
Name: dir,
|
|
||||||
Mode: cpio.FileMode(0600 | os.ModeDir),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// - the layers
|
|
||||||
for _, layer := range cfg.Layers {
|
|
||||||
layerVersion := ctx.Host.Versions[layer]
|
|
||||||
if layerVersion == "" {
|
|
||||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := ctx.distFetch("layers", layer, layerVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
stat, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
archive.WriteHeader(&cpio.Header{
|
|
||||||
Name: "boot/current/layers/" + layer + ".fs",
|
|
||||||
Mode: 0600,
|
|
||||||
Size: stat.Size(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = writeFile(archive, path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// - the configuration
|
|
||||||
ba, err := yaml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
archive.WriteHeader(&cpio.Header{
|
|
||||||
Name: "boot/config.yaml",
|
|
||||||
Mode: 0600,
|
|
||||||
Size: int64(len(ba)),
|
|
||||||
})
|
|
||||||
|
|
||||||
archive.Write(ba)
|
|
||||||
|
|
||||||
// finalize the archive
|
|
||||||
archive.Flush()
|
|
||||||
archive.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -21,13 +21,15 @@ const (
|
|||||||
etcDir = "/etc/direktil"
|
etcDir = "/etc/direktil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
address = flag.String("address", ":7606", "HTTP listen address")
|
address = flag.String("address", ":7606", "HTTP listen address")
|
||||||
tlsAddress = flag.String("tls-address", "", "HTTPS listen address")
|
tlsAddress = flag.String("tls-address", "", "HTTPS listen address")
|
||||||
certFile = flag.String("tls-cert", etcDir+"/server.crt", "Server TLS certificate")
|
certFile = flag.String("tls-cert", etcDir+"/server.crt", "Server TLS certificate")
|
||||||
keyFile = flag.String("tls-key", etcDir+"/server.key", "Server TLS key")
|
keyFile = flag.String("tls-key", etcDir+"/server.key", "Server TLS key")
|
||||||
|
|
||||||
autoUnlock = flag.String("auto-unlock", "", "Auto-unlock store (testing only!)")
|
autoUnlock = flag.String("auto-unlock", "", "Auto-unlock store (testing only!) env: DLS_AUTO_UNLOCK")
|
||||||
|
|
||||||
casStore cas.Store
|
casStore cas.Store
|
||||||
)
|
)
|
||||||
@ -41,6 +43,9 @@ func main() {
|
|||||||
log.Fatal("no listen address given")
|
log.Fatal("no listen address given")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Print("Direktil local-server version ", Version)
|
||||||
|
wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
|
||||||
|
|
||||||
computeUIHash()
|
computeUIHash()
|
||||||
|
|
||||||
openSecretStore()
|
openSecretStore()
|
||||||
@ -52,12 +57,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
if autoUnlock != "" {
|
if autoUnlock != "" {
|
||||||
log.Printf("auto-unlocking the store")
|
log.Printf("auto-unlocking the store")
|
||||||
err := unlockSecretStore([]byte(autoUnlock))
|
err := unlockSecretStore("test", []byte(autoUnlock))
|
||||||
if err.Any() {
|
if err.Any() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("store auto-unlocked, admin token is ", *adminToken)
|
log.Print("store auto-unlocked")
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("DLS_AUTO_UNLOCK", "")
|
os.Setenv("DLS_AUTO_UNLOCK", "")
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -175,6 +176,17 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
|||||||
funcs := templateFuncs(ctx.SSLConfig)
|
funcs := templateFuncs(ctx.SSLConfig)
|
||||||
|
|
||||||
for name, method := range map[string]any{
|
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) {
|
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = ctx.Host.Name
|
host = ctx.Host.Name
|
||||||
|
@ -59,11 +59,11 @@ func openSecretStore() {
|
|||||||
var (
|
var (
|
||||||
unlockMutex = sync.Mutex{}
|
unlockMutex = sync.Mutex{}
|
||||||
|
|
||||||
ErrStoreAlreadyUnlocked = httperr.NewStd(http.StatusConflict, 1, "store already unlocked")
|
ErrStoreAlreadyUnlocked = httperr.NewStd(1, http.StatusConflict, "store already unlocked")
|
||||||
ErrInvalidPassphrase = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase")
|
ErrInvalidPassphrase = httperr.NewStd(2, http.StatusBadRequest, "invalid passphrase")
|
||||||
)
|
)
|
||||||
|
|
||||||
func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
func unlockSecretStore(name string, passphrase []byte) (err httperr.Error) {
|
||||||
unlockMutex.Lock()
|
unlockMutex.Lock()
|
||||||
defer unlockMutex.Unlock()
|
defer unlockMutex.Unlock()
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if secStore.IsNew() {
|
if secStore.IsNew() {
|
||||||
err := secStore.Init(passphrase)
|
err := secStore.Init(name, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperr.Internal(err)
|
return httperr.Internal(err)
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
|||||||
log.Print("wrote new admin token")
|
log.Print("wrote new admin token")
|
||||||
}
|
}
|
||||||
|
|
||||||
*adminToken = token
|
adminToken = token
|
||||||
|
|
||||||
{
|
{
|
||||||
token, err := newToken(16)
|
token, err := newToken(16)
|
||||||
@ -247,6 +247,31 @@ func (s KVSecrets[T]) Data() (kvs map[string]T, err error) {
|
|||||||
return
|
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) {
|
func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
|
||||||
kvs, err := s.Data()
|
kvs, err := s.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -291,6 +316,38 @@ func (s KVSecrets[T]) Put(key string, v T) (err error) {
|
|||||||
return
|
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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
v, err = create()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Put(key, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s KVSecrets[T]) WsList(resp *restful.Response, prefix string) {
|
func (s KVSecrets[T]) WsList(resp *restful.Response, prefix string) {
|
||||||
keys, err := s.Keys(prefix)
|
keys, err := s.Keys(prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -330,3 +387,11 @@ func (s KVSecrets[T]) WsPut(req *restful.Request, resp *restful.Response, key st
|
|||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@ -43,7 +42,7 @@ func loadSecretData(config *config.Config) (sd *SecretData, err error) {
|
|||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
ba, err := ioutil.ReadFile(secretDataPath())
|
ba, err := os.ReadFile(secretDataPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
err = nil
|
err = nil
|
||||||
|
39
cmd/dkl-local-server/sha512crypt.go
Normal file
39
cmd/dkl-local-server/sha512crypt.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
crypthash "github.com/sergeymakinen/go-crypt/hash"
|
||||||
|
"github.com/sergeymakinen/go-crypt/sha512"
|
||||||
|
)
|
||||||
|
|
||||||
|
// for some reason, no implementation of crypt's sha512 is clean enough :(
|
||||||
|
|
||||||
|
func sha512crypt(password, seed []byte) (string, error) {
|
||||||
|
// loose salt entropy because of character restriction in the salt
|
||||||
|
salt := []byte(base64.RawStdEncoding.EncodeToString(seed))[:sha512.MaxSaltLength]
|
||||||
|
// - base64 allows '+' where the salt accepts '.'
|
||||||
|
for i, c := range salt {
|
||||||
|
if c == '+' {
|
||||||
|
salt[i] = '.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := struct {
|
||||||
|
HashPrefix string
|
||||||
|
Rounds uint32 `hash:"param:rounds,omitempty"`
|
||||||
|
Salt []byte
|
||||||
|
Sum [86]byte
|
||||||
|
}{
|
||||||
|
HashPrefix: sha512.Prefix,
|
||||||
|
Rounds: sha512.DefaultRounds,
|
||||||
|
Salt: salt,
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := sha512.Key([]byte(password), scheme.Salt, scheme.Rounds)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
crypthash.LittleEndianEncoding.Encode(scheme.Sum[:], key)
|
||||||
|
return crypthash.Marshal(scheme)
|
||||||
|
}
|
@ -10,7 +10,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
@ -42,7 +41,7 @@ genLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = func() (err error) {
|
err = func() (err error) {
|
||||||
outFile, err := ioutil.TempFile("/tmp", "dls-key.")
|
outFile, err := os.CreateTemp("/tmp", "dls-key.")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -70,12 +69,12 @@ genLoop:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
privKey, err = ioutil.ReadFile(outPath)
|
privKey, err = os.ReadFile(outPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey, err = ioutil.ReadFile(outPath + ".pub")
|
pubKey, err = os.ReadFile(outPath + ".pub")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PublicState struct {
|
type PublicState struct {
|
||||||
UIHash string
|
ServerVersion string
|
||||||
Store struct {
|
UIHash string
|
||||||
|
Store struct {
|
||||||
New bool
|
New bool
|
||||||
Open bool
|
Open bool
|
||||||
}
|
}
|
||||||
@ -22,6 +23,7 @@ type State struct {
|
|||||||
|
|
||||||
Store struct {
|
Store struct {
|
||||||
DownloadToken string
|
DownloadToken string
|
||||||
|
KeyNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
Clusters []ClusterState
|
Clusters []ClusterState
|
||||||
@ -29,6 +31,8 @@ type State struct {
|
|||||||
Config *localconfig.Config
|
Config *localconfig.Config
|
||||||
|
|
||||||
Downloads map[string]DownloadSpec
|
Downloads map[string]DownloadSpec
|
||||||
|
|
||||||
|
HostTemplates []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClusterState struct {
|
type ClusterState struct {
|
||||||
@ -43,6 +47,8 @@ type HostState struct {
|
|||||||
Name string
|
Name string
|
||||||
Cluster string
|
Cluster string
|
||||||
IPs []string
|
IPs []string
|
||||||
|
|
||||||
|
Template string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CAState struct {
|
type CAState struct {
|
||||||
@ -59,14 +65,21 @@ func init() {
|
|||||||
func updateState() {
|
func updateState() {
|
||||||
log.Print("updating state")
|
log.Print("updating state")
|
||||||
|
|
||||||
|
// store key names
|
||||||
|
keyNames := make([]string, 0, len(secStore.Keys))
|
||||||
|
for _, key := range secStore.Keys {
|
||||||
|
keyNames = append(keyNames, key.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// config
|
||||||
cfg, err := readConfig()
|
cfg, err := readConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if secStore.IsNew() || !secStore.Unlocked() {
|
if secStore.IsNew() || !secStore.Unlocked() {
|
||||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,22 +121,44 @@ func updateState() {
|
|||||||
clusters = append(clusters, c)
|
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 {
|
for _, host := range cfg.Hosts {
|
||||||
h := HostState{
|
h := HostState{
|
||||||
Name: host.Name,
|
Name: host.Name,
|
||||||
Cluster: host.ClusterName,
|
Cluster: host.ClusterName,
|
||||||
IPs: host.IPs,
|
IPs: host.IPs,
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts = append(hosts, h)
|
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, len(cfg.HostTemplates))
|
||||||
|
for i, ht := range cfg.HostTemplates {
|
||||||
|
hostTemplates[i] = ht.Name
|
||||||
|
}
|
||||||
|
|
||||||
// done
|
// done
|
||||||
wState.Change(func(v *State) {
|
wState.Change(func(v *State) {
|
||||||
v.HasConfig = true
|
v.HasConfig = true
|
||||||
//v.Config = cfg
|
v.Store.KeyNames = keyNames
|
||||||
v.Clusters = clusters
|
v.Clusters = clusters
|
||||||
v.Hosts = hosts
|
v.Hosts = hosts
|
||||||
|
v.HostTemplates = hostTemplates
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -129,7 +131,26 @@ func getUsableKeyCert(cluster, caName, name, profile, label string, req *csr.Cer
|
|||||||
|
|
||||||
if found {
|
if found {
|
||||||
if rh == kc.ReqHash {
|
if rh == kc.ReqHash {
|
||||||
err = checkCertUsable(kc.Cert)
|
err = func() (err error) {
|
||||||
|
err = checkCertUsable(kc.Cert)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if !pool.AppendCertsFromPEM(ca.Cert) {
|
||||||
|
panic("unexpected invalid CA certificate at this point")
|
||||||
|
}
|
||||||
|
|
||||||
|
certBlock, _ := pem.Decode(kc.Cert)
|
||||||
|
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cert.Verify(x509.VerifyOptions{Roots: pool})
|
||||||
|
return
|
||||||
|
}()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return // all good, no need to create or renew
|
return // all good, no need to create or renew
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func adminAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
func adminAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||||
tokenAuth(req, resp, chain, *adminToken)
|
tokenAuth(req, resp, chain, adminToken)
|
||||||
}
|
|
||||||
|
|
||||||
func hostsAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
|
||||||
tokenAuth(req, resp, chain, *hostsToken, *adminToken)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain, allowedTokens ...string) {
|
func tokenAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain, allowedTokens ...string) {
|
||||||
token := getToken(req)
|
token := getToken(req)
|
||||||
|
|
||||||
for _, allowedToken := range allowedTokens {
|
for _, allowedToken := range allowedTokens {
|
||||||
if allowedToken == "" || token == allowedToken {
|
if allowedToken != "" && token == allowedToken {
|
||||||
chain.ProcessFilter(req, resp)
|
chain.ProcessFilter(req, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/helpers"
|
||||||
"github.com/cloudflare/cfssl/log"
|
"github.com/cloudflare/cfssl/log"
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
)
|
)
|
||||||
@ -55,11 +57,22 @@ func getUsableClusterCA(cluster, name string) (ca CA, err error) {
|
|||||||
if checkErr != nil {
|
if checkErr != nil {
|
||||||
log.Infof("cluster %s: CA %s: regenerating certificate: %v", cluster, name, checkErr)
|
log.Infof("cluster %s: CA %s: regenerating certificate: %v", cluster, name, checkErr)
|
||||||
|
|
||||||
|
prevCerts, _ := helpers.ParseCertificatesPEM(ca.Cert)
|
||||||
|
|
||||||
err = ca.RenewCert()
|
err = ca.RenewCert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("renew: %w", err)
|
err = fmt.Errorf("renew: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, cert := range prevCerts {
|
||||||
|
if cert.NotAfter.After(now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certPEM := helpers.EncodeCertificatePEM(cert)
|
||||||
|
ca.Cert = append(ca.Cert, certPEM...)
|
||||||
|
}
|
||||||
|
|
||||||
err = clusterCAs.Put(key, ca)
|
err = clusterCAs.Put(key, ca)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var seeds = newClusterSecretKV[[]byte]("seeds")
|
||||||
|
|
||||||
var clusterPasswords = newClusterSecretKV[string]("passwords")
|
var clusterPasswords = newClusterSecretKV[string]("passwords")
|
||||||
|
|
||||||
func wsClusterPasswords(req *restful.Request, resp *restful.Response) {
|
func wsClusterPasswords(req *restful.Request, resp *restful.Response) {
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeNewConfig(reader io.Reader) (err error) {
|
func writeNewConfig(reader io.Reader) (err error) {
|
||||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "host":
|
case "host":
|
||||||
host := cfg.Host(spec.Name)
|
host := hostOrTemplate(cfg, spec.Name)
|
||||||
if host == nil {
|
if host == nil {
|
||||||
wsNotFound(resp)
|
wsNotFound(resp)
|
||||||
return
|
return
|
||||||
|
@ -13,21 +13,23 @@ import (
|
|||||||
"novit.tech/direktil/local-server/pkg/mime"
|
"novit.tech/direktil/local-server/pkg/mime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var trustXFF = flag.Bool("trust-xff", true, "Trust the X-Forwarded-For header")
|
var (
|
||||||
|
allowDetectedHost = flag.Bool("allow-detected-host", false, "Allow access to host assets from its IP (insecure but enables unattended netboot)")
|
||||||
|
trustXFF = flag.Bool("trust-xff", false, "Trust the X-Forwarded-For header")
|
||||||
|
)
|
||||||
|
|
||||||
type wsHost struct {
|
type wsHost struct {
|
||||||
prefix string
|
|
||||||
hostDoc string
|
hostDoc string
|
||||||
getHost func(req *restful.Request) (hostName string, err error)
|
getHost func(req *restful.Request) (hostName string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
|
func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
|
||||||
b := func(what string) *restful.RouteBuilder {
|
b := func(what string) *restful.RouteBuilder {
|
||||||
return rws.GET(ws.prefix + "/" + what).To(ws.render)
|
return rws.GET("/" + what).To(ws.render)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rb := range []*restful.RouteBuilder{
|
for _, rb := range []*restful.RouteBuilder{
|
||||||
rws.GET(ws.prefix).To(ws.get).
|
rws.GET("").To(ws.get).
|
||||||
Doc("Get the "+ws.hostDoc+"'s details").
|
Doc("Get the "+ws.hostDoc+"'s details").
|
||||||
Returns(200, "OK", localconfig.Host{}),
|
Returns(200, "OK", localconfig.Host{}),
|
||||||
|
|
||||||
@ -44,13 +46,30 @@ func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteB
|
|||||||
Produces(mime.DISK).
|
Produces(mime.DISK).
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image"),
|
||||||
|
|
||||||
|
// - raw + compressed
|
||||||
b("boot.img.gz").
|
b("boot.img.gz").
|
||||||
Produces(mime.DISK + "+gzip").
|
Produces(mime.DISK + "+gzip").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image (gzip compressed)"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, gzip compressed"),
|
||||||
|
|
||||||
b("boot.img.lz4").
|
b("boot.img.lz4").
|
||||||
Produces(mime.DISK + "+lz4").
|
Produces(mime.DISK + "+lz4").
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot disk image (lz4 compressed)"),
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, lz4 compressed"),
|
||||||
|
|
||||||
|
// - other formats
|
||||||
|
b("boot.qcow2").
|
||||||
|
Produces(mime.DISK + "+qcow2").
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, QCOW2 (KVM, Xen)"),
|
||||||
|
b("boot.qed").
|
||||||
|
Produces(mime.DISK + "+qed").
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, QED (KVM)"),
|
||||||
|
b("boot.vmdk").
|
||||||
|
Produces(mime.DISK + "+vdi").
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, VDI (VirtualBox)"),
|
||||||
|
b("boot.qcow2").
|
||||||
|
Produces(mime.DISK + "+vpc").
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, VHD (Hyper-V)"),
|
||||||
|
b("boot.vmdk").
|
||||||
|
Produces(mime.DISK + "+vmdk").
|
||||||
|
Doc("Get the " + ws.hostDoc + "'s boot disk image, VMDK (VMware)"),
|
||||||
|
|
||||||
// metal/local HDD upgrades
|
// metal/local HDD upgrades
|
||||||
b("boot.tar").
|
b("boot.tar").
|
||||||
@ -71,40 +90,31 @@ func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteB
|
|||||||
Produces(mime.IPXE).
|
Produces(mime.IPXE).
|
||||||
Doc("Get the " + ws.hostDoc + "'s IPXE code (for netboot)"),
|
Doc("Get the " + ws.hostDoc + "'s IPXE code (for netboot)"),
|
||||||
|
|
||||||
|
// boot support
|
||||||
b("kernel").
|
b("kernel").
|
||||||
Produces(mime.OCTET).
|
Produces(mime.OCTET).
|
||||||
Doc("Get the " + ws.hostDoc + "'s kernel (ie: for netboot)"),
|
Doc("Get the " + ws.hostDoc + "'s kernel (ie: for netboot)"),
|
||||||
|
|
||||||
b("initrd").
|
b("initrd").
|
||||||
Produces(mime.OCTET).
|
Produces(mime.OCTET).
|
||||||
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
|
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
|
||||||
|
|
||||||
// boot v2
|
|
||||||
// - bootstrap config
|
// - bootstrap config
|
||||||
b("bootstrap-config").
|
b("bootstrap-config").
|
||||||
Produces(mime.YAML).
|
Produces(mime.YAML).
|
||||||
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration"),
|
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration"),
|
||||||
b("bootstrap-config.json").
|
b("bootstrap-config.json").
|
||||||
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration (as JSON)"),
|
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration (as JSON)"),
|
||||||
// - initrd
|
|
||||||
b("initrd-v2").
|
|
||||||
Produces(mime.OCTET).
|
|
||||||
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (v2)"),
|
|
||||||
// - bootstrap
|
// - bootstrap
|
||||||
b("bootstrap.tar").
|
b("bootstrap.tar").
|
||||||
Produces(mime.TAR).
|
Produces(mime.TAR).
|
||||||
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
||||||
b("boot-v2.iso").
|
|
||||||
Produces(mime.ISO).
|
|
||||||
Param(cmdlineParam).
|
|
||||||
Doc("Get the " + ws.hostDoc + "'s boot CD-ROM image (v2)"),
|
|
||||||
} {
|
} {
|
||||||
alterRB(rb)
|
alterRB(rb)
|
||||||
rws.Route(rb)
|
rws.Route(rb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
|
func (ws wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
|
||||||
hostname, err := ws.getHost(req)
|
hostname, err := ws.getHost(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
@ -121,16 +131,16 @@ func (ws *wsHost) host(req *restful.Request, resp *restful.Response) (host *loca
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
host = cfg.Host(hostname)
|
host = hostOrTemplate(cfg, hostname)
|
||||||
if host == nil {
|
if host == nil {
|
||||||
log.Print("no host named ", hostname)
|
|
||||||
wsNotFound(resp)
|
wsNotFound(resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *wsHost) get(req *restful.Request, resp *restful.Response) {
|
func (ws wsHost) get(req *restful.Request, resp *restful.Response) {
|
||||||
host, _ := ws.host(req, resp)
|
host, _ := ws.host(req, resp)
|
||||||
if host == nil {
|
if host == nil {
|
||||||
return
|
return
|
||||||
@ -139,7 +149,7 @@ func (ws *wsHost) get(req *restful.Request, resp *restful.Response) {
|
|||||||
resp.WriteEntity(host)
|
resp.WriteEntity(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *wsHost) render(req *restful.Request, resp *restful.Response) {
|
func (ws wsHost) render(req *restful.Request, resp *restful.Response) {
|
||||||
host, cfg := ws.host(req, resp)
|
host, cfg := ws.host(req, resp)
|
||||||
if host == nil {
|
if host == nil {
|
||||||
return
|
return
|
||||||
@ -171,12 +181,17 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
|||||||
case "kernel":
|
case "kernel":
|
||||||
err = renderKernel(w, r, ctx)
|
err = renderKernel(w, r, ctx)
|
||||||
|
|
||||||
|
// boot v2
|
||||||
|
case "bootstrap-config":
|
||||||
|
err = renderBootstrapConfig(w, r, ctx, false)
|
||||||
|
case "bootstrap-config.json":
|
||||||
|
err = renderBootstrapConfig(w, r, ctx, true)
|
||||||
case "initrd":
|
case "initrd":
|
||||||
err = renderCtx(w, r, ctx, what, buildInitrd)
|
err = renderCtx(w, r, ctx, what, buildInitrd)
|
||||||
|
case "bootstrap.tar":
|
||||||
|
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
||||||
case "boot.iso":
|
case "boot.iso":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootISO)
|
err = renderCtx(w, r, ctx, what, buildBootISO)
|
||||||
|
|
||||||
case "boot.tar":
|
case "boot.tar":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootTar)
|
err = renderCtx(w, r, ctx, what, buildBootTar)
|
||||||
case "boot-efi.tar":
|
case "boot-efi.tar":
|
||||||
@ -184,24 +199,20 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
|||||||
|
|
||||||
case "boot.img":
|
case "boot.img":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImg)
|
err = renderCtx(w, r, ctx, what, buildBootImg)
|
||||||
|
|
||||||
case "boot.img.gz":
|
case "boot.img.gz":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImgGZ)
|
err = renderCtx(w, r, ctx, what, buildBootImgGZ)
|
||||||
|
|
||||||
case "boot.img.lz4":
|
case "boot.img.lz4":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
|
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
|
||||||
|
case "boot.qcow2":
|
||||||
// boot v2
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2"))
|
||||||
case "bootstrap-config":
|
case "boot.qed":
|
||||||
err = renderBootstrapConfig(w, r, ctx, false)
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed"))
|
||||||
case "bootstrap-config.json":
|
case "boot.vdi":
|
||||||
err = renderBootstrapConfig(w, r, ctx, true)
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi"))
|
||||||
case "initrd-v2":
|
case "boot.vmdk":
|
||||||
err = renderCtx(w, r, ctx, what, buildInitrdV2)
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk"))
|
||||||
case "bootstrap.tar":
|
case "boot.vpc":
|
||||||
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc"))
|
||||||
case "boot-v2.iso":
|
|
||||||
err = renderCtx(w, r, ctx, what, buildBootISOv2)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
117
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
117
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"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 wsHostsFromTemplateList(req *restful.Request, resp *restful.Response) {
|
||||||
|
hostsFromTemplate.WsList(resp, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if _, err := netip.ParseAddr(v.IP); err != nil {
|
||||||
|
wsBadRequest(resp, "bad IP: "+err.Error())
|
||||||
|
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()
|
||||||
|
}
|
@ -5,26 +5,58 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
"m.cluseau.fr/go/httperr"
|
||||||
|
"novit.tech/direktil/local-server/secretstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NamedPassphrase struct {
|
||||||
|
Name string
|
||||||
|
Passphrase []byte
|
||||||
|
}
|
||||||
|
|
||||||
func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
||||||
var passphrase string
|
np := NamedPassphrase{}
|
||||||
err := req.ReadEntity(&passphrase)
|
err := req.ReadEntity(&np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.WriteError(http.StatusBadRequest, err)
|
resp.WriteError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := unlockSecretStore([]byte(passphrase)); err.Any() {
|
defer secretstore.Memzero(np.Passphrase)
|
||||||
|
|
||||||
|
if secStore.IsNew() {
|
||||||
|
if len(np.Name) == 0 {
|
||||||
|
wsBadRequest(resp, "no name given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(np.Passphrase) == 0 {
|
||||||
|
wsBadRequest(resp, "no passphrase given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if secStore.Unlocked() {
|
||||||
|
if secStore.HasKey(np.Passphrase) {
|
||||||
|
resp.WriteEntity(adminToken)
|
||||||
|
} else {
|
||||||
|
wsError(resp, ErrUnauthorized)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unlockSecretStore(np.Name, np.Passphrase); err.Any() {
|
||||||
err.WriteJSON(resp.ResponseWriter)
|
err.WriteJSON(resp.ResponseWriter)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.WriteEntity(*adminToken)
|
resp.WriteEntity(adminToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
||||||
@ -96,3 +128,68 @@ func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
|||||||
|
|
||||||
buf.WriteTo(resp)
|
buf.WriteTo(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wsStoreUpload(req *restful.Request, resp *restful.Response) {
|
||||||
|
if !secStore.IsNew() {
|
||||||
|
wsError(resp, httperr.BadRequest("store is not new"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
_, err := io.Copy(buf, req.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
arch := tar.NewReader(buf)
|
||||||
|
|
||||||
|
root := secStoreRoot()
|
||||||
|
|
||||||
|
for {
|
||||||
|
hdr, err := arch.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(hdr.Name)
|
||||||
|
|
||||||
|
fullPath := filepath.Join(root, hdr.Name)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case hdr.FileInfo().IsDir():
|
||||||
|
err = os.MkdirAll(fullPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
content, err := io.ReadAll(io.LimitReader(arch, hdr.Size))
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fullPath, content, 0600)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openSecretStore()
|
||||||
|
|
||||||
|
resp.WriteEntity(map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
@ -1,24 +1,77 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
"novit.tech/direktil/local-server/secretstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
||||||
var passphrase string
|
np := NamedPassphrase{}
|
||||||
|
|
||||||
err := req.ReadEntity(&passphrase)
|
err := req.ReadEntity(&np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsBadRequest(resp, err.Error())
|
wsBadRequest(resp, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(passphrase) == 0 {
|
np.Name = strings.TrimSpace(np.Name)
|
||||||
|
|
||||||
|
if len(np.Name) == 0 {
|
||||||
|
wsBadRequest(resp, "no name given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(np.Passphrase) == 0 {
|
||||||
wsBadRequest(resp, "no passphrase given")
|
wsBadRequest(resp, "no passphrase given")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secStore.AddKey([]byte(passphrase))
|
for _, k := range secStore.Keys {
|
||||||
|
if k.Name == np.Name {
|
||||||
|
wsBadRequest(resp, "there's already a passphrase named "+strconv.Quote(np.Name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secStore.AddKey(np.Name, np.Passphrase)
|
||||||
|
defer updateState()
|
||||||
|
|
||||||
|
err = secStore.SaveTo(secKeysStorePath())
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsStoreDelKey(req *restful.Request, resp *restful.Response) {
|
||||||
|
name := ""
|
||||||
|
|
||||||
|
err := req.ReadEntity(&name)
|
||||||
|
if err != nil {
|
||||||
|
wsBadRequest(resp, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys := make([]secretstore.KeyEntry, 0, len(secStore.Keys))
|
||||||
|
for _, k := range secStore.Keys {
|
||||||
|
if k.Name == name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newKeys = append(newKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newKeys) == 0 {
|
||||||
|
wsBadRequest(resp, "can't remove the last key from the store")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secStore.Keys = newKeys
|
||||||
|
defer updateState()
|
||||||
|
|
||||||
err = secStore.SaveTo(secKeysStorePath())
|
err = secStore.SaveTo(secKeysStorePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
|
@ -27,13 +27,16 @@ func registerWS(rest *restful.Container) {
|
|||||||
Produces(mime.JSON).
|
Produces(mime.JSON).
|
||||||
Consumes(mime.JSON).
|
Consumes(mime.JSON).
|
||||||
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
||||||
Reads("").
|
Reads(NamedPassphrase{}).
|
||||||
Writes("").
|
Writes("").
|
||||||
Doc("Try to unlock the store")).
|
Doc("Try to unlock the store")).
|
||||||
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
||||||
Produces(mime.TAR).
|
Produces(mime.TAR).
|
||||||
Param(ws.QueryParameter("token", "the download token")).
|
Param(ws.QueryParameter("token", "the download token")).
|
||||||
Doc("Fetch the encrypted store")).
|
Doc("Fetch the encrypted store")).
|
||||||
|
Route(ws.POST("/store.tar").To(wsStoreUpload).
|
||||||
|
Consumes(mime.TAR).
|
||||||
|
Doc("Upload an existing store")).
|
||||||
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
|
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
|
||||||
Param(ws.PathParameter("token", "the download token")).
|
Param(ws.PathParameter("token", "the download token")).
|
||||||
Param(ws.PathParameter("asset", "the requested asset")).
|
Param(ws.PathParameter("asset", "the requested asset")).
|
||||||
@ -43,16 +46,19 @@ func registerWS(rest *restful.Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Admin-level APIs
|
// Admin-level APIs
|
||||||
ws := &restful.WebService{}
|
ws := (&restful.WebService{}).
|
||||||
ws.
|
Filter(requireSecStore).
|
||||||
Filter(adminAuth).
|
Filter(adminAuth).
|
||||||
Param(ws.HeaderParameter("Authorization", "Admin bearer token").Required(true)).
|
Param(restful.HeaderParameter("Authorization", "Admin bearer token").Required(true)).
|
||||||
Produces(mime.JSON)
|
Produces(mime.JSON)
|
||||||
|
|
||||||
// - store management
|
// - store management
|
||||||
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
||||||
Consumes(mime.JSON).Reads("").
|
Consumes(mime.JSON).Reads(NamedPassphrase{}).
|
||||||
Doc("Add an unlock key to the store"))
|
Doc("Add an unlock key to the store"))
|
||||||
|
ws.Route(ws.POST("/store/delete-key").To(wsStoreDelKey).
|
||||||
|
Consumes(mime.JSON).Reads("").
|
||||||
|
Doc("Remove an unlock key to the store (by its name)"))
|
||||||
|
|
||||||
// - downloads
|
// - downloads
|
||||||
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
||||||
@ -70,6 +76,15 @@ func registerWS(rest *restful.Container) {
|
|||||||
ws.Route(ws.GET("/clusters").To(wsListClusters).
|
ws.Route(ws.GET("/clusters").To(wsListClusters).
|
||||||
Doc("List clusters"))
|
Doc("List clusters"))
|
||||||
|
|
||||||
|
ws.Route(ws.GET("/hosts-from-template").To(wsHostsFromTemplateList).
|
||||||
|
Doc("List host template instances"))
|
||||||
|
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 (
|
const (
|
||||||
GET = http.MethodGet
|
GET = http.MethodGet
|
||||||
PUT = http.MethodPut
|
PUT = http.MethodPut
|
||||||
@ -118,8 +133,20 @@ func registerWS(rest *restful.Container) {
|
|||||||
ws.Route(ws.GET("/hosts").To(wsListHosts).
|
ws.Route(ws.GET("/hosts").To(wsListHosts).
|
||||||
Doc("List hosts"))
|
Doc("List hosts"))
|
||||||
|
|
||||||
|
ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List))
|
||||||
|
ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get))
|
||||||
|
ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set))
|
||||||
|
|
||||||
|
rest.Add(ws)
|
||||||
|
|
||||||
|
// Hosts API
|
||||||
|
ws = (&restful.WebService{}).
|
||||||
|
Filter(requireSecStore).
|
||||||
|
Filter(adminAuth).
|
||||||
|
Path("/hosts/{host-name}").
|
||||||
|
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||||
|
|
||||||
(&wsHost{
|
(&wsHost{
|
||||||
prefix: "/hosts/{host-name}",
|
|
||||||
hostDoc: "given host",
|
hostDoc: "given host",
|
||||||
getHost: func(req *restful.Request) (string, error) {
|
getHost: func(req *restful.Request) (string, error) {
|
||||||
return req.PathParameter("host-name"), nil
|
return req.PathParameter("host-name"), nil
|
||||||
@ -128,17 +155,12 @@ func registerWS(rest *restful.Container) {
|
|||||||
rb.Param(ws.PathParameter("host-name", "host's name"))
|
rb.Param(ws.PathParameter("host-name", "host's name"))
|
||||||
})
|
})
|
||||||
|
|
||||||
ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List))
|
|
||||||
ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get))
|
|
||||||
ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set))
|
|
||||||
|
|
||||||
rest.Add(ws)
|
rest.Add(ws)
|
||||||
|
|
||||||
// Hosts API
|
// Detected host API
|
||||||
ws = &restful.WebService{}
|
ws = (&restful.WebService{}).
|
||||||
ws.Produces(mime.JSON).
|
Filter(requireSecStore).
|
||||||
Path("/me").
|
Path("/me").
|
||||||
Filter(hostsAuth).
|
|
||||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||||
|
|
||||||
(&wsHost{
|
(&wsHost{
|
||||||
@ -149,8 +171,10 @@ func registerWS(rest *restful.Container) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Hosts by token API
|
// Hosts by token API
|
||||||
ws = &restful.WebService{}
|
ws = (&restful.WebService{}).
|
||||||
ws.Path("/hosts-by-token/{host-token}").Param(ws.PathParameter("host-token", "host's download token"))
|
Filter(requireSecStore).
|
||||||
|
Path("/hosts-by-token/{host-token}").
|
||||||
|
Param(ws.PathParameter("host-token", "host's download token"))
|
||||||
|
|
||||||
(&wsHost{
|
(&wsHost{
|
||||||
hostDoc: "token's host",
|
hostDoc: "token's host",
|
||||||
@ -178,7 +202,19 @@ func registerWS(rest *restful.Container) {
|
|||||||
rest.Add(ws)
|
rest.Add(ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||||
|
if !secStore.Unlocked() {
|
||||||
|
wsError(resp, ErrStoreLocked)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
func detectHost(req *restful.Request) (hostName string, err error) {
|
func detectHost(req *restful.Request) (hostName string, err error) {
|
||||||
|
if !*allowDetectedHost {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r := req.Request
|
r := req.Request
|
||||||
remoteAddr := r.RemoteAddr
|
remoteAddr := r.RemoteAddr
|
||||||
|
|
||||||
|
70
go.mod
70
go.mod
@ -1,43 +1,51 @@
|
|||||||
module novit.tech/direktil/local-server
|
module novit.tech/direktil/local-server
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cavaliergopher/cpio v1.0.1
|
|
||||||
github.com/cespare/xxhash v1.1.0
|
github.com/cespare/xxhash v1.1.0
|
||||||
github.com/cloudflare/cfssl v1.6.3
|
github.com/cloudflare/cfssl v1.6.5
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/emicklei/go-restful v2.16.0+incompatible
|
github.com/emicklei/go-restful v2.16.0+incompatible
|
||||||
github.com/emicklei/go-restful-openapi v1.4.1
|
github.com/emicklei/go-restful-openapi v1.4.1
|
||||||
|
github.com/go-git/go-git/v5 v5.12.0
|
||||||
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34
|
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34
|
||||||
github.com/miolini/datacounter v1.0.3
|
github.com/miolini/datacounter v1.0.3
|
||||||
github.com/oklog/ulid v1.3.1
|
github.com/oklog/ulid v1.3.1
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible
|
github.com/pierrec/lz4 v2.6.1+incompatible
|
||||||
golang.org/x/crypto v0.7.0
|
github.com/sergeymakinen/go-crypt v1.0.0
|
||||||
|
golang.org/x/crypto v0.22.0
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2
|
gopkg.in/src-d/go-billy.v4 v4.3.2
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
k8s.io/apimachinery v0.26.2
|
k8s.io/apimachinery v0.29.3
|
||||||
m.cluseau.fr/go v0.0.0-20230213160503-2365f4cbf1d4
|
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
|
||||||
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e
|
novit.tech/direktil/pkg v0.0.0-20240415130406-0d2e181a4ed6
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
|
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/frankban/quicktest v1.5.0 // indirect
|
github.com/frankban/quicktest v1.5.0 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
github.com/go-openapi/spec v0.20.8 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.22.3 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/gobuffalo/envy v1.10.2 // indirect
|
github.com/gobuffalo/envy v1.10.2 // indirect
|
||||||
github.com/gobuffalo/packd v1.0.2 // indirect
|
github.com/gobuffalo/packd v1.0.2 // indirect
|
||||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/certificate-transparency-go v1.1.8 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
@ -45,27 +53,29 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 // indirect
|
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 // indirect
|
||||||
github.com/lib/pq v1.10.7 // indirect
|
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||||
github.com/src-d/gcfg v1.4.0 // indirect
|
github.com/src-d/gcfg v1.4.0 // indirect
|
||||||
github.com/weppos/publicsuffix-go v0.30.0 // indirect
|
github.com/weppos/publicsuffix-go v0.30.2 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/zmap/zcrypto v0.0.0-20230205235340-d51ce4775101 // indirect
|
github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c // indirect
|
||||||
github.com/zmap/zlint/v3 v3.1.0 // indirect
|
github.com/zmap/zlint/v3 v3.5.0 // indirect
|
||||||
golang.org/x/mod v0.9.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/net v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
golang.org/x/tools v0.20.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/klog/v2 v2.90.1 // indirect
|
k8s.io/klog/v2 v2.120.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
|
k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect
|
||||||
)
|
)
|
||||||
|
8
govc.env
8
govc.env
@ -1,8 +0,0 @@
|
|||||||
export GOVC_DATACENTER=
|
|
||||||
export GOVC_PASSWORD=
|
|
||||||
export GOVC_URL=
|
|
||||||
export GOVC_USERNAME=
|
|
||||||
export GOVC_INSECURE=1
|
|
||||||
export GOVC_DATASTORE=
|
|
||||||
export NOVIT_VM_FOLDER=
|
|
||||||
export NOVIT_ISO_FOLDER=
|
|
3
hack/build
Executable file
3
hack/build
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
set -ex
|
||||||
|
go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)}" $*
|
8
hack/docker-build
Executable file
8
hack/docker-build
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
set -ex
|
||||||
|
case "$1" in
|
||||||
|
commit) tag=$(git describe --always --dirty) ;;
|
||||||
|
"") tag=latest ;;
|
||||||
|
*) tag=$1 ;;
|
||||||
|
esac
|
||||||
|
docker build -t novit.tech/direktil/local-server:$tag .
|
4
hack/install
Executable file
4
hack/install
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
set -ex
|
||||||
|
go install -trimpath -ldflags "-X main.Version=$(git describe --always --dirty)" \
|
||||||
|
./cmd/dkl-dir2config
|
@ -21,21 +21,3 @@
|
|||||||
.cluster {
|
.cluster {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#store-infos {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
align-content: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
border-bottom: dashed 1pt;
|
|
||||||
margin-bottom: 1ex;
|
|
||||||
}
|
|
||||||
#store-infos > * {
|
|
||||||
display: block;
|
|
||||||
font-size: medium;
|
|
||||||
padding: 2pt 1ex;
|
|
||||||
margin: 0 0 0 1ex;
|
|
||||||
}
|
|
||||||
#store-infos > *:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span id="uiHash">ui <code>{{ uiHash || '-----' }}</code></span>
|
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||||
|
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||||
|
|
||||||
<span class="green" v-if="publicState">🗲</span>
|
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||||
<span class="red" v-else >🗲</span>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -40,16 +40,23 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="publicState.Store.New">
|
<template v-else-if="publicState.Store.New">
|
||||||
<p>Store is new.</p>
|
<p>Store is new.</p>
|
||||||
<form @submit="unlockStore" action="/public/unlock-store">
|
<p>Option 1: initialize a new store</p>
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" required />
|
<form @submit="unlockStore">
|
||||||
<input type="password" v-model="forms.store.pass2" required />
|
<input type="text" v-model="forms.store.name" name="name" placeholder="Name" /><br/>
|
||||||
|
<input type="password" v-model="forms.store.pass1" name="passphrase" required placeholder="Passphrase" />
|
||||||
|
<input type="password" v-model="forms.store.pass2" required placeholder="Passphrase confirmation" />
|
||||||
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||||
</form>
|
</form>
|
||||||
|
<p>Option 2: upload a previously downloaded store</p>
|
||||||
|
<form @submit="uploadStore">
|
||||||
|
<input type="file" ref="storeUpload" />
|
||||||
|
<input type="submit" value="upload" />
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!publicState.Store.Open">
|
<template v-else-if="!publicState.Store.Open">
|
||||||
<p>Store is not open.</p>
|
<p>Store is not open.</p>
|
||||||
<form @submit="unlockStore" action="/public/unlock-store">
|
<form @submit="unlockStore">
|
||||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required />
|
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@ -57,24 +64,13 @@
|
|||||||
<p v-if="!session.token">Not logged in.</p>
|
<p v-if="!session.token">Not logged in.</p>
|
||||||
<p v-else>Invalid token</p>
|
<p v-else>Invalid token</p>
|
||||||
|
|
||||||
<form @submit="setToken">
|
<form @submit="unlockStore">
|
||||||
<input type="password" v-model="forms.setToken" required />
|
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||||
<input type="submit" value="set token"/>
|
<input type="submit" value="log in"/>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div id="store-infos">
|
|
||||||
<h2>Store</h2>
|
|
||||||
<a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">download</a>
|
|
||||||
<form @submit="storeAddKey" action="/store/add-key">
|
|
||||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required />
|
|
||||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required />
|
|
||||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="state.Clusters" id="clusters">
|
<div v-if="state.Clusters" id="clusters">
|
||||||
|
|
||||||
<h2>Clusters</h2>
|
<h2>Clusters</h2>
|
||||||
|
|
||||||
<div class="sheets">
|
<div class="sheets">
|
||||||
@ -90,7 +86,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre v-if="false">{{ state }}</pre>
|
<h2>Admin actions</h2>
|
||||||
|
|
||||||
|
<h3>Config</h3>
|
||||||
|
<form @submit="uploadConfig">
|
||||||
|
<input type="file" ref="configUpload" required />
|
||||||
|
<input type="submit" value="upload config" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>Store</h3>
|
||||||
|
<p><a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">Download</a></p>
|
||||||
|
<form @submit="storeAddKey" action="/store/add-key">
|
||||||
|
<p>Add an unlock phrase:</p>
|
||||||
|
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||||
|
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||||
|
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||||
|
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||||
|
</form>
|
||||||
|
<form @submit="storeDelKey" action="/store/delete-key">
|
||||||
|
<p>Remove an unlock phrase:</p>
|
||||||
|
<input type="text" v-model="forms.delKey.name" name="name" required placeholder="Name" />
|
||||||
|
<input type="submit" value="remove unlock phrase" />
|
||||||
|
|
||||||
|
<p v-if="state.Store.KeyNames">Available names:
|
||||||
|
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<template v-if="state.HostTemplates && state.HostTemplates.length">
|
||||||
|
<h3>Hosts from template</h3>
|
||||||
|
<form @submit="hostFromTemplateAdd" action="">
|
||||||
|
<p>Add a host from template instance:</p>
|
||||||
|
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
||||||
|
<select v-model="forms.hostFromTemplate.Template" required>
|
||||||
|
<option v-for="name in state.HostTemplates" :value="name">{{name}}</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
||||||
|
<input type="submit" value="add instance" />
|
||||||
|
</form>
|
||||||
|
<form @submit="hostFromTemplateDel" action="">
|
||||||
|
<p>Remove a host from template instance:</p>
|
||||||
|
<select v-model="forms.hostFromTemplateDel" required>
|
||||||
|
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="delete instance" :disabled="!forms.hostFromTemplateDel" />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,18 +10,18 @@ export default {
|
|||||||
cluster: ['addons'],
|
cluster: ['addons'],
|
||||||
host: [
|
host: [
|
||||||
"kernel",
|
"kernel",
|
||||||
"initrd-v2",
|
"initrd",
|
||||||
"bootstrap.tar",
|
"bootstrap.tar",
|
||||||
"boot-v2.iso",
|
"boot.img.lz4",
|
||||||
|
"boot.iso",
|
||||||
"config",
|
"config",
|
||||||
"bootstrap-config",
|
"bootstrap-config",
|
||||||
"boot.iso",
|
|
||||||
"boot.tar",
|
"boot.tar",
|
||||||
"boot-efi.tar",
|
"boot-efi.tar",
|
||||||
"boot.img",
|
|
||||||
"boot.img.gz",
|
"boot.img.gz",
|
||||||
"boot.img.lz4",
|
"boot.img",
|
||||||
"initrd",
|
"boot.qcow2",
|
||||||
|
"boot.vmdk",
|
||||||
"ipxe",
|
"ipxe",
|
||||||
],
|
],
|
||||||
}[this.kind]
|
}[this.kind]
|
||||||
|
@ -8,8 +8,9 @@ export default {
|
|||||||
<div class="host">
|
<div class="host">
|
||||||
<div class="title">Host {{ host.Name }}</div>
|
<div class="title">Host {{ host.Name }}</div>
|
||||||
<section>
|
<section>
|
||||||
|
<div><small>Cluster: {{ host.Cluster }}<template v-if="host.Template"> ({{ host.Template }})</template></small></div>
|
||||||
<template v-for="ip in host.IPs">
|
<template v-for="ip in host.IPs">
|
||||||
{{ ip }}
|
<code>{{ ip }}</code>{{" "}}
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
<div class="section">Downloads</div>
|
<div class="section">Downloads</div>
|
||||||
|
@ -9,11 +9,16 @@ createApp({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
forms: {
|
forms: {
|
||||||
store: { },
|
store: {},
|
||||||
|
storeUpload: {},
|
||||||
|
delKey: {},
|
||||||
|
hostFromTemplate: {},
|
||||||
|
hostFromTemplateDel: "",
|
||||||
},
|
},
|
||||||
session: {},
|
session: {},
|
||||||
error: null,
|
error: null,
|
||||||
publicState: null,
|
publicState: null,
|
||||||
|
serverVersion: null,
|
||||||
uiHash: null,
|
uiHash: null,
|
||||||
watchingState: false,
|
watchingState: false,
|
||||||
state: null,
|
state: null,
|
||||||
@ -39,6 +44,7 @@ createApp({
|
|||||||
deep: true,
|
deep: true,
|
||||||
handler(v) {
|
handler(v) {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
this.serverVersion = v.ServerVersion
|
||||||
if (this.uiHash && v.UIHash != this.uiHash) {
|
if (this.uiHash && v.UIHash != this.uiHash) {
|
||||||
console.log("reloading")
|
console.log("reloading")
|
||||||
location.reload()
|
location.reload()
|
||||||
@ -50,6 +56,12 @@ createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hostsFromTemplate() {
|
||||||
|
return (this.state.Hosts||[]).filter((h) => h.Template)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
copyText(text) {
|
copyText(text) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -60,13 +72,34 @@ createApp({
|
|||||||
this.session.token = this.forms.setToken
|
this.session.token = this.forms.setToken
|
||||||
this.forms.setToken = null
|
this.forms.setToken = null
|
||||||
},
|
},
|
||||||
|
uploadStore() {
|
||||||
|
event.preventDefault()
|
||||||
|
this.apiPost('/public/store.tar', this.$refs.storeUpload.files[0], (v) => {
|
||||||
|
this.forms.store = {}
|
||||||
|
}, "application/tar")
|
||||||
|
},
|
||||||
|
namedPassphrase(name, passphrase) {
|
||||||
|
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||||
|
},
|
||||||
storeAddKey() {
|
storeAddKey() {
|
||||||
this.apiPost('/store/add-key', this.forms.store.pass1, (v) => {
|
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
storeDelKey() {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
let name = this.forms.delKey.name
|
||||||
|
|
||||||
|
if (!confirm("Remove key named "+JSON.stringify(name)+"?")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.apiPost('/store/delete-key', name , (v) => {
|
||||||
|
this.forms.delKey = {}
|
||||||
|
})
|
||||||
|
},
|
||||||
unlockStore() {
|
unlockStore() {
|
||||||
this.apiPost('/public/unlock-store', this.forms.store.pass1, (v) => {
|
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
|
||||||
this.forms.store = {}
|
this.forms.store = {}
|
||||||
|
|
||||||
if (v) {
|
if (v) {
|
||||||
@ -78,7 +111,23 @@ createApp({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
apiPost(action, data, onload) {
|
uploadConfig() {
|
||||||
|
this.apiPost('/configs', this.$refs.configUpload.files[0], (v) => {}, "text/vnd.yaml")
|
||||||
|
},
|
||||||
|
hostFromTemplateAdd() {
|
||||||
|
let v = this.forms.hostFromTemplate;
|
||||||
|
this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} });
|
||||||
|
},
|
||||||
|
hostFromTemplateDel() {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
let v = this.forms.hostFromTemplateDel;
|
||||||
|
if (!confirm("delete host template instance "+v+"?")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
|
||||||
|
},
|
||||||
|
apiPost(action, data, onload, contentType = 'application/json') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
@ -97,7 +146,7 @@ createApp({
|
|||||||
var xhr = new XMLHttpRequest()
|
var xhr = new XMLHttpRequest()
|
||||||
|
|
||||||
xhr.responseType = 'json'
|
xhr.responseType = 'json'
|
||||||
// TODO spinner, pending aciton notification, or something
|
// TODO spinner, pending action notification, or something
|
||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
|
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
|
||||||
}
|
}
|
||||||
@ -115,11 +164,37 @@ createApp({
|
|||||||
|
|
||||||
xhr.open("POST", action)
|
xhr.open("POST", action)
|
||||||
xhr.setRequestHeader('Accept', 'application/json')
|
xhr.setRequestHeader('Accept', 'application/json')
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
xhr.setRequestHeader('Content-Type', contentType)
|
||||||
if (this.session.token) {
|
if (this.session.token) {
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||||
}
|
}
|
||||||
xhr.send(JSON.stringify(data))
|
|
||||||
|
if (contentType == "application/json") {
|
||||||
|
xhr.send(JSON.stringify(data))
|
||||||
|
} else {
|
||||||
|
xhr.send(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apiDelete(action, data, onload) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest()
|
||||||
|
xhr.onload = (r) => {
|
||||||
|
if (xhr.status != 200) {
|
||||||
|
this.error = xhr.response
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.error = null
|
||||||
|
if (onload) {
|
||||||
|
onload(xhr.response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open("DELETE", action)
|
||||||
|
xhr.setRequestHeader('Accept', 'application/json')
|
||||||
|
if (this.session.token) {
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||||
|
}
|
||||||
|
xhr.send()
|
||||||
},
|
},
|
||||||
download(url) {
|
download(url) {
|
||||||
event.target.target = '_blank'
|
event.target.target = '_blank'
|
||||||
|
22
modd.conf
22
modd.conf
@ -3,20 +3,24 @@ modd.conf {}
|
|||||||
**/*.go go.mod go.sum {
|
**/*.go go.mod go.sum {
|
||||||
prep: go test ./...
|
prep: go test ./...
|
||||||
prep: mkdir -p dist
|
prep: mkdir -p dist
|
||||||
prep: go build -o dist/ -trimpath ./...
|
prep: hack/build ./...
|
||||||
#prep: docker build --build-arg GOPROXY=$GOPROXY -t dls .
|
#prep: docker build --build-arg GOPROXY=$GOPROXY -t dls .
|
||||||
#daemon +sigterm: /var/lib/direktil/test-run
|
#daemon +sigterm: bash test-run
|
||||||
}
|
}
|
||||||
|
|
||||||
html/**/* {
|
html/**/* {
|
||||||
prep: go build -o dist/ -trimpath ./cmd/dkl-local-server
|
prep: hack/build ./cmd/dkl-local-server
|
||||||
}
|
}
|
||||||
|
|
||||||
dist/dkl-local-server {
|
#dist/dkl-local-server {
|
||||||
prep: mkdir -p tmp
|
# prep: mkdir -p tmp
|
||||||
daemon +sigterm: dist/dkl-local-server -data tmp -auto-unlock test
|
# daemon +sigterm: dist/dkl-local-server -data tmp -auto-unlock test
|
||||||
|
#}
|
||||||
|
|
||||||
|
dist/dkl-dir2config {
|
||||||
|
# prep: dist/dkl-dir2config --debug --in test-dir2config
|
||||||
}
|
}
|
||||||
|
|
||||||
**/*.proto !dist/**/* {
|
#**/*.proto !dist/**/* {
|
||||||
prep: for mod in @mods; do protoc -I ./ --go_out=plugins=grpc,paths=source_relative:. $mod; done
|
# prep: for mod in @mods; do protoc -I ./ --go_out=plugins=grpc,paths=source_relative:. $mod; done
|
||||||
}
|
#}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@ -24,15 +23,13 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Hosts []*Host
|
Hosts []*Host
|
||||||
Groups []*Group
|
Clusters []*Cluster
|
||||||
Clusters []*Cluster
|
Configs []*Template
|
||||||
Configs []*Template
|
StaticPods map[string][]*Template `yaml:"static_pods"`
|
||||||
StaticPods []*Template `yaml:"static_pods"`
|
Addons map[string][]*Template
|
||||||
BootstrapPods map[string][]*Template `yaml:"bootstrap_pods"`
|
SSLConfig string `yaml:"ssl_config"`
|
||||||
Addons map[string][]*Template
|
CertRequests []*CertRequest `yaml:"cert_requests"`
|
||||||
SSLConfig string `yaml:"ssl_config"`
|
|
||||||
CertRequests []*CertRequest `yaml:"cert_requests"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromBytes(data []byte) (*Config, error) {
|
func FromBytes(data []byte) (*Config, error) {
|
||||||
@ -44,7 +41,7 @@ func FromBytes(data []byte) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FromFile(path string) (*Config, error) {
|
func FromFile(path string) (*Config, error) {
|
||||||
ba, err := ioutil.ReadFile(path)
|
ba, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -89,15 +86,6 @@ func (c *Config) HostByMAC(mac string) *Host {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Group(name string) *Group {
|
|
||||||
for _, group := range c.Groups {
|
|
||||||
if group.Name == name {
|
|
||||||
return group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Cluster(name string) *Cluster {
|
func (c *Config) Cluster(name string) *Cluster {
|
||||||
for _, cluster := range c.Clusters {
|
for _, cluster := range c.Clusters {
|
||||||
if cluster.Name == name {
|
if cluster.Name == name {
|
||||||
@ -116,15 +104,6 @@ func (c *Config) ConfigTemplate(name string) *Template {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) StaticPodsTemplate(name string) *Template {
|
|
||||||
for _, s := range c.StaticPods {
|
|
||||||
if s.Name == name {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) CSR(name string) *CertRequest {
|
func (c *Config) CSR(name string) *CertRequest {
|
||||||
for _, s := range c.CertRequests {
|
for _, s := range c.CertRequests {
|
||||||
if s.Name == name {
|
if s.Name == name {
|
||||||
@ -140,36 +119,36 @@ func (c *Config) SaveTo(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ioutil.WriteFile(path, ba, 0600)
|
return os.WriteFile(path, ba, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
Name string
|
Name string
|
||||||
Template string
|
Template string
|
||||||
|
|
||||||
parsedTemplate *template.Template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) Execute(contextName, elementName string, wr io.Writer, data interface{}, extraFuncs map[string]interface{}) error {
|
func (t *Template) Execute(contextName, elementName string, wr io.Writer, data interface{}, extraFuncs map[string]interface{}) error {
|
||||||
if t.parsedTemplate == nil {
|
var templateFuncs = map[string]interface{}{
|
||||||
var templateFuncs = map[string]interface{}{
|
"indent": func(indent, s string) (indented string) {
|
||||||
"indent": func(indent, s string) (indented string) {
|
indented = indent + strings.Replace(s, "\n", "\n"+indent, -1)
|
||||||
indented = indent + strings.Replace(s, "\n", "\n"+indent, -1)
|
return
|
||||||
return
|
},
|
||||||
},
|
"yaml": func(v any) (s string, err error) {
|
||||||
}
|
ba, err := yaml.Marshal(v)
|
||||||
|
s = string(ba)
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for name, f := range extraFuncs {
|
for name, f := range extraFuncs {
|
||||||
templateFuncs[name] = f
|
templateFuncs[name] = f
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.New(t.Name).
|
tmpl, err := template.New(t.Name).
|
||||||
Funcs(templateFuncs).
|
Funcs(templateFuncs).
|
||||||
Parse(t.Template)
|
Parse(t.Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
t.parsedTemplate = tmpl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *templateDetailsDir != "" {
|
if *templateDetailsDir != "" {
|
||||||
@ -181,7 +160,7 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
|||||||
base += string(filepath.Separator)
|
base += string(filepath.Separator)
|
||||||
log.Print("writing template details: ", base, "{in,data,out}")
|
log.Print("writing template details: ", base, "{in,data,out}")
|
||||||
|
|
||||||
if err := ioutil.WriteFile(base+"in", []byte(t.Template), 0600); err != nil {
|
if err := os.WriteFile(base+"in", []byte(t.Template), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +169,7 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(base+"data", yamlBytes, 0600); err != nil {
|
if err := os.WriteFile(base+"data", yamlBytes, 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,42 +183,38 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
|||||||
wr = io.MultiWriter(wr, out)
|
wr = io.MultiWriter(wr, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.parsedTemplate.Execute(wr, data)
|
return tmpl.Execute(wr, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host represents a host served by this server.
|
// Host represents a host served by this server.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
WithRev
|
WithRev
|
||||||
|
|
||||||
Name string
|
Template bool `json:",omitempty"`
|
||||||
Labels map[string]string
|
|
||||||
Annotations map[string]string
|
|
||||||
|
|
||||||
MAC string
|
Name string
|
||||||
|
Labels map[string]string `json:",omitempty"`
|
||||||
|
Annotations map[string]string `json:",omitempty"`
|
||||||
|
|
||||||
|
MAC string `json:",omitempty"`
|
||||||
IP string
|
IP string
|
||||||
IPs []string
|
IPs []string `json:",omitempty"`
|
||||||
Cluster string
|
Cluster string
|
||||||
Group string
|
Group string
|
||||||
Vars Vars
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group represents a group of hosts and provides their configuration.
|
Net string
|
||||||
type Group struct {
|
IPFrom map[string]string `json:",omitempty" yaml:"ip_from"`
|
||||||
WithRev
|
|
||||||
|
|
||||||
Name string
|
IPXE string `json:",omitempty"`
|
||||||
Labels map[string]string
|
|
||||||
Annotations map[string]string
|
|
||||||
|
|
||||||
Master bool
|
|
||||||
IPXE string
|
|
||||||
Kernel string
|
Kernel string
|
||||||
Initrd string
|
Initrd string
|
||||||
BootstrapConfig string `yaml:"bootstrap_config"`
|
BootstrapConfig string `yaml:"bootstrap_config"`
|
||||||
Config string
|
Config string
|
||||||
StaticPods string `yaml:"static_pods"`
|
|
||||||
Versions map[string]string
|
Versions map[string]string
|
||||||
Vars Vars
|
|
||||||
|
StaticPods string `yaml:"static_pods"`
|
||||||
|
|
||||||
|
Vars Vars
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vars store user-defined key-values
|
// Vars store user-defined key-values
|
||||||
@ -253,13 +228,13 @@ type Cluster struct {
|
|||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
|
|
||||||
Domain string
|
Domain string
|
||||||
Addons string
|
Addons []string
|
||||||
BootstrapPods string `yaml:"bootstrap_pods"`
|
Subnets struct {
|
||||||
Subnets struct {
|
|
||||||
Services string
|
Services string
|
||||||
Pods string
|
Pods string
|
||||||
}
|
}
|
||||||
|
|
||||||
Vars Vars
|
Vars Vars
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +249,7 @@ func (c *Cluster) DNSSvcIP() net.IP {
|
|||||||
func (c *Cluster) NthSvcIP(n byte) net.IP {
|
func (c *Cluster) NthSvcIP(n byte) net.IP {
|
||||||
_, cidr, err := net.ParseCIDR(c.Subnets.Services)
|
_, cidr, err := net.ParseCIDR(c.Subnets.Services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("Invalid services CIDR: %v", err))
|
panic(fmt.Errorf("invalid services CIDR: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := cidr.IP
|
ip := cidr.IP
|
||||||
|
@ -3,7 +3,6 @@ package clustersconfig
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,40 +14,38 @@ import (
|
|||||||
// Debug enables debug logs from this package.
|
// Debug enables debug logs from this package.
|
||||||
var Debug = false
|
var Debug = false
|
||||||
|
|
||||||
func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
func FromDir(
|
||||||
if Debug {
|
read func(path string) ([]byte, error),
|
||||||
log.Printf("loading config from dir %s (defaults from %s)", dirPath, defaultsPath)
|
assemble func(path string) ([]byte, error),
|
||||||
}
|
listBase func(path string) ([]string, error),
|
||||||
|
listMerged func(path string) ([]string, error),
|
||||||
|
) (*Config, error) {
|
||||||
|
|
||||||
defaults, err := NewDefaults(defaultsPath)
|
load := func(dir, name string, out any) (err error) {
|
||||||
if err != nil {
|
ba, err := assemble(filepath.Join(dir, name))
|
||||||
return nil, fmt.Errorf("failed to load defaults: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
store := &dirStore{dirPath}
|
|
||||||
load := func(dir, name string, out Rev) error {
|
|
||||||
ba, err := store.Get(path.Join(dir, name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load %s/%s from dir: %v", dir, name, err)
|
return
|
||||||
}
|
}
|
||||||
if err = defaults.Load(dir, ".yaml", out, ba); err != nil {
|
err = yaml.UnmarshalStrict(ba, out)
|
||||||
return fmt.Errorf("failed to enrich %s/%s from defaults: %v", dir, name, err)
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
Addons: make(map[string][]*Template),
|
Addons: make(map[string][]*Template),
|
||||||
BootstrapPods: make(map[string][]*Template),
|
StaticPods: make(map[string][]*Template),
|
||||||
}
|
}
|
||||||
|
|
||||||
// load clusters
|
// load clusters
|
||||||
names, err := store.List("clusters")
|
names, err := listBase("clusters")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list clusters: %v", err)
|
return nil, fmt.Errorf("failed to list clusters: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
name, _ = strings.CutSuffix(name, ".yaml")
|
||||||
cluster := &Cluster{Name: name}
|
cluster := &Cluster{Name: name}
|
||||||
if err := load("clusters", name, cluster); err != nil {
|
if err := load("clusters", name, cluster); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -57,103 +54,14 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
|||||||
config.Clusters = append(config.Clusters, cluster)
|
config.Clusters = append(config.Clusters, cluster)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load groups
|
|
||||||
names, err = store.List("groups")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to list groups: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
read := func(rev, filePath string) (data []byte, fromDefaults bool, err error) {
|
|
||||||
data, err = store.Get(filePath)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("faild to read %s: %v", filePath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
return // ok
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rev) == 0 {
|
|
||||||
err = fmt.Errorf("entry not found: %s", filePath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err = defaults.ReadAll(rev, filePath+".yaml")
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("failed to read %s:%s: %v", rev, filePath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fromDefaults = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
template := func(rev, dir, name string, templates *[]*Template) (ref string, err error) {
|
|
||||||
ref = name
|
|
||||||
if len(name) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ba, fromDefaults, err := read(rev, path.Join(dir, name))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if fromDefaults {
|
|
||||||
ref = rev + ":" + name
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasTemplate(ref, *templates) {
|
|
||||||
if Debug {
|
|
||||||
log.Printf("new template in %s: %s", dir, ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
*templates = append(*templates, &Template{
|
|
||||||
Name: ref,
|
|
||||||
Template: string(ba),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
group := &Group{Name: name}
|
|
||||||
if err := load("groups", name, group); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.BootstrapConfig, err = template(group.Rev(), "configs", group.BootstrapConfig, &config.Configs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config for group %q: %v", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
group.Config, err = template(group.Rev(), "configs", group.Config, &config.Configs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config for group %q: %v", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if Debug {
|
|
||||||
log.Printf("group %q: config=%q static_pods=%q",
|
|
||||||
group.Name, group.Config, group.StaticPods)
|
|
||||||
}
|
|
||||||
|
|
||||||
group.StaticPods, err = template(group.Rev(), "static-pods", group.StaticPods, &config.StaticPods)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load static pods for group %q: %v", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Groups = append(config.Groups, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
// load hosts
|
// load hosts
|
||||||
names, err = store.List("hosts")
|
names, err = listBase("hosts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list hosts: %v", err)
|
return nil, fmt.Errorf("failed to list hosts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
name, _ = strings.CutSuffix(name, ".yaml")
|
||||||
o := &Host{Name: name}
|
o := &Host{Name: name}
|
||||||
if err := load("hosts", name, o); err != nil {
|
if err := load("hosts", name, o); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -163,28 +71,20 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load config templates
|
// load config templates
|
||||||
loadTemplates := func(rev, dir string, templates *[]*Template) error {
|
loadTemplates := func(dir string, templates *[]*Template) error {
|
||||||
names, err := store.List(dir)
|
names, err := listMerged(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list %s: %v", dir, err)
|
return fmt.Errorf("failed to list %s: %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rev) != 0 {
|
for _, fullName := range names {
|
||||||
var defaultsNames []string
|
name, _ := strings.CutSuffix(fullName, ".yaml")
|
||||||
defaultsNames, err = defaults.List(rev, dir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to list %s:%s: %v", rev, dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
names = append(names, defaultsNames...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
if hasTemplate(name, *templates) {
|
if hasTemplate(name, *templates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ba, _, err := read(rev, path.Join(dir, name))
|
ba, err := read(path.Join(dir, fullName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -198,53 +98,57 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadTemplates("configs", &config.Configs)
|
||||||
|
|
||||||
// cluster addons
|
// cluster addons
|
||||||
for _, cluster := range config.Clusters {
|
for _, cluster := range config.Clusters {
|
||||||
addonSet := cluster.Addons
|
addonSets := cluster.Addons
|
||||||
if len(addonSet) == 0 {
|
if len(addonSets) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := config.Addons[addonSet]; ok {
|
for _, addonSet := range addonSets {
|
||||||
continue
|
if _, ok := config.Addons[addonSet]; ok {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
templates := make([]*Template, 0)
|
templates := make([]*Template, 0)
|
||||||
if err = loadTemplates(cluster.Rev(), path.Join("addons", addonSet), &templates); err != nil {
|
if err = loadTemplates(path.Join("addons", addonSet), &templates); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Addons[addonSet] = templates
|
config.Addons[addonSet] = templates
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cluster bootstrap pods
|
// cluster static pods
|
||||||
for _, cluster := range config.Clusters {
|
for _, host := range config.Hosts {
|
||||||
bpSet := cluster.BootstrapPods
|
bpSet := host.StaticPods
|
||||||
if bpSet == "" {
|
if bpSet == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := config.BootstrapPods[bpSet]; ok {
|
if _, ok := config.StaticPods[bpSet]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
templates := make([]*Template, 0)
|
templates := make([]*Template, 0)
|
||||||
if err = loadTemplates(cluster.Rev(), path.Join("bootstrap-pods", bpSet), &templates); err != nil {
|
if err = loadTemplates(path.Join("static-pods", bpSet), &templates); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.BootstrapPods[bpSet] = templates
|
config.StaticPods[bpSet] = templates
|
||||||
}
|
}
|
||||||
|
|
||||||
// load SSL configuration
|
// load SSL configuration
|
||||||
if ba, err := ioutil.ReadFile(filepath.Join(dirPath, "ssl-config.json")); err == nil {
|
if ba, err := read("ssl-config.json"); err == nil {
|
||||||
config.SSLConfig = string(ba)
|
config.SSLConfig = string(ba)
|
||||||
|
|
||||||
} else if !os.IsNotExist(err) {
|
} else if !os.IsNotExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ba, err := ioutil.ReadFile(filepath.Join(dirPath, "cert-requests.yaml")); err == nil {
|
if ba, err := read("cert-requests.yaml"); err == nil {
|
||||||
reqs := make([]*CertRequest, 0)
|
reqs := make([]*CertRequest, 0)
|
||||||
if err = yaml.Unmarshal(ba, &reqs); err != nil {
|
if err = yaml.Unmarshal(ba, &reqs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package secretstore
|
package secretstore
|
||||||
|
|
||||||
|
func Memzero(ba []byte) { memzero(ba) }
|
||||||
|
|
||||||
func memzero(ba []byte) {
|
func memzero(ba []byte) {
|
||||||
for i := range ba {
|
for i := range ba {
|
||||||
ba[i] = 0
|
ba[i] = 0
|
||||||
|
@ -2,29 +2,34 @@ package secretstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
|
Salt [aes.BlockSize]byte
|
||||||
|
Keys []KeyEntry
|
||||||
|
|
||||||
unlocked bool
|
unlocked bool
|
||||||
key [32]byte
|
key [32]byte
|
||||||
salt [aes.BlockSize]byte
|
|
||||||
keys []keyEntry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyEntry struct {
|
type KeyEntry struct {
|
||||||
hash [64]byte
|
Name string
|
||||||
encKey [32]byte
|
Hash [64]byte
|
||||||
|
EncKey [32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() (s *Store) {
|
func New() (s *Store) {
|
||||||
@ -77,30 +82,32 @@ func (s *Store) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) IsNew() bool {
|
func (s *Store) IsNew() bool {
|
||||||
return len(s.keys) == 0
|
return len(s.Keys) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Unlocked() bool {
|
func (s *Store) Unlocked() bool {
|
||||||
return s.unlocked
|
return s.unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Init(passphrase []byte) (err error) {
|
func (s *Store) Init(name string, passphrase []byte) (err error) {
|
||||||
err = randRead(s.key[:])
|
err = randRead(s.key[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = randRead(s.salt[:])
|
err = randRead(s.Salt[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddKey(passphrase)
|
s.AddKey(name, passphrase)
|
||||||
|
|
||||||
s.unlocked = true
|
s.unlocked = true
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsonFormatHdr = []byte("{json}")
|
||||||
|
|
||||||
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
||||||
memzero(s.key[:])
|
memzero(s.key[:])
|
||||||
s.unlocked = false
|
s.unlocked = false
|
||||||
@ -117,69 +124,77 @@ func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
|||||||
n += int64(nr)
|
n += int64(nr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the salt
|
// read the file's start (json header or start of salt)
|
||||||
readFull(s.salt[:])
|
|
||||||
|
readFull(s.Salt[:len(jsonFormatHdr)])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the (encrypted) keys
|
if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
|
||||||
s.keys = make([]keyEntry, 0)
|
// old key file
|
||||||
for {
|
|
||||||
k := keyEntry{}
|
// finish reading the salt
|
||||||
readFull(k.hash[:])
|
readFull(s.Salt[len(jsonFormatHdr):])
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
readFull(k.encKey[:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.keys = append(s.keys, k)
|
// read the (encrypted) keys
|
||||||
|
s.Keys = make([]KeyEntry, 0)
|
||||||
|
for {
|
||||||
|
k := KeyEntry{Name: "key-" + strconv.Itoa(len(s.Keys))}
|
||||||
|
readFull(k.Hash[:])
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readFull(k.EncKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Keys = append(s.Keys, k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(in).Decode(s)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
||||||
write := func(ba []byte) {
|
_, err = out.Write(jsonFormatHdr)
|
||||||
var nr int
|
|
||||||
nr, err = out.Write(ba)
|
|
||||||
n += int64(nr)
|
|
||||||
}
|
|
||||||
|
|
||||||
write(s.salt[:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = json.NewEncoder(out).Encode(s)
|
||||||
for _, k := range s.keys {
|
|
||||||
write(k.hash[:])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
write(k.encKey[:])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNoSuchKey = errors.New("no such key")
|
var ErrNoSuchKey = errors.New("no such key")
|
||||||
|
|
||||||
|
func (s *Store) HasKey(passphrase []byte) bool {
|
||||||
|
key, hash := s.keyPairFromPassword(passphrase)
|
||||||
|
defer memzero(key[:])
|
||||||
|
|
||||||
|
for _, k := range s.Keys {
|
||||||
|
if k.Hash == hash {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
||||||
key, hash := s.keyPairFromPassword(passphrase)
|
key, hash := s.keyPairFromPassword(passphrase)
|
||||||
memzero(passphrase)
|
|
||||||
defer memzero(key[:])
|
defer memzero(key[:])
|
||||||
|
|
||||||
var idx = -1
|
var idx = -1
|
||||||
for i := range s.keys {
|
for i := range s.Keys {
|
||||||
if hash == s.keys[i].hash {
|
if hash == s.Keys[i].Hash {
|
||||||
idx = i
|
idx = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -189,28 +204,28 @@ func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.decryptTo(s.key[:], s.keys[idx].encKey[:], &key)
|
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
|
||||||
|
|
||||||
s.unlocked = true
|
s.unlocked = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) AddKey(passphrase []byte) {
|
func (s *Store) AddKey(name string, passphrase []byte) {
|
||||||
key, hash := s.keyPairFromPassword(passphrase)
|
key, hash := s.keyPairFromPassword(passphrase)
|
||||||
memzero(passphrase)
|
memzero(passphrase)
|
||||||
|
|
||||||
defer memzero(key[:])
|
defer memzero(key[:])
|
||||||
|
|
||||||
k := keyEntry{hash: hash}
|
k := KeyEntry{Name: name, Hash: hash}
|
||||||
|
|
||||||
encKey := s.encrypt(s.key[:], &key)
|
encKey := s.encrypt(s.key[:], &key)
|
||||||
copy(k.encKey[:], encKey)
|
copy(k.EncKey[:], encKey)
|
||||||
|
|
||||||
s.keys = append(s.keys, k)
|
s.Keys = append(s.Keys, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
||||||
keySlice := argon2.IDKey(password, s.salt[:], 1, 64*1024, 4, 32)
|
keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)
|
||||||
|
|
||||||
copy(key[:], keySlice)
|
copy(key[:], keySlice)
|
||||||
memzero(keySlice)
|
memzero(keySlice)
|
||||||
@ -236,12 +251,12 @@ func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
|||||||
|
|
||||||
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
||||||
dst = make([]byte, len(src))
|
dst = make([]byte, len(src))
|
||||||
newEncrypter(s.salt, key).XORKeyStream(dst, src)
|
newEncrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
||||||
newDecrypter(s.salt, key).XORKeyStream(dst, src)
|
newDecrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
set -e
|
|
||||||
|
|
||||||
dir=/var/lib/direktil/
|
|
||||||
PATH=$PATH:$dir
|
|
||||||
cd $dir
|
|
||||||
|
|
||||||
if [ ! -f govc.env ]; then
|
|
||||||
echo ERROR: govc.env file not found in dir $dir ; exit 1
|
|
||||||
fi
|
|
||||||
source govc.env
|
|
||||||
|
|
||||||
if [ $# != 2 ]; then
|
|
||||||
echo "Usage: $0 <VM_NAME> <NOVIT_HOST>" ; exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z $NOVIT_VM_FOLDER || -z $NOVIT_ISO_FOLDER ]]; then
|
|
||||||
echo "ERROR: All GOVC env vars (including NOVIT_VM_FOLDER and NOVIT_ISO_FOLDER) must be provided" ; exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VM=$1
|
|
||||||
HOST=$2
|
|
||||||
|
|
||||||
govc vm.power -off "$NOVIT_VM_FOLDER/$VM" || true
|
|
||||||
sleep 5
|
|
||||||
curl localhost:7606/hosts/$HOST/boot.iso | govc datastore.upload - "$NOVIT_ISO_FOLDER/$VM.iso"
|
|
||||||
sleep 5
|
|
||||||
govc vm.power -on "$NOVIT_VM_FOLDER/$VM"
|
|
Reference in New Issue
Block a user