2 Commits

Author SHA1 Message Date
83a866133d sign bootstrap.tar content 2025-07-06 11:11:36 +02:00
5e285e9b36 add public key template functions 2025-07-06 10:18:54 +02:00
58 changed files with 577 additions and 18200 deletions

View File

@ -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/

View File

@ -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) }

View File

@ -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
} }
@ -292,14 +290,6 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
"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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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() {

View File

@ -51,7 +51,7 @@ func buildBootISO(out io.Writer, ctx *renderContext) (err error) {
} }
// create a tag file // create a tag file
bootstrapBytes, err := ctx.BootstrapConfig() bootstrapBytes, _, err := ctx.BootstrapConfig()
if err != nil { if err != nil {
return return
} }

View File

@ -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
} }

View File

@ -4,27 +4,34 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"crypto" "crypto"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
"github.com/klauspost/compress/zstd" yaml "gopkg.in/yaml.v2"
"novit.tech/direktil/pkg/cpiocat" "novit.tech/direktil/pkg/cpiocat"
) )
func renderBootstrapConfig(w http.ResponseWriter, ctx *renderContext) (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)
ba, err := ctx.BootstrapConfig() _, cfg, err := ctx.BootstrapConfig()
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write(ba) if asJson {
return err = json.NewEncoder(w).Encode(cfg)
} else {
err = yaml.NewEncoder(w).Encode(cfg)
}
return nil
} }
func buildInitrd(out io.Writer, ctx *renderContext) (err error) { func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
@ -33,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
} }
@ -51,11 +53,9 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
for _, layer := range cfg.Layers { for _, layer := range cfg.Layers {
switch layer { switch layer {
case "modules": case "modules":
layerVersion := ctx.Host.Versions[layer] layerVersion := ctx.Host.Versions[layer]
if layerVersion == "" { modulesPath, err := ctx.distFetch("layers", layer, layerVersion)
layerVersion = ctx.Host.Kernel
}
modulesPath, err := distFetch("layers", layer, layerVersion)
if err != nil { if err != nil {
return err return err
} }
@ -65,7 +65,7 @@ func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
} }
// config // config
cfgBytes, err := ctx.BootstrapConfig() cfgBytes, _, err := ctx.BootstrapConfig()
if err != nil { if err != nil {
return return
} }
@ -76,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)
} }
@ -89,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) {
@ -121,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,
@ -174,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
} }

View File

