Compare commits
2 Commits
main
...
014a8a7c83
| Author | SHA1 | Date | |
|---|---|---|---|
| 014a8a7c83 | |||
| 44fc114dee |
@ -1,6 +1,5 @@
|
|||||||
from novit.tech/direktil/dkl:bbea9b9 as dkl
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from golang:1.25.5-trixie as build
|
from golang:1.24.4-bookworm as build
|
||||||
|
|
||||||
run apt-get update && apt-get install -y git
|
run apt-get update && apt-get install -y git
|
||||||
|
|
||||||
@ -22,14 +21,13 @@ run \
|
|||||||
hack/build ./...
|
hack/build ./...
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from debian:trixie
|
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 \
|
||||||
&& yes |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 wireguard-tools \
|
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils \
|
||||||
&& apt-get clean
|
&& apt-get clean
|
||||||
|
|
||||||
copy --from=dkl /bin/dkl /bin/dls /bin/
|
|
||||||
copy --from=build /src/dist/ /bin/
|
copy --from=build /src/dist/ /bin/
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
@ -129,22 +127,12 @@ func main() {
|
|||||||
|
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
switch ext := path.Ext(*outPath); ext {
|
|
||||||
case ".yaml":
|
|
||||||
out.Write([]byte("# dkl-dir2config " + Version + "\n"))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
case ".json":
|
|
||||||
if err = json.NewEncoder(out).Encode(dst); err != nil {
|
|
||||||
log.Fatal("failed to render output: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Fatal("unknown output file extension: ", ext)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cfgPath(subPath string) string { return filepath.Join(*dir, subPath) }
|
func cfgPath(subPath string) string { return filepath.Join(*dir, subPath) }
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cespare/xxhash"
|
"github.com/cespare/xxhash"
|
||||||
@ -224,7 +223,6 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reqJson := cleanJsonObject(buf.String())
|
|
||||||
|
|
||||||
key := name
|
key := name
|
||||||
if req.PerHost {
|
if req.PerHost {
|
||||||
@ -237,11 +235,11 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
|
|||||||
dir := "/etc/tls/" + name
|
dir := "/etc/tls/" + name
|
||||||
|
|
||||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q %q }}", funcName,
|
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q %q }}", funcName,
|
||||||
dir, cluster, req.CA, key, req.Profile, req.Label, reqJson)
|
dir, cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||||
|
|
||||||
default:
|
default:
|
||||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
|
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
|
||||||
cluster, req.CA, key, req.Profile, req.Label, reqJson)
|
cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -282,24 +280,16 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"ssh_user_ca": func(path string) (s string) {
|
"ssh_user_ca": func(path string) (s string) {
|
||||||
return fmt.Sprintf("{{ ssh_user_ca %q %q }}",
|
return fmt.Sprintf("{{ ssh_user_ca %q %q}}",
|
||||||
path, cluster)
|
path, cluster)
|
||||||
},
|
},
|
||||||
"ssh_host_keys": func(dir string) (s string) {
|
"ssh_host_keys": func(dir string) (s string) {
|
||||||
return fmt.Sprintf("{{ ssh_host_keys %q %q \"\" }}",
|
return fmt.Sprintf("{{ ssh_host_keys %q %q \"\"}}",
|
||||||
dir, cluster)
|
dir, cluster)
|
||||||
},
|
},
|
||||||
"host_download_token": func() (s string) {
|
"host_download_token": func() (s string) {
|
||||||
return "{{ host_download_token }}"
|
return "{{ host_download_token }}"
|
||||||
},
|
},
|
||||||
"asset_download_token": func(args ...string) (s string) {
|
|
||||||
argsStr := new(strings.Builder)
|
|
||||||
for _, arg := range args {
|
|
||||||
argsStr.WriteByte(' ')
|
|
||||||
argsStr.WriteString(strconv.Quote(arg))
|
|
||||||
}
|
|
||||||
return "{{ asset_download_token" + argsStr.String() + " }}"
|
|
||||||
},
|
|
||||||
|
|
||||||
"hosts_of_group": func() (hosts []any) {
|
"hosts_of_group": func() (hosts []any) {
|
||||||
hosts = make([]any, 0)
|
hosts = make([]any, 0)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ func (ctx *renderContext) renderStaticPods() (pods []namePod) {
|
|||||||
for n := 0; ; n++ {
|
for n := 0; ; n++ {
|
||||||
str := buf.String()
|
str := buf.String()
|
||||||
|
|
||||||
podMap := map[string]any{}
|
podMap := map[string]interface{}{}
|
||||||
err := dec.Decode(podMap)
|
err := dec.Decode(podMap)
|
||||||
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@ -46,7 +46,7 @@ func (ctx *renderContext) renderStaticPods() (pods []namePod) {
|
|||||||
log.Fatalf("static 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[any]any)
|
md := podMap["metadata"].(map[interface{}]interface{})
|
||||||
|
|
||||||
namespace := md["namespace"].(string)
|
namespace := md["namespace"].(string)
|
||||||
name := md["name"].(string)
|
name := md["name"].(string)
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func cleanJsonObject(raw string) string {
|
|
||||||
v := map[string]any{}
|
|
||||||
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
|
||||||
panic(fmt.Errorf("invalid json: %w\n%s", err, raw))
|
|
||||||
}
|
|
||||||
clean, _ := json.Marshal(v)
|
|
||||||
return string(clean)
|
|
||||||
}
|
|
||||||
@ -109,7 +109,7 @@ func qemuImgBootImg(format string) func(out io.Writer, ctx *renderContext) (err
|
|||||||
var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version")
|
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 := distFetch("grub-support", *grubSupportVersion)
|
path, err := ctx.distFetch("grub-support", *grubSupportVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -148,7 +148,6 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
log.Print("device: ", dev)
|
log.Print("device: ", dev)
|
||||||
syncSysToDev()
|
|
||||||
|
|
||||||
tempDir := bootImg.Name() + ".p1.mount"
|
tempDir := bootImg.Name() + ".p1.mount"
|
||||||
|
|
||||||
@ -162,10 +161,9 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
|||||||
os.RemoveAll(tempDir)
|
os.RemoveAll(tempDir)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
devp1 := dev + "p1"
|
err = syscall.Mount(dev+"p1", tempDir, "vfat", 0, "")
|
||||||
err = syscall.Mount(devp1, tempDir, "vfat", 0, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err)
|
return fmt.Errorf("failed to mount %s to %s: %v", dev+"p1", tempDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kernel
|
// kernel
|
||||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
kernelPath, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kernel
|
// kernel
|
||||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
kernelPath, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -11,7 +12,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/cpiocat"
|
"novit.tech/direktil/pkg/cpiocat"
|
||||||
@ -40,15 +40,10 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zout, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(12)))
|
cat := cpiocat.New(out)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("zstd writer setup failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cat := cpiocat.New(zout)
|
|
||||||
|
|
||||||
// initrd
|
// initrd
|
||||||
initrdPath, err := distFetch("initrd", ctx.Host.Initrd)
|
initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,7 +55,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
case "modules":
|
case "modules":
|
||||||
|
|
||||||
layerVersion := ctx.Host.Versions[layer]
|
layerVersion := ctx.Host.Versions[layer]
|
||||||
modulesPath, err := distFetch("layers", layer, layerVersion)
|
modulesPath, err := ctx.distFetch("layers", layer, layerVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -81,7 +76,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
cat.AppendDir("/etc/ssh", 0o700)
|
cat.AppendDir("/etc/ssh", 0o700)
|
||||||
|
|
||||||
// XXX do we want bootstrap-stage keys instead of the real host key?
|
// XXX do we want bootstrap-stage keys instead of the real host key?
|
||||||
for _, format := range []string{"rsa", "ecdsa", "ed25519"} {
|
for _, format := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
|
||||||
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
||||||
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
||||||
}
|
}
|
||||||
@ -94,15 +89,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
|
|
||||||
cat.AppendBytes(userCA, "user_ca.pub", 0600)
|
cat.AppendBytes(userCA, "user_ca.pub", 0600)
|
||||||
|
|
||||||
if err = cat.Close(); err != nil {
|
return cat.Close()
|
||||||
return fmt.Errorf("cpio close failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = zout.Close(); err != nil {
|
|
||||||
return fmt.Errorf("zstd close failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
||||||
@ -126,6 +113,7 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
err = fmt.Errorf("signing to %s failed: %w", name, err)
|
err = fmt.Errorf("signing to %s failed: %w", name, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
sigBytes = []byte(base64.StdEncoding.EncodeToString(sigBytes))
|
||||||
|
|
||||||
if err = arch.WriteHeader(&tar.Header{
|
if err = arch.WriteHeader(&tar.Header{
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -179,7 +167,7 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
|||||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
return fmt.Errorf("layer %q not mapped to a version", layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
outPath, err := distFetch("layers", layer, layerVersion)
|
outPath, err := ctx.distFetch("layers", layer, layerVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,14 +60,6 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
|||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"quote": strconv.Quote,
|
"quote": strconv.Quote,
|
||||||
"yaml": asYaml,
|
|
||||||
"indent": func(s, indent string) string {
|
|
||||||
buf := new(strings.Builder)
|
|
||||||
for _, line := range strings.Split(s, "\n") {
|
|
||||||
buf.WriteString(indent + line + "\n")
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
},
|
|
||||||
|
|
||||||
"password": func(cluster, name, hashAlg string) (password string, err error) {
|
"password": func(cluster, name, hashAlg string) (password string, err error) {
|
||||||
key := cluster + "/" + name
|
key := cluster + "/" + name
|
||||||
@ -178,10 +170,40 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
|||||||
s = string(kc.Cert)
|
s = string(kc.Cert)
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||||
|
ca, err := getUsableClusterCA(cluster, caName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return asYaml([]config.FileDef{
|
||||||
|
{
|
||||||
|
Path: path.Join(dir, "ca.crt"),
|
||||||
|
Mode: 0644,
|
||||||
|
Content: string(ca.Cert),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: path.Join(dir, "tls.crt"),
|
||||||
|
Mode: 0644,
|
||||||
|
Content: string(kc.Cert),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: path.Join(dir, "tls.key"),
|
||||||
|
Mode: 0600,
|
||||||
|
Content: string(kc.Key),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asYaml(v any) (string, error) {
|
func asYaml(v interface{}) (string, error) {
|
||||||
ba, err := yaml.Marshal(v)
|
ba, err := yaml.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
func htmlHeader(title string) string {
|
|
||||||
return `<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>` + title + `</title>
|
|
||||||
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
|
||||||
</head>
|
|
||||||
<body><h1>` + title + `</h1>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
var htmlFooter = `</body>
|
|
||||||
</html>`
|
|
||||||
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error {
|
func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error {
|
||||||
path, err := distFetch("kernels", ctx.Host.Kernel)
|
path, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchKernel(out io.Writer, ctx *renderContext) (err error) {
|
func fetchKernel(out io.Writer, ctx *renderContext) (err error) {
|
||||||
path, err := distFetch("kernels", ctx.Host.Kernel)
|
path, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,6 @@ func main() {
|
|||||||
staticHandler := http.FileServer(http.FS(dlshtml.FS))
|
staticHandler := http.FileServer(http.FS(dlshtml.FS))
|
||||||
http.Handle("/favicon.ico", staticHandler)
|
http.Handle("/favicon.ico", staticHandler)
|
||||||
http.Handle("/ui/", staticHandler)
|
http.Handle("/ui/", staticHandler)
|
||||||
http.Handle("/dist/", http.StripPrefix("/dist/", upstreamServer{}))
|
|
||||||
|
|
||||||
http.Handle("/public-state", streamsse.StreamHandler(wPublicState))
|
http.Handle("/public-state", streamsse.StreamHandler(wPublicState))
|
||||||
http.Handle("/state", requireAdmin(streamsse.StreamHandler(wState)))
|
http.Handle("/state", requireAdmin(streamsse.StreamHandler(wState)))
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -14,12 +13,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
|
||||||
|
|
||||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||||
"github.com/cloudflare/cfssl/csr"
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
@ -142,7 +138,7 @@ func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func distFilePath(path ...string) string {
|
func (ctx *renderContext) distFilePath(path ...string) string {
|
||||||
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,21 +164,6 @@ func (ctx *renderContext) Tag() (string, error) {
|
|||||||
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||||
funcs := templateFuncs(ctx.SSLConfig)
|
funcs := templateFuncs(ctx.SSLConfig)
|
||||||
|
|
||||||
// FIXME duplicate from cluster-render-context
|
|
||||||
getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc KeyCert, err error) {
|
|
||||||
certReq := &csr.CertificateRequest{
|
|
||||||
KeyRequest: csr.NewKeyRequest(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(reqJson), certReq)
|
|
||||||
if err != nil {
|
|
||||||
log.Print("CSR unmarshal failed on: ", reqJson)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, ctx.SSLConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, method := range map[string]any{
|
for name, method := range map[string]any{
|
||||||
"host_ip": func() (s string) {
|
"host_ip": func() (s string) {
|
||||||
return ctx.Host.IPs[0]
|
return ctx.Host.IPs[0]
|
||||||
@ -195,39 +176,6 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
|||||||
return hex.EncodeToString(ba[:])
|
return hex.EncodeToString(ba[:])
|
||||||
},
|
},
|
||||||
|
|
||||||
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
|
||||||
ca, err := getUsableClusterCA(cluster, caName)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reqJson = strings.ReplaceAll(reqJson, "${host_ip}", ctx.Host.IPs[0])
|
|
||||||
reqJson = strings.ReplaceAll(reqJson, "${host_name}", ctx.Host.Name)
|
|
||||||
|
|
||||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return asYaml([]config.FileDef{
|
|
||||||
{
|
|
||||||
Path: path.Join(dir, "ca.crt"),
|
|
||||||
Mode: 0644,
|
|
||||||
Content: string(ca.Cert),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Path: path.Join(dir, "tls.crt"),
|
|
||||||
Mode: 0644,
|
|
||||||
Content: string(kc.Cert),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Path: path.Join(dir, "tls.key"),
|
|
||||||
Mode: 0600,
|
|
||||||
Content: string(kc.Key),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
"ssh_user_ca": func(path, cluster string) (s string, err error) {
|
"ssh_user_ca": func(path, cluster string) (s string, err error) {
|
||||||
userCA, err := sshCAPubKey(cluster)
|
userCA, err := sshCAPubKey(cluster)
|
||||||
return asYaml([]config.FileDef{{
|
return asYaml([]config.FileDef{{
|
||||||
@ -291,46 +239,6 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
|||||||
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
"asset_download_token": func(asset string, params ...string) (token string, err error) {
|
|
||||||
now := time.Now()
|
|
||||||
exp := now.Add(24 * time.Hour) // expire in 24h by default
|
|
||||||
if len(params) != 0 {
|
|
||||||
exp, err = parseCertDuration(params[0], now)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set := DownloadSet{
|
|
||||||
Expiry: exp,
|
|
||||||
Items: []DownloadSetItem{
|
|
||||||
{
|
|
||||||
Kind: "host",
|
|
||||||
Name: ctx.Host.Name,
|
|
||||||
Assets: []string{asset},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
privKey, _ := dlsSigningKeys()
|
|
||||||
token = set.Signed(privKey)
|
|
||||||
|
|
||||||
return
|
|
||||||
},
|
|
||||||
"wg_key": func(name string) (key string, err error) {
|
|
||||||
return wgKey(name + "/hosts/" + ctx.Host.Name)
|
|
||||||
},
|
|
||||||
"wg_psk": func(name, peerName string) (key string, err error) {
|
|
||||||
a := ctx.Host.Name
|
|
||||||
b := peerName
|
|
||||||
if a > b {
|
|
||||||
a, b = b, a
|
|
||||||
}
|
|
||||||
return wgKey(name + "/psks/" + a + " " + b)
|
|
||||||
},
|
|
||||||
"wg_pubkey": func(name, host string) (key string, err error) {
|
|
||||||
return wgKey(name + "/hosts/" + host)
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
funcs[name] = method
|
funcs[name] = method
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
@ -10,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudflare/cfssl/certinfo"
|
"github.com/cloudflare/cfssl/certinfo"
|
||||||
"github.com/cloudflare/cfssl/config"
|
"github.com/cloudflare/cfssl/config"
|
||||||
"github.com/cloudflare/cfssl/helpers/derhelpers"
|
|
||||||
"github.com/cloudflare/cfssl/log"
|
"github.com/cloudflare/cfssl/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -75,33 +73,3 @@ func checkCertUsable(certPEM []byte) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dlsSigningKeys() (ed25519.PrivateKey, ed25519.PublicKey) {
|
|
||||||
var signerDER []byte
|
|
||||||
|
|
||||||
if err := readSecret("signer", &signerDER); os.IsNotExist(err) {
|
|
||||||
_, key, err := ed25519.GenerateKey(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signerDER, err = derhelpers.MarshalEd25519PrivateKey(key)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSecret("signer", signerDER)
|
|
||||||
} else if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkeyGeneric, err := derhelpers.ParseEd25519PrivateKey(signerDER)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkey := pkeyGeneric.(ed25519.PrivateKey)
|
|
||||||
pubkey := pkey.Public().(ed25519.PublicKey)
|
|
||||||
|
|
||||||
return pkey, pubkey
|
|
||||||
}
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ func getSSHKeyPairs(host string) (pairs []SSHKeyPair, err error) {
|
|||||||
genLoop:
|
genLoop:
|
||||||
for _, keyType := range []string{
|
for _, keyType := range []string{
|
||||||
"rsa",
|
"rsa",
|
||||||
|
"dsa",
|
||||||
"ecdsa",
|
"ecdsa",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
} {
|
} {
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Simulate a udev run for our needs
|
|
||||||
func syncSysToDev() {
|
|
||||||
// loop devices
|
|
||||||
sysPaths, _ := filepath.Glob("/sys/devices/virtual/block/loop*/**/dev")
|
|
||||||
for _, sysPath := range sysPaths {
|
|
||||||
mknodBlk(sysPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mknodBlk(sysPath string) {
|
|
||||||
devPath := "/dev/" + filepath.Base(filepath.Dir(sysPath))
|
|
||||||
if _, err := os.Stat(devPath); os.IsNotExist(err) {
|
|
||||||
// ok
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("stat %s failed: %v", devPath, err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
return // exists
|
|
||||||
}
|
|
||||||
|
|
||||||
devBytes, err := os.ReadFile(sysPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("read %s failed: %v", sysPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
devBytes = bytes.TrimSpace(devBytes)
|
|
||||||
|
|
||||||
// rust: let Some(dev) = devBytes.split_once(':').filter_map(|a,b| Some(mkdev(a.parse().ok()?, b.parse().ok()?)));
|
|
||||||
majorStr, minorStr, ok := strings.Cut(string(devBytes), ":")
|
|
||||||
if !ok {
|
|
||||||
log.Printf("%s: invalid dev string: %s", sysPath, string(devBytes))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
major, err := strconv.ParseUint(majorStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s: invalid major: %q", sysPath, majorStr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
minor, err := strconv.ParseUint(minorStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s: invalid minor: %q", sysPath, minorStr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
devMajMin := int(unix.Mkdev(uint32(major), uint32(minor)))
|
|
||||||
|
|
||||||
log.Printf("mknod %s b %d %d", devPath, major, minor)
|
|
||||||
unix.Mknod(devPath, syscall.S_IFBLK|0o0600, devMajMin)
|
|
||||||
}
|
|
||||||
@ -9,11 +9,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
gopath "path"
|
gopath "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@ -24,22 +22,8 @@ var (
|
|||||||
upstreamURL = flag.String("upstream", "https://dkl.novit.io/dist", "Upstream server for dist elements")
|
upstreamURL = flag.String("upstream", "https://dkl.novit.io/dist", "Upstream server for dist elements")
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamServer struct{}
|
func (ctx *renderContext) distFetch(path ...string) (outPath string, err error) {
|
||||||
|
outPath = ctx.distFilePath(path...)
|
||||||
func (_ upstreamServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
path := path.Clean(req.URL.Path)
|
|
||||||
outPath, err := distFetch(strings.Split(path, "/")...)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
|
||||||
w.Write([]byte(err.Error() + "\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeFile(w, req, outPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func distFetch(path ...string) (outPath string, err error) {
|
|
||||||
outPath = distFilePath(path...)
|
|
||||||
|
|
||||||
if _, err = os.Stat(outPath); err == nil {
|
if _, err = os.Stat(outPath); err == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
)
|
|
||||||
|
|
||||||
var wgKeys = KVSecrets[string]{"wireguard"}
|
|
||||||
|
|
||||||
func wgKey(path string) (key string, err error) {
|
|
||||||
return wgKeys.GetOrCreate(path, func() (key string, err error) {
|
|
||||||
k, err := wgtypes.GeneratePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key = k.String()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func wgPSKey(path string) (key string, err error) {
|
|
||||||
return wgKeys.GetOrCreate(path, func() (key string, err error) {
|
|
||||||
k, err := wgtypes.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key = k.String()
|
|
||||||
return
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func wgPubKey(path string) (pubkey string, err error) {
|
|
||||||
key, err := wgKey(path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
k, err := wgtypes.ParseKey(key)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pubkey = k.PublicKey().String()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@ -1,32 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"m.cluseau.fr/go/httperr"
|
|
||||||
"novit.tech/direktil/pkg/localconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||||
cfg := &localconfig.Config{}
|
body := req.Request.Body
|
||||||
if err := req.ReadEntity(cfg); err != nil {
|
|
||||||
wsError(resp, httperr.BadRequest(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfgBytes, err := yaml.Marshal(cfg)
|
err := writeNewConfig(body)
|
||||||
if err != nil {
|
body.Close()
|
||||||
wsError(resp, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeNewConfig(cfgBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
return
|
return
|
||||||
@ -35,7 +23,7 @@ func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
|||||||
resp.WriteEntity(true)
|
resp.WriteEntity(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeNewConfig(cfgBytes []byte) (err error) {
|
func writeNewConfig(reader io.Reader) (err error) {
|
||||||
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -43,7 +31,7 @@ func writeNewConfig(cfgBytes []byte) (err error) {
|
|||||||
|
|
||||||
defer os.Remove(out.Name())
|
defer os.Remove(out.Name())
|
||||||
|
|
||||||
_, err = io.Copy(out, bytes.NewReader(cfgBytes))
|
_, err = io.Copy(out, reader)
|
||||||
out.Close()
|
out.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,292 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/base32"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
|
||||||
"github.com/pierrec/lz4"
|
|
||||||
"m.cluseau.fr/go/httperr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func globMatch(pattern, value string) bool {
|
|
||||||
ok, _ := filepath.Match(pattern, value)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadSet struct {
|
|
||||||
Expiry time.Time
|
|
||||||
Items []DownloadSetItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s DownloadSet) Contains(kind, name, asset string) bool {
|
|
||||||
for _, item := range s.Items {
|
|
||||||
if item.Kind == kind && globMatch(item.Name, name) &&
|
|
||||||
slices.Contains(item.Assets, asset) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s DownloadSet) Encode() string {
|
|
||||||
buf := new(strings.Builder)
|
|
||||||
s.EncodeTo(buf)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s DownloadSet) EncodeTo(buf *strings.Builder) {
|
|
||||||
buf.WriteString(strconv.FormatInt(s.Expiry.Unix(), 16))
|
|
||||||
|
|
||||||
for _, item := range s.Items {
|
|
||||||
buf.WriteByte('|')
|
|
||||||
item.EncodeTo(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DownloadSet) Decode(encoded string) (err error) {
|
|
||||||
exp, rem, _ := strings.Cut(encoded, "|")
|
|
||||||
|
|
||||||
expUnix, err := strconv.ParseInt(exp, 16, 64)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Expiry = time.Unix(expUnix, 0)
|
|
||||||
|
|
||||||
if rem == "" {
|
|
||||||
s.Items = nil
|
|
||||||
} else {
|
|
||||||
itemStrs := strings.Split(rem, "|")
|
|
||||||
s.Items = make([]DownloadSetItem, len(itemStrs))
|
|
||||||
for i, itemStr := range itemStrs {
|
|
||||||
s.Items[i].Decode(itemStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s DownloadSet) Signed(privKey ed25519.PrivateKey) string {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
{
|
|
||||||
setBytes := []byte(s.Encode())
|
|
||||||
|
|
||||||
w := lz4.NewWriter(buf)
|
|
||||||
w.Write(setBytes)
|
|
||||||
w.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
setBytes := buf.Bytes()
|
|
||||||
sig := ed25519.Sign(privKey, setBytes)
|
|
||||||
|
|
||||||
buf = bytes.NewBuffer(make([]byte, 0, 1+len(sig)+len(setBytes)))
|
|
||||||
buf.WriteByte(byte(len(sig)))
|
|
||||||
buf.Write(sig)
|
|
||||||
buf.Write(setBytes)
|
|
||||||
|
|
||||||
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
||||||
return enc.EncodeToString(buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadSetItem struct {
|
|
||||||
Kind string
|
|
||||||
Name string
|
|
||||||
Assets []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i DownloadSetItem) EncodeTo(buf *strings.Builder) {
|
|
||||||
kind := i.Kind
|
|
||||||
switch kind {
|
|
||||||
case "host":
|
|
||||||
kind = "h"
|
|
||||||
case "cluster":
|
|
||||||
kind = "c"
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(kind)
|
|
||||||
buf.WriteByte(':')
|
|
||||||
buf.WriteString(i.Name)
|
|
||||||
|
|
||||||
for _, asset := range i.Assets {
|
|
||||||
buf.WriteByte(':')
|
|
||||||
buf.WriteString(asset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *DownloadSetItem) Decode(encoded string) {
|
|
||||||
rem := encoded
|
|
||||||
i.Kind, rem, _ = strings.Cut(rem, ":")
|
|
||||||
|
|
||||||
switch i.Kind {
|
|
||||||
case "h":
|
|
||||||
i.Kind = "host"
|
|
||||||
case "c":
|
|
||||||
i.Kind = "cluster"
|
|
||||||
}
|
|
||||||
|
|
||||||
i.Name, rem, _ = strings.Cut(rem, ":")
|
|
||||||
|
|
||||||
if rem == "" {
|
|
||||||
i.Assets = nil
|
|
||||||
} else {
|
|
||||||
i.Assets = strings.Split(rem, ":")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadSetReq struct {
|
|
||||||
Expiry string
|
|
||||||
Items []DownloadSetItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsSignDownloadSet(req *restful.Request, resp *restful.Response) {
|
|
||||||
setReq := DownloadSetReq{}
|
|
||||||
if err := req.ReadEntity(&setReq); err != nil {
|
|
||||||
wsError(resp, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exp, err := parseCertDuration(setReq.Expiry, time.Now())
|
|
||||||
if err != nil {
|
|
||||||
wsError(resp, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
set := DownloadSet{
|
|
||||||
Expiry: exp,
|
|
||||||
Items: setReq.Items,
|
|
||||||
}
|
|
||||||
|
|
||||||
privKey, _ := dlsSigningKeys()
|
|
||||||
resp.WriteEntity(set.Signed(privKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDlSet(req *restful.Request) (*DownloadSet, *httperr.Error) {
|
|
||||||
setStr := req.QueryParameter("set")
|
|
||||||
|
|
||||||
setBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(setStr)
|
|
||||||
if err != nil {
|
|
||||||
err := httperr.BadRequest("invalid set")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(setBytes) == 0 {
|
|
||||||
err := httperr.BadRequest("invalid set")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
sigLen := int(setBytes[0])
|
|
||||||
setBytes = setBytes[1:]
|
|
||||||
|
|
||||||
if len(setBytes) < sigLen {
|
|
||||||
err := httperr.BadRequest("invalid set")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
sig := setBytes[:sigLen]
|
|
||||||
setBytes = setBytes[sigLen:]
|
|
||||||
|
|
||||||
_, pubkey := dlsSigningKeys()
|
|
||||||
if !ed25519.Verify(pubkey, setBytes, sig) {
|
|
||||||
err := httperr.BadRequest("invalid signature")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
setBytes, err = io.ReadAll(lz4.NewReader(bytes.NewBuffer(setBytes)))
|
|
||||||
if err != nil {
|
|
||||||
err := httperr.BadRequest("invalid data")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(string(setBytes))
|
|
||||||
|
|
||||||
set := DownloadSet{}
|
|
||||||
if err := set.Decode(string(setBytes)); err != nil {
|
|
||||||
err := httperr.BadRequest("invalid set: " + err.Error())
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(set.Expiry) {
|
|
||||||
err := httperr.BadRequest("set expired")
|
|
||||||
return nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &set, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsDownloadSetAsset(req *restful.Request, resp *restful.Response) {
|
|
||||||
set, err := getDlSet(req)
|
|
||||||
if err != nil {
|
|
||||||
wsError(resp, *err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := req.PathParameter("kind")
|
|
||||||
name := req.PathParameter("name")
|
|
||||||
asset := req.PathParameter("asset")
|
|
||||||
|
|
||||||
if !set.Contains(kind, name, asset) {
|
|
||||||
wsNotFound(resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadAsset(req, resp, kind, name, asset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
|
||||||
setStr := req.QueryParameter("set")
|
|
||||||
set, err := getDlSet(req)
|
|
||||||
if err != nil {
|
|
||||||
resp.WriteHeader(err.Status)
|
|
||||||
resp.Write([]byte(htmlHeader(err.Error())))
|
|
||||||
resp.Write([]byte(htmlFooter))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
buf.WriteString(htmlHeader("Download set"))
|
|
||||||
|
|
||||||
cfg, err2 := readConfig()
|
|
||||||
if err2 != nil {
|
|
||||||
wsError(resp, err2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range set.Items {
|
|
||||||
names := make([]string, 0)
|
|
||||||
switch item.Kind {
|
|
||||||
case "cluster":
|
|
||||||
for _, c := range cfg.Clusters {
|
|
||||||
if globMatch(item.Name, c.Name) {
|
|
||||||
names = append(names, c.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "host":
|
|
||||||
for _, h := range cfg.Hosts {
|
|
||||||
if globMatch(item.Name, h.Name) {
|
|
||||||
names = append(names, h.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
fmt.Fprintf(buf, "<h2>%s %s</h2>", strings.Title(item.Kind), name)
|
|
||||||
fmt.Fprintf(buf, "<p class=\"download-links\">\n")
|
|
||||||
for _, asset := range item.Assets {
|
|
||||||
fmt.Fprintf(buf, " <a href=\"/public/download-set/%s/%s/%s?set=%s\" download>%s</a>\n", item.Kind, name, asset, setStr, asset)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(buf, `</p>`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(htmlFooter)
|
|
||||||
buf.WriteTo(resp)
|
|
||||||
}
|
|
||||||
@ -1,14 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
@ -56,7 +53,7 @@ func wsAuthorizeDownload(req *restful.Request, resp *restful.Response) {
|
|||||||
resp.WriteAsJson(token)
|
resp.WriteAsJson(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsDownloadAsset(req *restful.Request, resp *restful.Response) {
|
func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||||
token := req.PathParameter("token")
|
token := req.PathParameter("token")
|
||||||
asset := req.PathParameter("asset")
|
asset := req.PathParameter("asset")
|
||||||
|
|
||||||
@ -105,10 +102,6 @@ func wsDownloadAsset(req *restful.Request, resp *restful.Response) {
|
|||||||
|
|
||||||
log.Printf("download via token: %s %q asset %q", spec.Kind, spec.Name, asset)
|
log.Printf("download via token: %s %q asset %q", spec.Kind, spec.Name, asset)
|
||||||
|
|
||||||
downloadAsset(req, resp, spec.Kind, spec.Name, asset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadAsset(req *restful.Request, resp *restful.Response, kind, name, asset string) {
|
|
||||||
cfg, err := readConfig()
|
cfg, err := readConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
@ -116,12 +109,12 @@ func downloadAsset(req *restful.Request, resp *restful.Response, kind, name, ass
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHeader := func(ext string) {
|
setHeader := func(ext string) {
|
||||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(kind+"_"+name+"_"+asset+ext))
|
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(spec.Kind+"_"+spec.Name+"_"+asset+ext))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch kind {
|
switch spec.Kind {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
cluster := cfg.ClusterByName(name)
|
cluster := cfg.ClusterByName(spec.Name)
|
||||||
if cluster == nil {
|
if cluster == nil {
|
||||||
wsNotFound(resp)
|
wsNotFound(resp)
|
||||||
return
|
return
|
||||||
@ -137,7 +130,7 @@ func downloadAsset(req *restful.Request, resp *restful.Response, kind, name, ass
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "host":
|
case "host":
|
||||||
host := hostOrTemplate(cfg, name)
|
host := hostOrTemplate(cfg, spec.Name)
|
||||||
if host == nil {
|
if host == nil {
|
||||||
wsNotFound(resp)
|
wsNotFound(resp)
|
||||||
return
|
return
|
||||||
@ -156,44 +149,3 @@ func downloadAsset(req *restful.Request, resp *restful.Response, kind, name, ass
|
|||||||
wsNotFound(resp)
|
wsNotFound(resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsDownload(req *restful.Request, resp *restful.Response) {
|
|
||||||
if strings.HasSuffix(req.Request.URL.Path, "/") {
|
|
||||||
wsDownloadPage(req, resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token := req.PathParameter("token")
|
|
||||||
|
|
||||||
spec, ok := wState.Get().Downloads[token]
|
|
||||||
if !ok {
|
|
||||||
wsNotFound(resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.WriteEntity(spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsDownloadPage(req *restful.Request, resp *restful.Response) {
|
|
||||||
token := req.PathParameter("token")
|
|
||||||
|
|
||||||
spec, ok := wState.Get().Downloads[token]
|
|
||||||
if !ok {
|
|
||||||
resp.WriteHeader(http.StatusNotFound)
|
|
||||||
resp.Write([]byte(htmlHeader("Token not found")))
|
|
||||||
resp.Write([]byte(htmlFooter))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
buf.WriteString(htmlHeader(fmt.Sprintf("Token assets: %s %s", spec.Kind, spec.Name)))
|
|
||||||
|
|
||||||
buf.WriteString("<ul>")
|
|
||||||
for _, asset := range spec.Assets {
|
|
||||||
fmt.Fprintf(buf, "<li><a href=\"%s\" download>%s</a></li>\n", asset, asset)
|
|
||||||
}
|
|
||||||
buf.WriteString("</ul>")
|
|
||||||
|
|
||||||
buf.WriteString(htmlFooter)
|
|
||||||
buf.WriteTo(resp)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ func (hft HostFromTemplate) ClusterName(cfg *localconfig.Config) string {
|
|||||||
func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Host) {
|
func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Host) {
|
||||||
host = cfg.Host(name)
|
host = cfg.Host(name)
|
||||||
if host != nil {
|
if host != nil {
|
||||||
|
log.Print("no host named ", name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,13 +39,13 @@ func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Hos
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
log.Print("no host named ", name)
|
log.Print("no host from template named ", name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ht := cfg.HostTemplate(hft.Template)
|
ht := cfg.HostTemplate(hft.Template)
|
||||||
if ht == nil {
|
if ht == nil {
|
||||||
log.Print("host ", name, " found but no template named ", hft.Template)
|
log.Print("no host template named ", name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||||
"github.com/emicklei/go-restful"
|
"github.com/emicklei/go-restful"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"m.cluseau.fr/go/httperr"
|
"m.cluseau.fr/go/httperr"
|
||||||
|
|
||||||
"novit.tech/direktil/pkg/localconfig"
|
"novit.tech/direktil/pkg/localconfig"
|
||||||
@ -38,13 +37,10 @@ func registerWS(rest *restful.Container) {
|
|||||||
Route(ws.POST("/store.tar").To(wsStoreUpload).
|
Route(ws.POST("/store.tar").To(wsStoreUpload).
|
||||||
Consumes(mime.TAR).
|
Consumes(mime.TAR).
|
||||||
Doc("Upload an existing store")).
|
Doc("Upload an existing store")).
|
||||||
Route(ws.GET("/downloads/{token}").To(wsDownload)).
|
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
|
||||||
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownloadAsset).
|
|
||||||
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")).
|
||||||
Doc("Fetch an asset via a download token")).
|
Doc("Fetch an asset via a download token"))
|
||||||
Route(ws.GET("/download-set").To(wsDownloadSet)).
|
|
||||||
Route(ws.GET("/download-set/{kind}/{name}/{asset}").To(wsDownloadSetAsset))
|
|
||||||
|
|
||||||
rest.Add(ws)
|
rest.Add(ws)
|
||||||
}
|
}
|
||||||
@ -69,14 +65,10 @@ func registerWS(rest *restful.Container) {
|
|||||||
Consumes(mime.JSON).Reads(DownloadSpec{}).
|
Consumes(mime.JSON).Reads(DownloadSpec{}).
|
||||||
Produces(mime.JSON).
|
Produces(mime.JSON).
|
||||||
Doc("Create a download token for the given download"))
|
Doc("Create a download token for the given download"))
|
||||||
ws.Route(ws.POST("/sign-download-set").To(wsSignDownloadSet).
|
|
||||||
Consumes(mime.JSON).Reads(DownloadSetReq{}).
|
|
||||||
Produces(mime.JSON).
|
|
||||||
Doc("Sign a download set"))
|
|
||||||
|
|
||||||
// - configs API
|
// - configs API
|
||||||
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
||||||
Consumes(mime.YAML, mime.JSON).Param(ws.BodyParameter("config", "The new full configuration")).
|
Consumes(mime.YAML).Param(ws.BodyParameter("config", "The new full configuration")).
|
||||||
Produces(mime.JSON).Writes(true).
|
Produces(mime.JSON).Writes(true).
|
||||||
Doc("Upload a new current configuration, archiving the previous one"))
|
Doc("Upload a new current configuration, archiving the previous one"))
|
||||||
|
|
||||||
@ -308,26 +300,3 @@ func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
restful.RegisterEntityAccessor(mime.YAML, yamlEntityAccessor{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type yamlEntityAccessor struct{}
|
|
||||||
|
|
||||||
var _ restful.EntityReaderWriter = yamlEntityAccessor{}
|
|
||||||
|
|
||||||
func (_ yamlEntityAccessor) Read(req *restful.Request, v any) error {
|
|
||||||
decoder := yaml.NewDecoder(req.Request.Body)
|
|
||||||
return decoder.Decode(v)
|
|
||||||
}
|
|
||||||
func (_ yamlEntityAccessor) Write(resp *restful.Response, status int, v any) error {
|
|
||||||
if v == nil {
|
|
||||||
resp.WriteHeader(status)
|
|
||||||
// do not write a nil representation
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp.Header().Set("Content-Type", mime.YAML)
|
|
||||||
resp.WriteHeader(status)
|
|
||||||
return yaml.NewEncoder(resp.ResponseWriter).Encode(v)
|
|
||||||
}
|
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -11,15 +11,12 @@ require (
|
|||||||
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.16.2
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
github.com/klauspost/compress v1.18.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
|
||||||
github.com/sergeymakinen/go-crypt v1.0.1
|
github.com/sergeymakinen/go-crypt v1.0.1
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/sys v0.33.0
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
|
||||||
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
|
||||||
@ -73,6 +70,7 @@ require (
|
|||||||
github.com/zmap/zlint/v3 v3.5.0 // indirect
|
github.com/zmap/zlint/v3 v3.5.0 // indirect
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -128,8 +128,6 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
|
|||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 h1:/pb3UJ+3ZtSEUKWnufwsoVF7f0AX5ytPULbTwHMgbq4=
|
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 h1:/pb3UJ+3ZtSEUKWnufwsoVF7f0AX5ytPULbTwHMgbq4=
|
||||||
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@ -311,8 +309,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
GIT_TAG=$(git describe --always --dirty)
|
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
commit) tag=$GIT_TAG ;;
|
commit) tag=$(git describe --always --dirty) ;;
|
||||||
"") tag=latest ;;
|
"") tag=latest ;;
|
||||||
*) tag=$1 ;;
|
*) tag=$1 ;;
|
||||||
esac
|
esac
|
||||||
docker build -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG
|
docker build -t novit.tech/direktil/local-server:$tag .
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads, .download-links {
|
.downloads {
|
||||||
& > * {
|
& > * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 1ex;
|
margin-right: 1ex;
|
||||||
@ -20,10 +20,18 @@
|
|||||||
border-radius: 1ex;
|
border-radius: 1ex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.downloads, .view-links {
|
|
||||||
& > .selected {
|
& > .selected {
|
||||||
color: var(--link);
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-links a {
|
||||||
|
margin-right: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.downloads > .selected,
|
||||||
|
.view-links > .selected {
|
||||||
|
color: #31b0fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div style="float:right;"><input type="search" placeholder="Filter" v-model="viewFilter"/></div>
|
<div style="float:right;"><input type="text" placeholder="Filter" v-model="viewFilter"/></div>
|
||||||
<p class="view-links"><span v-for="v in views" @click="view = v" :class="{selected: view.type==v.type && view.name==v.name}">{{v.title}}</span></p>
|
<p class="view-links"><span v-for="v in views" @click="view = v" :class="{selected: view.type==v.type && view.name==v.name}">{{v.title}}</span></p>
|
||||||
|
|
||||||
<h2 v-if="view">{{view.title}}</h2>
|
<h2 v-if="view">{{view.title}}</h2>
|
||||||
@ -112,9 +112,9 @@
|
|||||||
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template v-if="any(state.HostTemplates) || any(hostsFromTemplate)">
|
<template v-if="state.HostTemplates && state.HostTemplates.length">
|
||||||
<h3>Hosts from template</h3>
|
<h3>Hosts from template</h3>
|
||||||
<form @submit="hostFromTemplateAdd" action="" v-if="any(state.HostTemplates)">
|
<form @submit="hostFromTemplateAdd" action="">
|
||||||
<p>Add a host from template instance:</p>
|
<p>Add a host from template instance:</p>
|
||||||
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
||||||
<select v-model="forms.hostFromTemplate.Template" required>
|
<select v-model="forms.hostFromTemplate.Template" required>
|
||||||
@ -123,7 +123,7 @@
|
|||||||
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
||||||
<input type="submit" value="add instance" />
|
<input type="submit" value="add instance" />
|
||||||
</form>
|
</form>
|
||||||
<form @submit="hostFromTemplateDel" action="" v-if="any(hostsFromTemplate)">
|
<form @submit="hostFromTemplateDel" action="">
|
||||||
<p>Remove a host from template instance:</p>
|
<p>Remove a host from template instance:</p>
|
||||||
<select v-model="forms.hostFromTemplateDel" required>
|
<select v-model="forms.hostFromTemplateDel" required>
|
||||||
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
||||||
|
|||||||
@ -27,15 +27,16 @@ export default {
|
|||||||
}[this.kind]
|
}[this.kind]
|
||||||
},
|
},
|
||||||
downloads() {
|
downloads() {
|
||||||
return Object.entries(this.state.Downloads)
|
let ret = []
|
||||||
|
Object.entries(this.state.Downloads)
|
||||||
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
||||||
.map(e => {
|
.forEach(e => {
|
||||||
const token= e[0];
|
let token= e[0], d = e[1]
|
||||||
return {
|
d.Assets.forEach(asset => {
|
||||||
text: token.substring(0, 5) + '...',
|
ret.push({name: asset, url: '/public/downloads/'+token+'/'+asset})
|
||||||
url: '/public/downloads/'+token+"/",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
return ret
|
||||||
},
|
},
|
||||||
assets() {
|
assets() {
|
||||||
return this.availableAssets.filter(a => this.selectedAssets[a])
|
return this.availableAssets.filter(a => this.selectedAssets[a])
|
||||||
@ -63,9 +64,9 @@ export default {
|
|||||||
{{" "}}
|
{{" "}}
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
<p><button :disabled="createDisabled || assets.length==0" @click="createToken">Create link</button></p>
|
<p><button :disabled="createDisabled || assets.length==0" @click="createToken">Create links</button></p>
|
||||||
<template v-if="downloads.length">
|
<template v-if="downloads.length">
|
||||||
<h4>Active links</h4>
|
<h4>Active links</h4>
|
||||||
<p class="download-links"><template v-for="d in downloads"><a :href="d.url" target="_blank">{{ d.text }}</a>{{" "}}</template></p>
|
<p class="download-links"><template v-for="d in downloads"><a :href="d.url" download>{{ d.name }}</a>{{" "}}</template></p>
|
||||||
</template>`
|
</template>`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,14 +79,11 @@ createApp({
|
|||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
hostsFromTemplate() {
|
hostsFromTemplate() {
|
||||||
return (this.state.Hosts||[]).filter((h) => h.Template);
|
return (this.state.Hosts||[]).filter((h) => h.Template)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
any(array) {
|
|
||||||
return array && array.length != 0;
|
|
||||||
},
|
|
||||||
copyText(text) {
|
copyText(text) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
window.navigator.clipboard.writeText(text)
|
window.navigator.clipboard.writeText(text)
|
||||||
|
|||||||
@ -1,30 +1,5 @@
|
|||||||
:root {
|
|
||||||
--bg: #eee;
|
|
||||||
--color: black;
|
|
||||||
--bevel-dark: darkgray;
|
|
||||||
--bevel-light: lightgray;
|
|
||||||
--link: blue;
|
|
||||||
--input-bg: #ddd;
|
|
||||||
--input-text: white;
|
|
||||||
--btn-bg: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: black;
|
|
||||||
--color: orange;
|
|
||||||
--bevel-dark: #402900;
|
|
||||||
--bevel-light: #805300;
|
|
||||||
--link: #31b0fa;
|
|
||||||
--input-bg: #111;
|
|
||||||
--input-text: #ddd;
|
|
||||||
--btn-bg: #222;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--bg);
|
background: white;
|
||||||
color: var(--color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] {
|
button[disabled] {
|
||||||
@ -33,7 +8,7 @@ button[disabled] {
|
|||||||
|
|
||||||
a[href], a[href]:visited, button.link {
|
a[href], a[href]:visited, button.link {
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--link);
|
color: blue;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -62,40 +37,22 @@ th, tr:last-child > td {
|
|||||||
.red { color: red; }
|
.red { color: red; }
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
button, input[type=submit] {
|
||||||
|
background: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
a[href], a[href]:visited, button.link {
|
||||||
|
border: none;
|
||||||
|
color: #31b0fa;
|
||||||
|
}
|
||||||
|
|
||||||
.red { color: #c00; }
|
.red { color: #c00; }
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea, select, input {
|
|
||||||
background: var(--input-bg);
|
|
||||||
color: var(--input-text);
|
|
||||||
border: solid 1pt;
|
|
||||||
border-color: var(--bevel-light);
|
|
||||||
border-top-color: var(--bevel-dark);
|
|
||||||
border-left-color: var(--bevel-dark);
|
|
||||||
margin: 1pt;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: solid 1pt var(--color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button, input[type=button], input[type=submit], ::file-selector-button {
|
|
||||||
background: var(--btn-bg);
|
|
||||||
color: var(--color);
|
|
||||||
border: solid 2pt;
|
|
||||||
border-color: var(--bevel-dark);
|
|
||||||
border-top-color: var(--bevel-light);
|
|
||||||
border-left-color: var(--bevel-light);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--bevel-dark);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: var(--bevel-dark);
|
|
||||||
border-color: var(--bevel-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -1,23 +1,37 @@
|
|||||||
#! /bin/sh
|
#! /bin/sh
|
||||||
|
|
||||||
if [ $# -ne 2 ]; then
|
if [ $# -ne 2 ]; then
|
||||||
echo "USAGE: $0 <device>"
|
echo "USAGE: $0 <device> <base url>"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dev=$1
|
dev=$1
|
||||||
|
base_url=$2
|
||||||
|
|
||||||
|
: ${MP:=/mnt}
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
zcat boot.img.gz | dd of=$dev
|
mkdir -p $MP
|
||||||
|
|
||||||
apk add sgdisk
|
|
||||||
|
|
||||||
[[ $dev =~ nvme ]] &&
|
[[ $dev =~ nvme ]] &&
|
||||||
devp=${dev}p ||
|
devp=${dev}p ||
|
||||||
devp=${dev}
|
devp=${dev}
|
||||||
|
|
||||||
sgdisk --move-second-header --new=3:0:0 $dev
|
if vgdisplay storage; then
|
||||||
|
# the system is already installed, just upgrade
|
||||||
|
mount -t vfat ${devp}1 $MP
|
||||||
|
curl ${base_url}/boot.tar |tar xv -C $MP
|
||||||
|
umount $MP
|
||||||
|
|
||||||
pvcreate ${devp}3
|
else
|
||||||
vgcreate storage ${devp}3
|
sgdisk --clear $dev
|
||||||
|
|
||||||
|
curl ${base_url}/boot.img.lz4 |lz4cat >$dev
|
||||||
|
|
||||||
|
sgdisk --move-second-header --new=3:0:0 $dev
|
||||||
|
|
||||||
|
pvcreate ${devp}3
|
||||||
|
vgcreate storage ${devp}3
|
||||||
|
fi
|
||||||
|
|
||||||
|
while umount $MP; do true; done
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package clustersconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -177,7 +178,7 @@ type dirStore struct {
|
|||||||
|
|
||||||
// listDir
|
// listDir
|
||||||
func (b *dirStore) listDir(prefix string) (subDirs []string, err error) {
|
func (b *dirStore) listDir(prefix string) (subDirs []string, err error) {
|
||||||
entries, err := os.ReadDir(filepath.Join(b.path, prefix))
|
entries, err := ioutil.ReadDir(filepath.Join(b.path, prefix))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -225,7 +226,7 @@ func (b *dirStore) List(prefix string) ([]string, error) {
|
|||||||
|
|
||||||
// Load is part of the DataBackend interface
|
// Load is part of the DataBackend interface
|
||||||
func (b *dirStore) Get(key string) (ba []byte, err error) {
|
func (b *dirStore) Get(key string) (ba []byte, err error) {
|
||||||
ba, err = os.ReadFile(filepath.Join(b.path, filepath.Join(path.Split(key))+".yaml"))
|
ba, err = ioutil.ReadFile(filepath.Join(b.path, filepath.Join(path.Split(key))+".yaml"))
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user