Compare commits
6 Commits
5fa367949b
...
main
Author | SHA1 | Date | |
---|---|---|---|
8ae52501c9 | |||
98eb601fd3 | |||
8e87d406e4 | |||
f83b1eab23 | |||
d03a7ab4ec | |||
cd69d9234e |
@ -1,6 +1,6 @@
|
||||
from novit.tech/direktil/dkl:bbea9b9 as dkl
|
||||
# ------------------------------------------------------------------------
|
||||
from golang:1.24.4-bookworm as build
|
||||
from golang:1.25.0-trixie as build
|
||||
|
||||
run apt-get update && apt-get install -y git
|
||||
|
||||
@ -22,13 +22,13 @@ run \
|
||||
hack/build ./...
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
from debian:bookworm
|
||||
from debian:trixie
|
||||
entrypoint ["/bin/dkl-local-server"]
|
||||
|
||||
env _uncache=1
|
||||
run apt-get update \
|
||||
&& 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 \
|
||||
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \
|
||||
&& apt-get clean
|
||||
|
||||
copy --from=dkl /bin/dkl /bin/dls /bin/
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"math/rand"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
@ -290,6 +291,14 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
|
||||
"host_download_token": func() (s string) {
|
||||
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 = make([]any, 0)
|
||||
|
@ -60,6 +60,14 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
||||
|
||||
return map[string]any{
|
||||
"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) {
|
||||
key := cluster + "/" + name
|
||||
@ -203,7 +211,7 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func asYaml(v interface{}) (string, error) {
|
||||
func asYaml(v any) (string, error) {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
15
cmd/dkl-local-server/html.go
Normal file
15
cmd/dkl-local-server/html.go
Normal file
@ -0,0 +1,15 @@
|
||||
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>`
|
@ -14,6 +14,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
@ -239,6 +240,46 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
|
||||
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
|
||||
}
|
||||
|
44
cmd/dkl-local-server/wireguard.go
Normal file
44
cmd/dkl-local-server/wireguard.go
Normal file
@ -0,0 +1,44 @@
|
||||
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
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -16,6 +17,11 @@ import (
|
||||
"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
|
||||
@ -23,7 +29,7 @@ type DownloadSet struct {
|
||||
|
||||
func (s DownloadSet) Contains(kind, name, asset string) bool {
|
||||
for _, item := range s.Items {
|
||||
if item.Kind == kind && item.Name == name &&
|
||||
if item.Kind == kind && globMatch(item.Name, name) &&
|
||||
slices.Contains(item.Assets, asset) {
|
||||
return true
|
||||
}
|
||||
@ -69,6 +75,28 @@ func (s *DownloadSet) Decode(encoded string) (err error) {
|
||||
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
|
||||
@ -76,7 +104,15 @@ type DownloadSetItem struct {
|
||||
}
|
||||
|
||||
func (i DownloadSetItem) EncodeTo(buf *strings.Builder) {
|
||||
buf.WriteString(i.Kind)
|
||||
kind := i.Kind
|
||||
switch kind {
|
||||
case "host":
|
||||
kind = "h"
|
||||
case "cluster":
|
||||
kind = "c"
|
||||
}
|
||||
|
||||
buf.WriteString(kind)
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(i.Name)
|
||||
|
||||
@ -89,6 +125,14 @@ func (i DownloadSetItem) EncodeTo(buf *strings.Builder) {
|
||||
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 == "" {
|
||||
@ -121,32 +165,8 @@ func wsSignDownloadSet(req *restful.Request, resp *restful.Response) {
|
||||
Items: setReq.Items,
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
{
|
||||
setBytes := []byte(set.Encode())
|
||||
|
||||
w := lz4.NewWriter(buf)
|
||||
w.Write(setBytes)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
setBytes := buf.Bytes()
|
||||
|
||||
privkey, pubkey := dlsSigningKeys()
|
||||
sig := ed25519.Sign(privkey, setBytes)
|
||||
|
||||
if !ed25519.Verify(pubkey, setBytes, sig) {
|
||||
wsError(resp, fmt.Errorf("signature self-check failed"))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
resp.WriteEntity(enc.EncodeToString(buf.Bytes()))
|
||||
privKey, _ := dlsSigningKeys()
|
||||
resp.WriteEntity(set.Signed(privKey))
|
||||
}
|
||||
|
||||
func getDlSet(req *restful.Request) (*DownloadSet, *httperr.Error) {
|
||||
@ -226,42 +246,47 @@ func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
||||
set, err := getDlSet(req)
|
||||
if err != nil {
|
||||
resp.WriteHeader(err.Status)
|
||||
resp.Write([]byte(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + err.Error() + `</title>
|
||||
<style>
|
||||
@import url('/ui/style.css');
|
||||
@import url('/ui/app.css');
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>` + err.Error() + `</h1></body>
|
||||
</html>`))
|
||||
resp.Write([]byte(htmlHeader(err.Error())))
|
||||
resp.Write([]byte(htmlFooter))
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Download set</title>
|
||||
<style>
|
||||
@import url('/ui/style.css');
|
||||
@import url('/ui/app.css');
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Download set</h1>
|
||||
`)
|
||||
buf.WriteString(htmlHeader("Download set"))
|
||||
|
||||
for _, item := range set.Items {
|
||||
fmt.Fprintf(buf, "<h2>%s %s</h2>", strings.Title(item.Kind), item.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, item.Name, asset, setStr, asset)
|
||||
}
|
||||
fmt.Fprintf(buf, `</p>`)
|
||||
cfg, err2 := readConfig()
|
||||
if err2 != nil {
|
||||
wsError(resp, err2)
|
||||
return
|
||||
}
|
||||
|
||||
buf.WriteString("</body></html>")
|
||||
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)
|
||||
}
|
||||
|
@ -180,38 +180,20 @@ func wsDownloadPage(req *restful.Request, resp *restful.Response) {
|
||||
spec, ok := wState.Get().Downloads[token]
|
||||
if !ok {
|
||||
resp.WriteHeader(http.StatusNotFound)
|
||||
resp.Write([]byte(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Token not found</title>
|
||||
<style>
|
||||
@import url('/ui/style.css');
|
||||
@import url('/ui/app.css');
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Token not found</h1></body>
|
||||
</html>`))
|
||||
resp.Write([]byte(htmlHeader("Token not found")))
|
||||
resp.Write([]byte(htmlFooter))
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Token assets: %s %s</title>
|
||||
<style>
|
||||
@import url('/ui/style.css');
|
||||
@import url('/ui/app.css');
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Token assets: %s %s</h1>
|
||||
<ul>
|
||||
`, spec.Kind, spec.Name, spec.Kind, spec.Name)
|
||||
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("</ul></body></html>")
|
||||
buf.WriteString(htmlFooter)
|
||||
buf.WriteTo(resp)
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ func (hft HostFromTemplate) ClusterName(cfg *localconfig.Config) string {
|
||||
func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Host) {
|
||||
host = cfg.Host(name)
|
||||
if host != nil {
|
||||
log.Print("no host named ", name)
|
||||
return
|
||||
}
|
||||
|
||||
@ -39,13 +38,13 @@ func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Hos
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Print("no host from template named ", name)
|
||||
log.Print("no host named ", name)
|
||||
return
|
||||
}
|
||||
|
||||
ht := cfg.HostTemplate(hft.Template)
|
||||
if ht == nil {
|
||||
log.Print("no host template named ", name)
|
||||
log.Print("host ", name, " found but no template named ", hft.Template)
|
||||
return
|
||||
}
|
||||
|
||||
|
5
go.mod
5
go.mod
@ -11,12 +11,15 @@ require (
|
||||
github.com/emicklei/go-restful v2.16.0+incompatible
|
||||
github.com/emicklei/go-restful-openapi v1.4.1
|
||||
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/miolini/datacounter v1.0.3
|
||||
github.com/oklog/ulid v1.3.1
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible
|
||||
github.com/sergeymakinen/go-crypt v1.0.1
|
||||
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-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@ -55,7 +58,6 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@ -71,7 +73,6 @@ require (
|
||||
github.com/zmap/zlint/v3 v3.5.0 // indirect
|
||||
golang.org/x/mod v0.25.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
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -3,8 +3,6 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@ -313,6 +311,8 @@ 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
|
@ -1,8 +1,11 @@
|
||||
#! /bin/bash
|
||||
set -ex
|
||||
|
||||
GIT_TAG=$(git describe --always --dirty)
|
||||
|
||||
case "$1" in
|
||||
commit) tag=$(git describe --always --dirty) ;;
|
||||
commit) tag=$GIT_TAG ;;
|
||||
"") tag=latest ;;
|
||||
*) tag=$1 ;;
|
||||
esac
|
||||
docker build -t novit.tech/direktil/local-server:$tag .
|
||||
docker build -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG
|
||||
|
@ -20,14 +20,10 @@
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
& > .selected {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.downloads > .selected,
|
||||
.view-links > .selected {
|
||||
color: #31b0fa;
|
||||
.downloads, .view-links {
|
||||
& > .selected {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div style="float:right;"><input type="text" placeholder="Filter" v-model="viewFilter"/></div>
|
||||
<div style="float:right;"><input type="search" 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>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<template v-if="state.HostTemplates && state.HostTemplates.length">
|
||||
<template v-if="any(state.HostTemplates) || any(hostsFromTemplate)">
|
||||
<h3>Hosts from template</h3>
|
||||
<form @submit="hostFromTemplateAdd" action="">
|
||||
<form @submit="hostFromTemplateAdd" action="" v-if="any(state.HostTemplates)">
|
||||
<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>
|
||||
@ -123,7 +123,7 @@
|
||||
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
||||
<input type="submit" value="add instance" />
|
||||
</form>
|
||||
<form @submit="hostFromTemplateDel" action="">
|
||||
<form @submit="hostFromTemplateDel" action="" v-if="any(hostsFromTemplate)">
|
||||
<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>
|
||||
|
@ -79,11 +79,14 @@ createApp({
|
||||
return undefined;
|
||||
},
|
||||
hostsFromTemplate() {
|
||||
return (this.state.Hosts||[]).filter((h) => h.Template)
|
||||
return (this.state.Hosts||[]).filter((h) => h.Template);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
any(array) {
|
||||
return array && array.length != 0;
|
||||
},
|
||||
copyText(text) {
|
||||
event.preventDefault()
|
||||
window.navigator.clipboard.writeText(text)
|
||||
|
@ -1,5 +1,30 @@
|
||||
: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 {
|
||||
background: white;
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
@ -8,7 +33,7 @@ button[disabled] {
|
||||
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: blue;
|
||||
color: var(--link);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
@ -37,30 +62,38 @@ th, tr:last-child > td {
|
||||
.red { color: red; }
|
||||
|
||||
@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; }
|
||||
}
|
||||
|
||||
textarea, input[type=text] {
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
border: dotted 1pt;
|
||||
border-top-color: #805300;
|
||||
border-left-color: #805300;
|
||||
border-bottom-color: orange;
|
||||
border-right-color: orange;
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user