@ -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
@ -117,12 +109,7 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
return return
} }
extra, err := caExtraCerts(cluster, name) s = string(ca.Cert)
if err != nil {
return
}
s = string(ca.Cert) + extra
return return
}, },
@ -132,18 +119,13 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
return return
} }
extra, err := caExtraCerts(cluster, name)
if err != nil {
return
}
dir := "/etc/tls-ca/" + name dir := "/etc/tls-ca/" + name
return asYaml([]config.FileDef{ return asYaml([]config.FileDef{
{ {
Path: path.Join(dir, "ca.crt"), Path: path.Join(dir, "ca.crt"),
Mode: 0644, Mode: 0644,
Content: string(ca.Cert) + extra, Content: string(ca.Cert),
}, },
{ {
Path: path.Join(dir, "ca.key"), Path: path.Join(dir, "ca.key"),
@ -169,7 +151,7 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
return return
} }
ba, err := x509.MarshalPKIXPublicKey(priv.Public()) ba, err := x509.MarshalPKIXPublicKey(priv)
if err != nil { if err != nil {
err = fmt.Errorf("marshal public key failed: %w", err) err = fmt.Errorf("marshal public key failed: %w", err)
return return
@ -188,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

View File

@ -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>`

View File

@ -4,12 +4,14 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
yaml "gopkg.in/yaml.v2"
) )
func renderConfig(w http.ResponseWriter, _ *http.Request, ctx *renderContext, asJson bool) (err error) { func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
log.Printf("sending config for %q", ctx.Host.Name) log.Printf("sending config for %q", ctx.Host.Name)
cfgBytes, cfg, err := ctx.Config() _, cfg, err := ctx.Config()
if err != nil { if err != nil {
return err return err
} }
@ -17,7 +19,7 @@ func renderConfig(w http.ResponseWriter, _ *http.Request, ctx *renderContext, as
if asJson { if asJson {
err = json.NewEncoder(w).Encode(cfg) err = json.NewEncoder(w).Encode(cfg)
} else { } else {
_, err = w.Write(cfgBytes) err = yaml.NewEncoder(w).Encode(cfg)
} }
return nil return nil

View File

@ -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
} }

View File

@ -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)))

View File

@ -4,9 +4,7 @@ import (
"bytes" "bytes"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -15,17 +13,16 @@ 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"
"novit.tech/direktil/pkg/config" "novit.tech/direktil/pkg/config"
"novit.tech/direktil/pkg/localconfig" "novit.tech/direktil/pkg/localconfig"
bsconfig "novit.tech/direktil/pkg/bootstrapconfig"
) )
var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition") var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition")
@ -100,8 +97,6 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
return return
} }
// log.Print("rendered config:\n", string(ba))
cfg = &config.Config{} cfg = &config.Config{}
if err = yaml.Unmarshal(ba, cfg); err != nil { if err = yaml.Unmarshal(ba, cfg); err != nil {
return return
@ -110,8 +105,19 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
return return
} }
func (ctx *renderContext) BootstrapConfig() (ba []byte, err error) { func (ctx *renderContext) BootstrapConfig() (ba []byte, cfg *bsconfig.Config, err error) {
return ctx.render(ctx.Host.BootstrapConfig) ba, err = ctx.render(ctx.Host.BootstrapConfig)
if err != nil {
return
}
cfg = &bsconfig.Config{}
if err = yaml.Unmarshal(ba, cfg); err != nil {
log.Print("invalid bootstrap config yaml:\n", string(ba))
return
}
return
} }
func (ctx *renderContext) render(templateText string) (ba []byte, err error) { func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
@ -132,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...)...)
} }
@ -158,26 +164,7 @@ 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{
"base64": func(input string) string {
return base64.StdEncoding.EncodeToString([]byte(input))
},
"host_ip": func() (s string) { "host_ip": func() (s string) {
return ctx.Host.IPs[0] return ctx.Host.IPs[0]
}, },
@ -189,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{{
@ -230,10 +184,6 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
Content: string(userCA), Content: string(userCA),
}}) }})
}, },
"ssh_user_ca_pub": func(cluster string) (s string, err error) {
userCA, err := sshCAPubKey(cluster)
return string(userCA), err
},
"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
@ -268,20 +218,6 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
return asYaml(files) return asYaml(files)
}, },
"ssh_host_key": func(type_ string) (s string, err error) {
pair, err := ctx.sshHostKeyPair(type_)
if err != nil {
return
}
return pair.Private, nil
},
"ssh_host_pubkey": func(type_ string) (s string, err error) {
pair, err := ctx.sshHostKeyPair(type_)
if err != nil {
return
}
return pair.Public, nil
},
"host_download_token": func() (token string, err error) { "host_download_token": func() (token string, err error) {
key := ctx.Host.Name key := ctx.Host.Name
token, found, err := hostDownloadTokens.Get(key) token, found, err := hostDownloadTokens.Get(key)
@ -303,65 +239,9 @@ 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
} }
return funcs return funcs
} }
func (ctx *renderContext) sshHostKeyPair(type_ string) (kp SSHKeyPair, err error) {
pairs, err := getSSHKeyPairs(ctx.Host.Name)
if err != nil {
return
}
for _, pair := range pairs {
if pair.Type == type_ {
return pair, nil
}
}
err = fmt.Errorf("no key pair with type %q", type_)
return
}

View File

@ -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
}

View File

@ -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",
} { } {
@ -156,25 +157,28 @@ func sshCASign(cluster string, userPubKey []byte, principal, validity string, op
return return
} }
pubkey, identity, _, _, err := ssh.ParseAuthorizedKey(bytes.TrimSpace(userPubKey)) _, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey)
if err != nil { if err != nil {
return return
} }
ak := ssh.MarshalAuthorizedKey(pubkey)
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub") userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
if err != nil { if err != nil {
return return
} }
defer os.Remove(userPubKeyFile.Name()) defer os.Remove(userPubKeyFile.Name())
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(ak)) _, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey))
userPubKeyFile.Close() userPubKeyFile.Close()
if err != nil { if err != nil {
return return
} }
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
if err != nil {
return
}
serial := strconv.FormatInt(time.Now().Unix(), 10) serial := strconv.FormatInt(time.Now().Unix(), 10)
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal) cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -79,17 +79,6 @@ func getUsableClusterCA(cluster, name string) (ca CA, err error) {
return return
} }
func caExtraCerts(cluster, name string) (extra string, err error) {
cfg, err := readConfig()
if err != nil {
return
}
if cfg.ExtraCaCerts != nil {
extra = cfg.ExtraCaCerts[cluster+"/"+name]
}
return
}
var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys") var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys")
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) { func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -211,7 +211,9 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
// boot v2 // boot v2
case "bootstrap-config": case "bootstrap-config":
err = renderBootstrapConfig(w, ctx) err = renderBootstrapConfig(w, r, ctx, false)
case "bootstrap-config.json":
err = renderBootstrapConfig(w, r, ctx, true)
default: default:
http.NotFound(w, r) http.NotFound(w, r)

View File

@ -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
} }

View File

@ -0,0 +1,44 @@
package main
import (
"net/http"
"os"
"path/filepath"
restful "github.com/emicklei/go-restful"
yaml "gopkg.in/yaml.v2"
)
type SSH_ACL struct {
Keys []string
Clusters []string
Groups []string
Hosts []string
}
func loadSSH_ACLs() (acls []SSH_ACL, err error) {
f, err := os.Open(filepath.Join(*dataDir, "ssh-acls.yaml"))
if err != nil {
return
}
defer f.Close()
err = yaml.NewDecoder(f).Decode(&acls)
return
}
func wsSSH_ACL_List(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}
func wsSSH_ACL_Get(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}
func wsSSH_ACL_Set(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}

View File

@ -4,12 +4,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"net/http" "net/http"
"strings"
"text/template" "text/template"
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"
@ -36,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)
} }
@ -67,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"))
@ -150,6 +144,10 @@ 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) rest.Add(ws)
// Hosts API // Hosts API
@ -170,6 +168,19 @@ func registerWS(rest *restful.Container) {
rest.Add(ws) rest.Add(ws)
// Detected host API
ws = (&restful.WebService{}).
Filter(requireSecStore).
Path("/me").
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
(&wsHost{
hostDoc: "detected host",
getHost: detectHost,
}).register(ws, func(rb *restful.RouteBuilder) {
rb.Notes("In this case, the host is detected from the remote IP")
})
// Hosts by token API // Hosts by token API
ws = (&restful.WebService{}). ws = (&restful.WebService{}).
Filter(requireSecStore). Filter(requireSecStore).
@ -210,6 +221,41 @@ func requireSecStore(req *restful.Request, resp *restful.Response, chain *restfu
chain.ProcessFilter(req, resp) chain.ProcessFilter(req, resp)
} }
func detectHost(req *restful.Request) (hostName string, err error) {
if !*allowDetectedHost {
return
}
r := req.Request
remoteAddr := r.RemoteAddr
if *trustXFF {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
remoteAddr = strings.Split(xff, ",")[0]
}
}
hostIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
hostIP = remoteAddr
}
cfg, err := readConfig()
if err != nil {
return
}
host := cfg.HostByIP(hostIP)
if host == nil {
log.Print("no host found for IP ", hostIP)
return
}
return host.Name, nil
}
func wsReadConfig(resp *restful.Response) *localconfig.Config { func wsReadConfig(resp *restful.Response) *localconfig.Config {
cfg, err := readConfig() cfg, err := readConfig()
if err != nil { if err != nil {
@ -254,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)
}

8
go.mod
View File

@ -11,21 +11,18 @@ 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
k8s.io/apimachinery v0.33.2 k8s.io/apimachinery v0.33.2
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766 m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
novit.tech/direktil/pkg v0.0.0-20260221072850-b72bed72bb51 novit.tech/direktil/pkg v0.0.0-20250630100055-554aaf0a8931
) )
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
@ -73,7 +70,10 @@ 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/sync v0.15.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
golang.org/x/tools v0.34.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
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect

249
go.sum
View File

@ -1,13 +1,20 @@
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@ -20,12 +27,20 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -34,16 +49,17 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM= github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM=
github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@ -52,40 +68,63 @@ github.com/emicklei/go-restful-openapi v1.4.1/go.mod h1:kWQ8rQMVQ6G6lePwjDveJ00K
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.5.0 h1:Tb4jWdSpdjKzTUicPnY61PZxKbDoGa7ABbrReT3gQVY= github.com/frankban/quicktest v1.5.0 h1:Tb4jWdSpdjKzTUicPnY61PZxKbDoGa7ABbrReT3gQVY=
github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.0.0-20180322222742-3fb327e6747d/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.0.0-20180415031709-bcff419492ee/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.0.0-20180405201759-811b1089cde9/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
@ -98,20 +137,37 @@ github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wK
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw=
github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE=
github.com/google/certificate-transparency-go v1.1.8 h1:LGYKkgZF7satzgTak9R4yzfJXEeYVAjV6/EAEJOf1to=
github.com/google/certificate-transparency-go v1.1.8/go.mod h1:bV/o8r0TBKRf1X//iiiSgWrvII4d7/8OiA+3vG26gI8=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@ -128,8 +184,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=
@ -140,19 +194,25 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34 h1:F3u4DKQ4T30mlBNFmSGzTqdkmVqbfVORv34ZRvc7PuE= github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34 h1:F3u4DKQ4T30mlBNFmSGzTqdkmVqbfVORv34ZRvc7PuE=
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34/go.mod h1:lcyE8C83VRamH/oTpikU4+yVCCxLthWgDOqjHSsu+ZY= github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34/go.mod h1:lcyE8C83VRamH/oTpikU4+yVCCxLthWgDOqjHSsu+ZY=
github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA= github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA=
@ -165,40 +225,59 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergeymakinen/go-crypt v1.0.0 h1:R9yyekVU0bxP7MUUJd3uCcGQxg5RqMJ/ahy2bllx9qg=
github.com/sergeymakinen/go-crypt v1.0.0/go.mod h1:5NPTCUNfQrNlck+I9ONHFT55UvGWzosSf+QQPSuxYBE=
github.com/sergeymakinen/go-crypt v1.0.1 h1:InrJqCaOzWHN7Pv27V1c/MeW6ydQqdrDjU99dXHaoNY= github.com/sergeymakinen/go-crypt v1.0.1 h1:InrJqCaOzWHN7Pv27V1c/MeW6ydQqdrDjU99dXHaoNY=
github.com/sergeymakinen/go-crypt v1.0.1/go.mod h1:sTVy1KCTr+ot85sw2h2fMalv5QRD1cgIWqlBFI/8axg= github.com/sergeymakinen/go-crypt v1.0.1/go.mod h1:sTVy1KCTr+ot85sw2h2fMalv5QRD1cgIWqlBFI/8axg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
@ -218,12 +297,20 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222 h1:h2JizvZl9aIj6za9S5AyrkU+OzIS4CetQthH/ejO+lg=
github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ=
github.com/weppos/publicsuffix-go v0.30.2 h1:Np18yzfMR90jNampWFs7iSh2sw/qCZkhL41/ffyihCU=
github.com/weppos/publicsuffix-go v0.30.2/go.mod h1:/hGscit36Yt+wammfBBwdMdxBT8btsTt6KvwO9OvMyM=
github.com/weppos/publicsuffix-go v0.40.3-0.20250617082559-9b2e24a9e482 h1:0HudNf74HwwerH9HSlQYxfK+53VqFo6U04lQuTxfRf8= github.com/weppos/publicsuffix-go v0.40.3-0.20250617082559-9b2e24a9e482 h1:0HudNf74HwwerH9HSlQYxfK+53VqFo6U04lQuTxfRf8=
github.com/weppos/publicsuffix-go v0.40.3-0.20250617082559-9b2e24a9e482/go.mod h1:Efaen92I7hksG9EA+bsuHPWscS8ePs86CXxNFfG2cG4= github.com/weppos/publicsuffix-go v0.40.3-0.20250617082559-9b2e24a9e482/go.mod h1:Efaen92I7hksG9EA+bsuHPWscS8ePs86CXxNFfG2cG4=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
@ -231,39 +318,86 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk=
github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
github.com/zmap/zcrypto v0.0.0-20220402174210-599ec18ecbac/go.mod h1:egdRkzUylATvPkWMpebZbXhv0FMEMJGX/ur0D3Csk2s=
github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c h1:U1b4THKcgOpJ+kILupuznNwPiURtwVW3e9alJvji9+s=
github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c/go.mod h1:GSDpFDD4TASObxvfZfvpZZ3OWHIUHMlhVWlkOe4ewVk=
github.com/zmap/zcrypto v0.0.0-20250627161936-38850a079d72 h1:QcaEozNpjw8LcvzepkftwJ3kevF6qY7qRMikCvxWhpg= github.com/zmap/zcrypto v0.0.0-20250627161936-38850a079d72 h1:QcaEozNpjw8LcvzepkftwJ3kevF6qY7qRMikCvxWhpg=
github.com/zmap/zcrypto v0.0.0-20250627161936-38850a079d72/go.mod h1:uvqhJWCdbMIHIXZSKcqnJYy0yR/9v/TON/JQFbM2g6Q= github.com/zmap/zcrypto v0.0.0-20250627161936-38850a079d72/go.mod h1:uvqhJWCdbMIHIXZSKcqnJYy0yR/9v/TON/JQFbM2g6Q=
github.com/zmap/zlint/v3 v3.3.1 h1:IrIY2Qd2Wr9ZHhdQ3mszehSydz+x6OROClztMEK+2bU= github.com/zmap/zlint/v3 v3.3.1 h1:IrIY2Qd2Wr9ZHhdQ3mszehSydz+x6OROClztMEK+2bU=
github.com/zmap/zlint/v3 v3.3.1/go.mod h1:fPCW5acxhqw4HU1Vm0t9oFEPo1/uH9hI0sci/Z++hEI= github.com/zmap/zlint/v3 v3.3.1/go.mod h1:fPCW5acxhqw4HU1Vm0t9oFEPo1/uH9hI0sci/Z++hEI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -272,35 +406,59 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
@ -310,11 +468,27 @@ golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDq
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
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/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -333,20 +507,37 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY=
k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766 h1:JRzMBDbUwrTTGDJaJSH0ap4vRL0Q9CN1bG8a6n49eaQ= m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766 h1:JRzMBDbUwrTTGDJaJSH0ap4vRL0Q9CN1bG8a6n49eaQ=
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766/go.mod h1:BMv3aOSYpupuiiG3Ch3ND88aB5CfAks3YZuRLE8j1ls= m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766/go.mod h1:BMv3aOSYpupuiiG3Ch3ND88aB5CfAks3YZuRLE8j1ls=
novit.tech/direktil/pkg v0.0.0-20260210141740-4d5661fa8ecd h1:proGf8Cid9tzJzoRbqQHGGpZZKTpUDFwOREbjYrCbkM= novit.tech/direktil/pkg v0.0.0-20240120172717-8498a102796f h1:7y11nLhChrrsLQwRaW7wn/9x+Xn2gEVtzj75VkOpJ+o=
novit.tech/direktil/pkg v0.0.0-20260210141740-4d5661fa8ecd/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10= novit.tech/direktil/pkg v0.0.0-20240120172717-8498a102796f/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
novit.tech/direktil/pkg v0.0.0-20260221072850-b72bed72bb51 h1:NBcpvWcTBMzFos0pkuLsbVCQ+mHf8KqNOdVywMX6FFk= novit.tech/direktil/pkg v0.0.0-20240415114750-ccdbf2e2a5e4 h1:8HviBvbPzIBo2VpDl2jc6iPwiqlOrAbXqRhtn/yYMYE=
novit.tech/direktil/pkg v0.0.0-20260221072850-b72bed72bb51/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10= novit.tech/direktil/pkg v0.0.0-20240415114750-ccdbf2e2a5e4/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
novit.tech/direktil/pkg v0.0.0-20240415130406-0d2e181a4ed6 h1:D0TN5GyZ4d88ILpgVZgcZ62027lW8/LLnQSpQyN2yOw=
novit.tech/direktil/pkg v0.0.0-20240415130406-0d2e181a4ed6/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
novit.tech/direktil/pkg v0.0.0-20250628165657-648e16ad9855 h1:/wfTtrkOvfTXJw/JVAJwezMly8bu5Ix1S02kx8kEtg0=
novit.tech/direktil/pkg v0.0.0-20250628165657-648e16ad9855/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
novit.tech/direktil/pkg v0.0.0-20250630100055-554aaf0a8931 h1:q49xx2xpk7ot5dBTuSiCr5hTkynycdT1w0AcfTEFyxw=
novit.tech/direktil/pkg v0.0.0-20250630100055-554aaf0a8931/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=

View File

@ -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 .

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -2,5 +2,5 @@ package dlshtml
import "embed" import "embed"
//go:embed ui //go:embed favicon.ico ui
var FS embed.FS var FS embed.FS

View File

@ -1,131 +0,0 @@
const Cluster = {
components: { Downloads, GetCopy },
props: [ 'cluster', 'token', 'state' ],
data() {
return {
signReqValidity: "1d",
sshSignReq: {
PubKey: "",
Principal: "root",
},
sshUserCert: null,
kubeSignReq: {
CSR: "",
User: "",
Group: "system:masters",
},
kubeUserCert: null,
};
},
methods: {
sshCASign() {
event.preventDefault();
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
method: 'POST',
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => {
if (resp.ok) {
resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); })
},
kubeCASign() {
event.preventDefault();
fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
method: 'POST',
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => {
if (resp.ok) {
resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); })
},
readFile(e, onload) {
const file = e.target.files[0];
if (!file) { return; }
const reader = new FileReader();
reader.onload = () => { onload(reader.result) };
reader.onerror = () => { alert("error reading file"); };
reader.readAsText(file);
},
loadPubKey(e) {
this.readFile(e, (v) => {
this.sshSignReq.PubKey = v;
});
},
loadCSR(e) {
this.readFile(e, (v) => {
this.kubeSignReq.CSR = v;
});
},
},
template: `
<h3>Access</h3>
<p>Allow cluster access from a public key</p>
<h4>Grant SSH access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
<p>Public key (OpenSSH format):<br/>
<span class="text-and-file"><textarea v-model="sshSignReq.PubKey" style="height:3lh"></textarea>
<input type="file" accept=".pub" @change="loadPubKey" /></span>
</p>
<p><button @click="sshCASign">Sign SSH access</button>
<template v-if="sshUserCert">
=&gt; <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</template>
</p>
<h4>Grant Kubernetes API access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="kubeSignReq.User"/> (by default, from the CSR)</p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p>Certificate signing request (PEM format):<br/>
<span class="text-and-file"><textarea v-model="kubeSignReq.CSR" style="height:7lh;"></textarea>
<input type="file" accept=".csr" @change="loadCSR" /></span>
</p>
<p><button @click="kubeCASign">Sign Kubernetes API access</button>
<template v-if="kubeUserCert">
=&gt; <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</template>
</p>
<h3>Tokens</h3>
<section class="links">
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
</section>
<h3>Passwords</h3>
<section class="links">
<GetCopy v-for="n in cluster.Passwords" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/passwords/'+n" />
</section>
<h3>Downloads</h3>
<Downloads :token="token" :state="state" kind="cluster" :name="cluster.Name" />
<h3>CAs</h3>
<table><tr><th>Name</th><th>Certificate</th><th>Signed certificates</th></tr>
<tr v-for="ca in cluster.CAs">
<td>{{ ca.Name }}</td>
<td><GetCopy :token="token" name="cert" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/certificate'" /></td>
<td><template v-for="signed in ca.Signed">
{{" "}}
<GetCopy :token="token" :name="signed" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/signed?name='+signed" />
</template></td>
</tr></table>
`
}

View File

@ -1,70 +0,0 @@
const Downloads = {
props: [ 'kind', 'name', 'token', 'state' ],
data() {
return { createDisabled: false, selectedAssets: {} }
},
computed: {
availableAssets() {
return {
cluster: ['addons'],
host: [
"kernel",
"initrd",
"bootstrap.tar",
"boot.img.lz4",
"boot.img.gz",
"boot.qcow2",
"boot.vmdk",
"boot.img",
"boot.iso",
"boot.tar",
"boot-efi.tar",
"config",
"bootstrap-config",
"ipxe",
],
}[this.kind]
},
downloads() {
return Object.entries(this.state.Downloads)
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
.map(e => {
const token= e[0];
return {
text: token.substring(0, 5) + '...',
url: '/public/downloads/'+token+"/",
}
})
},
assets() {
return this.availableAssets.filter(a => this.selectedAssets[a])
},
},
methods: {
createToken() {
event.preventDefault()
this.createDisabled = true
fetch('/authorize-download', {
method: 'POST',
body: JSON.stringify({Kind: this.kind, Name: this.name, Assets: this.assets}),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => resp.json())
.then((token) => { this.selectedAssets = {}; this.createDisabled = false })
.catch((e) => { alert('failed to create link'); this.createDisabled = false })
},
},
template: `
<h4>Available assets</h4>
<p class="downloads">
<template v-for="asset in availableAssets">
<label :class="{selected: selectedAssets[asset]}"><input type="checkbox" v-model="selectedAssets[asset]" />&nbsp;{{ asset }}</label>
{{" "}}
</template>
</p>
<p><button :disabled="createDisabled || assets.length==0" @click="createToken">Create link</button></p>
<template v-if="downloads.length">
<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>
</template>`
}

View File

@ -1,32 +0,0 @@
const GetCopy = {
props: [ 'name', 'href', 'token' ],
data() { return {showCopied: false} },
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a>&nbsp;<a href="#" class="copy" @click="fetchAndCopy()">&#x1F5D0;</a></span>`,
methods: {
fetch() {
event.preventDefault()
return fetch(this.href, {
method: 'GET',
headers: { 'Authorization': 'Bearer ' + this.token },
})
},
handleFetchError(e) {
console.log("failed to get value:", e)
alert('failed to get value')
},
fetchAndSave() {
this.fetch().then(resp => resp.blob()).then((value) => {
window.open(URL.createObjectURL(value), "_blank")
}).catch(this.handleFetchError)
},
fetchAndCopy() {
this.fetch()
.then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text())
.then((value) => {
window.navigator.clipboard.writeText(value)
this.showCopied = true
setTimeout(() => { this.showCopied = false }, 1000)
}).catch(this.handleFetchError)
},
},
}

View File

@ -1,14 +0,0 @@
const Host = {
components: { Downloads },
props: [ 'host', 'token', 'state' ],
template: `
<p>Cluster: {{ host.Cluster }}<template v-if="host.Template"> ({{ host.Template }})</template></p>
<p>IPs:
<code v-for="ip in host.IPs">
{{ ip }}{{" "}}
</code>
</p>
<h3>Downloads</h3>
<Downloads :token="token" :state="state" kind="host" :name="host.Name" />
`
}

View File

@ -1,267 +0,0 @@
import { createApp } from './vue.esm-browser.js';
createApp({
components: { Cluster, Host },
data() {
return {
forms: {
store: {},
storeUpload: {},
delKey: {},
hostFromTemplate: {},
hostFromTemplateDel: "",
},
view: "",
viewFilter: "",
session: {},
error: null,
publicState: null,
serverVersion: null,
uiHash: null,
watchingState: false,
state: null,
}
},
mounted() {
this.session = JSON.parse(sessionStorage.state || "{}")
this.watchPublicState()
},
watch: {
session: {
deep: true,
handler(v) {
sessionStorage.state = JSON.stringify(v)
if (v.token && !this.watchingState) {
this.watchState()
this.watchingState = true
}
}
},
publicState: {
deep: true,
handler(v) {
if (v) {
this.serverVersion = v.ServerVersion
if (this.uiHash && v.UIHash != this.uiHash) {
console.log("reloading")
location.reload()
} else {
this.uiHash = v.UIHash
}
}
},
}
},
computed: {
views() {
var views = [{type: "actions", name: "admin", title: "Admin actions"}];
(this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`}));
(this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`}));
return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter));
},
viewObj() {
if (this.view) {
if (this.view.type == "cluster") {
return this.state.Clusters.find((c) => c.Name == this.view.name);
}
if (this.view.type == "host") {
return this.state.Hosts.find((h) => h.Name == this.view.name);
}
}
return undefined;
},
hostsFromTemplate() {
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)
},
setToken() {
event.preventDefault()
this.session.token = this.forms.setToken
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() {
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
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() {
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
this.forms.store = {}
if (v) {
this.session.token = v
if (!this.watchingState) {
this.watchState()
this.watchingState = true
}
}
})
},
uploadConfig() {
const file = this.$refs.configUpload.files[0];
let contentType = "text/vnd.yaml";
if (file.name.endsWith(".json")) {
contentType = "application/json";
}
this.apiPost('/configs', file, (v) => {}, contentType, true)
},
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', raw = false) {
event.preventDefault()
if (data === undefined) {
throw("action " + action + ": no data")
}
/* TODO
fetch(action, {
method: 'POST',
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((result) => onload)
// */
var xhr = new XMLHttpRequest()
xhr.responseType = 'json'
// TODO spinner, pending action notification, or something
xhr.onerror = () => {
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
}
xhr.onload = (r) => {
if (xhr.status != 200) {
this.error = xhr.response
return
}
// this.actionResults.splice(idx, 1, {...item, done: true, resp: xhr.responseText})
this.error = null
if (onload) {
onload(xhr.response)
}
}
xhr.open("POST", action)
xhr.setRequestHeader('Accept', 'application/json')
xhr.setRequestHeader('Content-Type', contentType)
if (this.session.token) {
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
}
if (!raw && 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) {
event.target.target = '_blank'
event.target.href = this.downloadLink(url)
},
downloadLink(url) {
// TODO once-shot download link
return url + '?token=' + this.session.token
},
watchPublicState() {
this.watchStream('publicState', '/public-state')
},
watchState() {
this.watchStream('state', '/state', true)
},
watchStream(field, path, withToken) {
let evtSrc = new EventSource(path + (withToken ? '?token='+this.session.token : ''));
evtSrc.onmessage = (e) => {
let update = JSON.parse(e.data)
console.log("watch "+path+":", update)
if (update.err) {
console.log("watch error from server:", err)
}
if (update.set) {
this[field] = update.set
}
if (update.p) { // patch
new jsonpatch.JSONPatch(update.p, true).apply(this[field])
}
}
evtSrc.onerror = (e) => {
// console.log("event source " + path + " error:", e)
if (evtSrc) evtSrc.close()
this[field] = null
window.setTimeout(() => { this.watchStream(field, path, withToken) }, 1000)
}
},
}
}).mount('#app');

View File

@ -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,23 +20,18 @@
border-radius: 1ex; border-radius: 1ex;
cursor: pointer; cursor: pointer;
} }
}
.downloads, .view-links {
& > .selected { & > .selected {
color: var(--link); color: blue;
} }
} }
.text-and-file { .download-links a {
position:relative; margin-right: 1ex;
textarea {
width: 64em;
} }
input[type="file"] { @media (prefers-color-scheme: dark) {
position:absolute; .downloads > .selected,
bottom:0;right:0; .view-links > .selected {
color: #31b0fa;
} }
} }

View File

@ -2,25 +2,18 @@
<html> <html>
<head> <head>
<title>Direktil Local Server</title> <title>Direktil Local Server</title>
<base href="/ui/" /> <style>
@import url('./style.css');
<link rel="icon" href="favicon.ico" /> @import url('./app.css');
</style>
<script src="js/jsonpatch.min.js" crossorigin="anonymous"></script>
<script src="js/app.js" type="module" defer></script>
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
<script src="/ui/jsonpatch.min-942279a1c4209cc2.js" integrity="sha384-GcPrkRS12jtrElEkbJcrZ8asvvb9s3mc+CUq9UW/8bL4L0bpkmh9M+oFnWN6qLq2"></script>
<script src="/ui/app-7f4645e84eaae7ce.js" defer integrity="sha384-31uiQnXVSPLDq61o+chfyKRkSdkmzv023M7KafOo+0xRw5AM70BUFyYO6yo0kPiZ" type="module"></script>
<script src="/ui/Downloads-c9374f19f52c46d.js" integrity="sha384-WQIkCSxlUkvu4jFIWwS3bESMaazGwwnBn9dyvB7nItz3L8BmusRMsJACzfJa7sC4"></script>
<script src="/ui/GetCopy-7e3c9678f9647d40.js" integrity="sha384-LzxUXylxE/t25HyTch8y2qvKcehvP2nqCo37swIBGEKZZUzHVJVQrS5UJDWNskTA"></script>
<script src="/ui/Cluster-703dcdca97841304.js" integrity="sha384-ifGpq/GB1nDfqczm5clTRtcfnc9UMkT+SptMyS3UG9Di3xoKORtOGGjmK5RnJx+v"></script>
<script src="/ui/Host-61916516a854adff.js" integrity="sha384-/wh3KrC0sb4MT7ekO2U84rswxI42WSH/0jB4dbDaaGaGhX60xTEZHFsdQAf7UgTG"></script>
<body> <body>
<div id="app"> <div id="app">
<header> <header>
<div id="logo"> <div id="logo">
<img src="favicon.ico" /> <img src="/favicon.ico" />
<span>Direktil Local Server</span> <span>Direktil Local Server</span>
</div> </div>
<div class="utils"> <div class="utils">
@ -81,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>
@ -119,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>
@ -130,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>
@ -141,5 +134,6 @@
</div> </div>
</template> </template>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,4 +1,8 @@
const Cluster = {
import Downloads from './Downloads.js';
import GetCopy from './GetCopy.js';
export default {
components: { Downloads, GetCopy }, components: { Downloads, GetCopy },
props: [ 'cluster', 'token', 'state' ], props: [ 'cluster', 'token', 'state' ],
data() { data() {
@ -11,8 +15,8 @@ const Cluster = {
sshUserCert: null, sshUserCert: null,
kubeSignReq: { kubeSignReq: {
CSR: "", CSR: "",
User: "", User: "anonymous",
Group: "system:masters", Group: "",
}, },
kubeUserCert: null, kubeUserCert: null,
}; };
@ -24,13 +28,8 @@ const Cluster = {
method: 'POST', method: 'POST',
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => { }).then((resp) => resp.blob())
if (resp.ok) { .then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); }) .catch((e) => { alert('failed to sign: '+e); })
}, },
kubeCASign() { kubeCASign() {
@ -39,70 +38,12 @@ const Cluster = {
method: 'POST', method: 'POST',
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => { }).then((resp) => resp.blob())
if (resp.ok) { .then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); }) .catch((e) => { alert('failed to sign: '+e); })
}, },
readFile(e, onload) {
const file = e.target.files[0];
if (!file) { return; }
const reader = new FileReader();
reader.onload = () => { onload(reader.result) };
reader.onerror = () => { alert("error reading file"); };
reader.readAsText(file);
},
loadPubKey(e) {
this.readFile(e, (v) => {
this.sshSignReq.PubKey = v;
});
},
loadCSR(e) {
this.readFile(e, (v) => {
this.kubeSignReq.CSR = v;
});
},
}, },
template: ` template: `
<h3>Access</h3>
<p>Allow cluster access from a public key</p>
<h4>Grant SSH access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
<p>Public key (OpenSSH format):<br/>
<span class="text-and-file"><textarea v-model="sshSignReq.PubKey" style="height:3lh"></textarea>
<input type="file" accept=".pub" @change="loadPubKey" /></span>
</p>
<p><button @click="sshCASign">Sign SSH access</button>
<template v-if="sshUserCert">
=&gt; <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</template>
</p>
<h4>Grant Kubernetes API access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="kubeSignReq.User"/> (by default, from the CSR)</p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p>Certificate signing request (PEM format):<br/>
<span class="text-and-file"><textarea v-model="kubeSignReq.CSR" style="height:7lh;"></textarea>
<input type="file" accept=".csr" @change="loadCSR" /></span>
</p>
<p><button @click="kubeCASign">Sign Kubernetes API access</button>
<template v-if="kubeUserCert">
=&gt; <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</template>
</p>
<h3>Tokens</h3> <h3>Tokens</h3>
<section class="links"> <section class="links">
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" /> <GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
@ -127,5 +68,34 @@ const Cluster = {
</template></td> </template></td>
</tr></table> </tr></table>
<h3>Access</h3>
<p>Allow cluster access from a public key</p>
<p>Certificate time validity: <input type="text" v-model="signReqValidity"/> <small>ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</p>
<h4>Grant SSH access</h4>
<p>Public key (OpenSSH format):<br/>
<textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea>
</p>
<p>Principal: <input type="text" v-model="sshSignReq.Principal"/></p>
<p><button @click="sshCASign">Sign SSH access request</button></p>
<p v-if="sshUserCert">
<a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</p>
<h4>Grant Kubernetes API access</h4>
<p>Certificate signing request (PEM format):<br/>
<textarea v-model="kubeSignReq.CSR" style="width:64em;height:7lh;"></textarea>
</p>
<p>User: <input type="text" v-model="kubeSignReq.User"/></p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p><button @click="kubeCASign">Sign Kubernetes API access request</button></p>
<p v-if="kubeUserCert">
<a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</p>
` `
} }

View File

@ -1,4 +1,5 @@
const Downloads = {
export default {
props: [ 'kind', 'name', 'token', 'state' ], props: [ 'kind', 'name', 'token', 'state' ],
data() { data() {
return { createDisabled: false, selectedAssets: {} } return { createDisabled: false, selectedAssets: {} }
@ -26,15 +27,16 @@ const Downloads = {
}[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])
@ -62,9 +64,9 @@ const Downloads = {
{{" "}} {{" "}}
</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>`
} }

View File

@ -1,4 +1,4 @@
const GetCopy = { export default {
props: [ 'name', 'href', 'token' ], props: [ 'name', 'href', 'token' ],
data() { return {showCopied: false} }, data() { return {showCopied: false} },
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a>&nbsp;<a href="#" class="copy" @click="fetchAndCopy()">&#x1F5D0;</a></span>`, template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a>&nbsp;<a href="#" class="copy" @click="fetchAndCopy()">&#x1F5D0;</a></span>`,

View File

@ -1,4 +1,7 @@
const Host = {
import Downloads from './Downloads.js';
export default {
components: { Downloads }, components: { Downloads },
props: [ 'host', 'token', 'state' ], props: [ 'host', 'token', 'state' ],
template: ` template: `

View File

@ -1,6 +1,9 @@
import { createApp } from './vue.esm-browser.js'; import { createApp } from './vue.esm-browser.js';
import Cluster from './Cluster.js';
import Host from './Host.js';
createApp({ createApp({
components: { Cluster, Host }, components: { Cluster, Host },
data() { data() {
@ -62,7 +65,7 @@ createApp({
(this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`})); (this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`}));
(this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`})); (this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`}));
return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter)); return views.filter((v) => v.name.includes(this.viewFilter));
}, },
viewObj() { viewObj() {
if (this.view) { if (this.view) {
@ -76,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)
@ -133,12 +133,7 @@ createApp({
}) })
}, },
uploadConfig() { uploadConfig() {
const file = this.$refs.configUpload.files[0]; this.apiPost('/configs', this.$refs.configUpload.files[0], (v) => {}, "text/vnd.yaml")
let contentType = "text/vnd.yaml";
if (file.name.endsWith(".json")) {
contentType = "application/json";
}
this.apiPost('/configs', file, (v) => {}, contentType, true)
}, },
hostFromTemplateAdd() { hostFromTemplateAdd() {
let v = this.forms.hostFromTemplate; let v = this.forms.hostFromTemplate;
@ -153,7 +148,7 @@ createApp({
} }
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" }); this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
}, },
apiPost(action, data, onload, contentType = 'application/json', raw = false) { apiPost(action, data, onload, contentType = 'application/json') {
event.preventDefault() event.preventDefault()
if (data === undefined) { if (data === undefined) {
@ -195,7 +190,7 @@ createApp({
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token) xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
} }
if (!raw && contentType == "application/json") { if (contentType == "application/json") {
xhr.send(JSON.stringify(data)) xhr.send(JSON.stringify(data))
} else { } else {
xhr.send(data) xhr.send(data)
@ -264,4 +259,5 @@ createApp({
}, },
} }
}).mount('#app'); }).mount('#app')

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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}
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
else
sgdisk --clear $dev
curl ${base_url}/boot.img.lz4 |lz4cat >$dev
sgdisk --move-second-header --new=3:0:0 $dev sgdisk --move-second-header --new=3:0:0 $dev
pvcreate ${devp}3 pvcreate ${devp}3
vgcreate storage ${devp}3 vgcreate storage ${devp}3
fi
while umount $MP; do true; done

View File

@ -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
} }

View File

@ -1,24 +0,0 @@
[build]
public_url = "/ui"
dist = "../html/ui"
[[proxy]]
backend = "http://localhost:7606/public-state"
[[proxy]]
backend = "http://localhost:7606/state"
[[proxy]]
backend = "http://localhost:7606/public"
[[proxy]]
backend = "http://localhost:7606/store"
[[proxy]]
backend = "http://localhost:7606/authorize-download"
[[proxy]]
backend = "http://localhost:7606/sign-download-set"
[[proxy]]
backend = "http://localhost:7606/configs"
[[proxy]]
backend = "http://localhost:7606/clusters"
[[proxy]]
backend = "http://localhost:7606/hosts-from-template"
[[proxy]]
backend = "http://localhost:7606/hosts"

View File

@ -1,42 +0,0 @@
.view-links > span {
display: inline-block;
white-space: nowrap;
margin-right: 1ex;
margin-bottom: 1ex;
padding: 0.5ex;
border: 1pt solid;
border-radius: 1ex;
cursor: pointer;
}
.downloads, .download-links {
& > * {
display: inline-block;
margin-right: 1ex;
margin-bottom: 1ex;
padding: 0.5ex;
border: 1px solid;
border-radius: 1ex;
cursor: pointer;
}
}
.downloads, .view-links {
& > .selected {
color: var(--link);
}
}
.text-and-file {
position:relative;
textarea {
width: 64em;
}
input[type="file"] {
position:absolute;
bottom:0;right:0;
}
}

View File

@ -1 +0,0 @@
trunk build --release

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,145 +0,0 @@
<!doctype html>
<html>
<head>
<title>Direktil Local Server</title>
<base data-trunk-public-url />
<link data-trunk rel="copy-file" href="favicon.ico" />
<link rel="icon" href="favicon.ico" />
<link data-trunk rel="copy-file" href="js/vue.esm-browser.js" />
<link data-trunk rel="copy-file" href="style.css" />
<link data-trunk rel="copy-file" href="app.css" />
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
<script data-trunk src="js/jsonpatch.min.js"></script>
<script data-trunk src="js/app.js" type="module" defer></script>
<script data-trunk src="js/Downloads.js"></script>
<script data-trunk src="js/GetCopy.js"></script>
<script data-trunk src="js/Cluster.js"></script>
<script data-trunk src="js/Host.js"></script>
<body>
<div id="app">
<header>
<div id="logo">
<img src="favicon.ico" />
<span>Direktil Local Server</span>
</div>
<div class="utils">
<span id="login-hdr" v-if="session.token">
Logged in
<button class="link" @click="copyText(session.token)">&#x1F5D0;</button>
</span>
<span>server <code>{{ serverVersion || '-----' }}</code></span>
<span>ui <code>{{ uiHash || '-----' }}</code></span>
<span :class="publicState ? 'green' : 'red'">&#x1F5F2;</span>
</div>
</header>
<div class="error" v-if="error">
<button class="btn-close" @click="error=null">&times;</button>
<div class="code" v-if="error.code">{{ error.code }}</div>
<div class="message">{{ error.message }}</div>
</div>
<template v-if="!publicState">
<p>Not connected.</p>
</template>
<template v-else-if="publicState.Store.New">
<p>Store is new.</p>
<p>Option 1: initialize a new store</p>
<form @submit="unlockStore">
<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" />
</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 v-else-if="!publicState.Store.Open">
<p>Store is not open.</p>
<form @submit="unlockStore">
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
</form>
</template>
<template v-else-if="!state">
<p v-if="!session.token">Not logged in.</p>
<p v-else>Invalid token</p>
<form @submit="unlockStore">
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
<input type="submit" value="log in"/>
</form>
</template>
<template v-else>
<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>
<div v-if="view.type == 'cluster'" id="clusters">
<Cluster :cluster="viewObj" :token="session.token" :state="state" />
</div>
<div v-if="view.type == 'host'" id="hosts">
<Host :host="viewObj" :token="session.token" :state="state" />
</div>
<div v-if="view.type == 'actions' && view.name == 'admin'">
<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="any(state.HostTemplates) || any(hostsFromTemplate)">
<h3>Hosts from template</h3>
<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>
<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="" 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>
</select>
<input type="submit" value="delete instance" :disabled="!forms.hostFromTemplateDel" />
</form>
</template>
</div>
</template>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,195 +0,0 @@
: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: var(--bg);
color: var(--color);
}
button[disabled] {
opacity: 0.5;
}
a[href], a[href]:visited, button.link {
border: none;
color: var(--link);
background: none;
cursor: pointer;
text-decoration: none;
}
table {
border-collapse: collapse;
}
th, td {
border-left: dotted 1pt;
border-right: dotted 1pt;
border-bottom: dotted 1pt;
padding: 2pt 4pt;
}
tr:first-child > th {
border-top: dotted 1pt;
}
th, tr:last-child > td {
border-bottom: solid 1pt;
}
.flat > * { margin-left: 1ex; }
.flat > *:first-child { margin-left: 0; }
.green { color: green; }
.red { color: red; }
@media (prefers-color-scheme: dark) {
.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 {
display: flex;
align-items: center;
border-bottom: 2pt solid;
margin: 0 0 1em 0;
padding: 1ex;
justify-content: space-between;
}
#logo > img {
vertical-align: middle;
}
header .utils > * {
margin-left: 1ex;
}
.error {
display: flex;
position: relative;
background: rgba(255,0,0,0.2);
border: 1pt solid red;
justify-content: space-between;
align-items: center;
}
.error .btn-close,
.error .code {
background: #600;
color: white;
font-weight: bold;
border: none;
align-self: stretch;
padding: 1ex 1em;
}
.error .code {
order: 1;
display: flex;
align-items: center;
text-align: center;
}
.error .message {
order: 2;
padding: 1ex 2em;
}
.error .btn-close {
order: 3;
}
.sheets {
display: flex;
align-items: stretch;
}
.sheets > div {
margin: 0 1ex;
border: 1pt solid;
border-radius: 6pt;
}
.sheets .title {
text-align: center;
font-weight: bold;
font-size: large;
padding: 2pt 6pt;
background: rgba(127,127,127,0.5);
}
.sheets .section {
padding: 2pt 6pt 2pt 6pt;
font-weight: bold;
border-top: 1px dotted;
}
.sheets section {
margin: 2pt 6pt 6pt 6pt;
}
.sheets > *:last-child > table:last-child > tr:last-child > td {
border-bottom: none;
}
.notif {
display: inline-block;
position: relative;
}
.notif > div:first-child {
position: absolute;
min-width: 100%; height: 100%;
background: white;
opacity: 75%;
text-align: center;
}
.links > * { margin-left: 1ex; }
.links > *:first-child { margin-left: 0; }
@media (prefers-color-scheme: dark) {
.notif > div:first-child {
background: black;
}
}
.copy { font-size: small; }