Compare commits
70 Commits
50bb60823f
...
dev
Author | SHA1 | Date | |
---|---|---|---|
98eb601fd3 | |||
8e87d406e4 | |||
f83b1eab23 | |||
d03a7ab4ec | |||
cd69d9234e | |||
5fa367949b | |||
cef4441208 | |||
d4087d3534 | |||
ab6f0b6358 | |||
af2758dead | |||
899a0a9dab | |||
08cbccc756 | |||
62882e78d8 | |||
47843f202f | |||
216236c1eb | |||
6651ff0364 | |||
d77588274b | |||
20b6769cbb | |||
9ad7715a29 | |||
5c91736202 | |||
73c533116c | |||
c8759e03d5 | |||
f5abdfdf3f | |||
7a55735cc4 | |||
48201132bd | |||
37713f8c16 | |||
85b9a45856 | |||
af41df6ab4 | |||
4b05458cec | |||
84a0e286e7 | |||
58cfaa7d0f | |||
1871eac7bb | |||
b12ce7299f | |||
82f7cbcc92 | |||
ce8b7f01ef | |||
edbe1641fd | |||
aac792c341 | |||
eaeb38b8c2 | |||
e0f755ec42 | |||
bb7c3835bc | |||
7c9334233d | |||
699b8e71a6 | |||
d4dbe709e0 | |||
22a3e0b6c2 | |||
e08bf0e99d | |||
1e904b7361 | |||
8ed0f12fb4 | |||
f59eca6724 | |||
b5b7272603 | |||
4f48866daa | |||
b616b710cb | |||
c02f701c04 | |||
7f429a863d | |||
29ed01a19f | |||
07e9dccd06 | |||
40d08139db | |||
efa6193954 | |||
f7b708ce4b | |||
41897c00b4 | |||
ee5629643c | |||
34afe03818 | |||
25c2d20c19 | |||
c338522b33 | |||
b6fa941fcc | |||
7619998d8f | |||
b6e7c55704 | |||
4ed50e3b78 | |||
dac6613646 | |||
a8ccb6990b | |||
b1cdb30622 |
@ -1 +1,3 @@
|
||||
tmp
|
||||
dist
|
||||
test-run
|
||||
|
42
Dockerfile
42
Dockerfile
@ -1,23 +1,35 @@
|
||||
from novit.tech/direktil/dkl:bbea9b9 as dkl
|
||||
# ------------------------------------------------------------------------
|
||||
from mcluseau/golang-builder:1.20.3 as build
|
||||
from golang:1.24.4-bookworm as build
|
||||
|
||||
run apt-get update && apt-get install -y git
|
||||
|
||||
workdir /src
|
||||
|
||||
copy go.mod go.sum ./
|
||||
run \
|
||||
--mount=type=cache,id=gomod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=gobuild,target=/root/.cache/go-build \
|
||||
go mod download
|
||||
|
||||
arg GIT_TAG
|
||||
|
||||
copy . ./
|
||||
run \
|
||||
--mount=type=cache,id=gomod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=gobuild,target=/root/.cache/go-build \
|
||||
go test ./... && \
|
||||
hack/build ./...
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
from debian:stretch
|
||||
from debian:bookworm
|
||||
entrypoint ["/bin/dkl-local-server"]
|
||||
|
||||
env _uncache 1
|
||||
env _uncache=1
|
||||
run apt-get update \
|
||||
&& apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
||||
&& yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
||||
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils \
|
||||
&& apt-get clean
|
||||
|
||||
run yes |apt-get install -y grub2 grub-pc-bin grub-efi-amd64-bin \
|
||||
&& apt-get clean
|
||||
|
||||
run apt-get install -y ca-certificates curl openssh-client \
|
||||
&& apt-get clean
|
||||
|
||||
run curl -L https://github.com/vmware/govmomi/releases/download/v0.21.0/govc_linux_amd64.gz | gunzip > /bin/govc && chmod +x /bin/govc
|
||||
|
||||
copy upload-vmware.sh govc.env /var/lib/direktil/
|
||||
|
||||
copy --from=build /go/bin/ /bin/
|
||||
copy --from=dkl /bin/dkl /bin/dls /bin/
|
||||
copy --from=build /src/dist/ /bin/
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
@ -116,9 +117,26 @@ func eachFragment(path string, searchList []FS, walk func(io.Reader) error) (err
|
||||
continue
|
||||
}
|
||||
|
||||
genCmd, found := strings.CutPrefix(line, "#!gen ")
|
||||
if found {
|
||||
cmdArgs := strings.Fields(genCmd)
|
||||
if Debug {
|
||||
log.Print("#!gen ", cmdArgs)
|
||||
}
|
||||
|
||||
cmd := *dir + "/gen/" + cmdArgs[0]
|
||||
args := cmdArgs[1:]
|
||||
genOutput, err := exec.Command(cmd, args...).Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gen %v: %w", cmdArgs, err)
|
||||
}
|
||||
walk(bytes.NewBuffer(genOutput))
|
||||
continue
|
||||
}
|
||||
|
||||
includePath, found := strings.CutPrefix(line, "#!include ")
|
||||
if !found {
|
||||
continue // or break?
|
||||
continue
|
||||
}
|
||||
|
||||
includePath = strings.TrimSpace(includePath)
|
||||
@ -127,8 +145,7 @@ func eachFragment(path string, searchList []FS, walk func(io.Reader) error) (err
|
||||
}
|
||||
err = eachFragment(includePath, searchList, walk)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("include %q: %w", includePath, err)
|
||||
return
|
||||
return fmt.Errorf("include %q: %w", includePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
var (
|
||||
Debug = false
|
||||
|
||||
@ -50,11 +52,6 @@ func main() {
|
||||
|
||||
openIncludes()
|
||||
|
||||
if false {
|
||||
assemble("hosts/m1")
|
||||
log.Fatal("--- debug: end ---")
|
||||
}
|
||||
|
||||
loadSrc()
|
||||
|
||||
dst = &localconfig.Config{
|
||||
@ -94,7 +91,7 @@ func main() {
|
||||
ctx.Host.Versions["modules"] = ctx.Host.Kernel
|
||||
}
|
||||
|
||||
dst.Hosts = append(dst.Hosts, &localconfig.Host{
|
||||
renderedHost := &localconfig.Host{
|
||||
Name: host.Name,
|
||||
|
||||
ClusterName: ctx.Cluster.Name,
|
||||
@ -113,7 +110,13 @@ func main() {
|
||||
|
||||
BootstrapConfig: ctx.BootstrapConfig(),
|
||||
Config: ctx.Config(),
|
||||
})
|
||||
}
|
||||
|
||||
if host.Template {
|
||||
dst.HostTemplates = append(dst.HostTemplates, renderedHost)
|
||||
} else {
|
||||
dst.Hosts = append(dst.Hosts, renderedHost)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@ -124,6 +127,8 @@ func main() {
|
||||
|
||||
defer out.Close()
|
||||
|
||||
out.Write([]byte("# dkl-dir2config " + Version + "\n"))
|
||||
|
||||
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
||||
log.Fatal("failed to render output: ", err)
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ import (
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
||||
func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]any {
|
||||
cluster := clusterSpec.Name
|
||||
|
||||
return map[string]interface{}{
|
||||
"password": func(name string) (s string) {
|
||||
return fmt.Sprintf("{{ password %q %q }}", cluster, name)
|
||||
return map[string]any{
|
||||
"password": func(name, hash string) (s string) {
|
||||
return fmt.Sprintf("{{ password %q %q %q | quote }}", cluster, name, hash)
|
||||
},
|
||||
|
||||
"token": func(name string) (s string) {
|
||||
@ -36,7 +36,7 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
||||
return fmt.Sprintf("{{ ca_dir %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"hosts_by_cluster": func(cluster string) (hosts []interface{}) {
|
||||
"hosts_by_cluster": func(cluster string) (hosts []any) {
|
||||
for _, host := range src.Hosts {
|
||||
if host.Cluster == cluster {
|
||||
hosts = append(hosts, asMap(host))
|
||||
@ -50,7 +50,7 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
||||
return
|
||||
},
|
||||
|
||||
"hosts_by_group": func(group string) (hosts []interface{}) {
|
||||
"hosts_by_group": func(group string) (hosts []any) {
|
||||
for _, host := range src.Hosts {
|
||||
if host.Cluster == cluster && host.Group == group {
|
||||
hosts = append(hosts, asMap(host))
|
||||
@ -63,6 +63,26 @@ func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]interface{} {
|
||||
|
||||
return
|
||||
},
|
||||
|
||||
"host_ip_from": func(hostName, net string) string {
|
||||
host := src.Host(hostName)
|
||||
if host == nil {
|
||||
log.Printf("WARNING: no host named %q", hostName)
|
||||
return "<no value>"
|
||||
}
|
||||
|
||||
ipFrom := host.IPFrom
|
||||
if ipFrom == nil {
|
||||
ipFrom = map[string]string{}
|
||||
}
|
||||
|
||||
ip, ok := ipFrom[net]
|
||||
if !ok {
|
||||
ip = host.IP
|
||||
}
|
||||
|
||||
return ip
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.tech/direktil/pkg/config"
|
||||
@ -156,8 +157,8 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
||||
|
||||
extraFuncs := ctx.templateFuncs(ctxMap)
|
||||
|
||||
extraFuncs["bootstrap_pods_files"] = func(dir string) (string, error) {
|
||||
namePods := ctx.renderBootstrapPods()
|
||||
extraFuncs["static_pods_files"] = func(dir string) (string, error) {
|
||||
namePods := ctx.renderStaticPods()
|
||||
|
||||
defs := make([]config.FileDef, 0)
|
||||
|
||||
@ -166,7 +167,7 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
||||
|
||||
ba, err := yaml.Marshal(namePod.Pod)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bootstrap pod %s: failed to render: %v", name, err)
|
||||
return "", fmt.Errorf("static pod %s: failed to render: %v", name, err)
|
||||
}
|
||||
|
||||
defs = append(defs, config.FileDef{
|
||||
@ -180,17 +181,30 @@ func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clusters
|
||||
return string(ba), err
|
||||
}
|
||||
|
||||
extraFuncs["machine_id"] = func() string {
|
||||
ba := sha1.Sum([]byte(ctx.Cluster.Name + "/" + ctx.Host.Name)) // TODO: check semantics of machine-id
|
||||
return hex.EncodeToString(ba[:])
|
||||
extraFuncs["host_ip"] = func() string {
|
||||
if ctx.Host.Template {
|
||||
return "{{ host_ip }}"
|
||||
}
|
||||
return ctx.Host.IP
|
||||
}
|
||||
extraFuncs["host_name"] = func() string {
|
||||
if ctx.Host.Template {
|
||||
return "{{ host_name }}"
|
||||
}
|
||||
return ctx.Host.Name
|
||||
}
|
||||
extraFuncs["machine_id"] = func() string {
|
||||
return "{{ machine_id }}"
|
||||
}
|
||||
|
||||
extraFuncs["version"] = func() string { return Version }
|
||||
|
||||
if err := configTemplate.Execute(ctxName, "config", buf, ctxMap, extraFuncs); err != nil {
|
||||
log.Fatalf("failed to render config %q for host %q: %v", ctx.Host.Config, ctx.Host.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[string]interface{} {
|
||||
func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
|
||||
cluster := ctx.Cluster.Name
|
||||
|
||||
getKeyCert := func(name, funcName string) (s string, err error) {
|
||||
@ -216,14 +230,15 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
key += "/" + ctx.Host.Name
|
||||
}
|
||||
|
||||
if funcName == "tls_dir" {
|
||||
switch funcName {
|
||||
case "tls_dir":
|
||||
// needs the dir name
|
||||
dir := "/etc/tls/" + name
|
||||
|
||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q %q }}", funcName,
|
||||
dir, cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||
|
||||
} else {
|
||||
default:
|
||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
|
||||
cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||
}
|
||||
@ -231,10 +246,31 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
}
|
||||
|
||||
funcs := clusterFuncs(ctx.Cluster)
|
||||
for k, v := range map[string]interface{}{
|
||||
for k, v := range map[string]any{
|
||||
"default": func(value, defaultValue any) any {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64:
|
||||
if v != 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
default:
|
||||
if v != nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
},
|
||||
"tls_key": func(name string) (string, error) {
|
||||
return getKeyCert(name, "tls_key")
|
||||
},
|
||||
"tls_pubkey": func(name string) string {
|
||||
return fmt.Sprintf("{{ tls_pubkey %q %q }}", ctx.Cluster.Name, name)
|
||||
},
|
||||
|
||||
"tls_crt": func(name string) (s string, err error) {
|
||||
return getKeyCert(name, "tls_crt")
|
||||
@ -244,6 +280,10 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
return getKeyCert(name, "tls_dir")
|
||||
},
|
||||
|
||||
"ssh_user_ca": func(path string) (s string) {
|
||||
return fmt.Sprintf("{{ ssh_user_ca %q %q}}",
|
||||
path, cluster)
|
||||
},
|
||||
"ssh_host_keys": func(dir string) (s string) {
|
||||
return fmt.Sprintf("{{ ssh_host_keys %q %q \"\"}}",
|
||||
dir, cluster)
|
||||
@ -251,12 +291,20 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
"host_download_token": func() (s string) {
|
||||
return "{{ host_download_token }}"
|
||||
},
|
||||
"asset_download_token": func(args ...string) (s string) {
|
||||
argsStr := new(strings.Builder)
|
||||
for _, arg := range args {
|
||||
argsStr.WriteByte(' ')
|
||||
argsStr.WriteString(strconv.Quote(arg))
|
||||
}
|
||||
return "{{ asset_download_token" + argsStr.String() + " }}"
|
||||
},
|
||||
|
||||
"hosts_of_group": func() (hosts []interface{}) {
|
||||
hosts = make([]interface{}, 0)
|
||||
"hosts_of_group": func() (hosts []any) {
|
||||
hosts = make([]any, 0)
|
||||
|
||||
for _, host := range ctx.clusterConfig.Hosts {
|
||||
if host.Group != ctx.Host.Group {
|
||||
if host.Cluster == ctx.Cluster.Name && host.Group != ctx.Host.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -268,12 +316,31 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
|
||||
"hosts_of_group_count": func() (count int) {
|
||||
for _, host := range ctx.clusterConfig.Hosts {
|
||||
if host.Group == ctx.Host.Group {
|
||||
if host.Cluster == ctx.Cluster.Name && host.Group == ctx.Host.Group {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
|
||||
"shuffled_hosts_by_group": func(group string) (hosts []any) {
|
||||
for _, host := range src.Hosts {
|
||||
if host.Cluster == ctx.Cluster.Name && host.Group == group {
|
||||
hosts = append(hosts, asMap(host))
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Printf("WARNING: no hosts in group %q", group)
|
||||
return
|
||||
}
|
||||
|
||||
seed := xxhash.Sum64String(ctx.Host.Name)
|
||||
rng := rand.New(rand.NewSource(int64(seed)))
|
||||
rng.Shuffle(len(hosts), func(i, j int) { hosts[i], hosts[j] = hosts[j], hosts[i] })
|
||||
|
||||
return
|
||||
},
|
||||
} {
|
||||
funcs[k] = v
|
||||
}
|
||||
|
@ -10,18 +10,18 @@ import (
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
||||
if ctx.Host.BootstrapPods == "" {
|
||||
func (ctx *renderContext) renderStaticPods() (pods []namePod) {
|
||||
if ctx.Host.StaticPods == "" {
|
||||
return
|
||||
}
|
||||
|
||||
bootstrapPods, ok := src.BootstrapPods[ctx.Host.BootstrapPods]
|
||||
staticPods, ok := src.StaticPods[ctx.Host.StaticPods]
|
||||
if !ok {
|
||||
log.Fatalf("no bootstrap pods template named %q", ctx.Host.BootstrapPods)
|
||||
log.Fatalf("no static pods template named %q", ctx.Host.StaticPods)
|
||||
}
|
||||
|
||||
// render bootstrap pods
|
||||
parts := bytes.Split(ctx.renderHostTemplates("bootstrap-pods", bootstrapPods), []byte("\n---\n"))
|
||||
// render static pods
|
||||
parts := bytes.Split(ctx.renderHostTemplates("static-pods", staticPods), []byte("\n---\n"))
|
||||
for _, part := range parts {
|
||||
buf := bytes.NewBuffer(part)
|
||||
dec := yaml.NewDecoder(buf)
|
||||
@ -35,7 +35,7 @@ func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Fatalf("bootstrap pod %d: failed to parse: %v\n%s", n, err, str)
|
||||
log.Fatalf("static pod %d: failed to parse: %v\n%s", n, err, str)
|
||||
}
|
||||
|
||||
if len(podMap) == 0 {
|
||||
@ -43,7 +43,7 @@ func (ctx *renderContext) renderBootstrapPods() (pods []namePod) {
|
||||
}
|
||||
|
||||
if podMap["metadata"] == nil {
|
||||
log.Fatalf("bootstrap pod %d: no metadata\n%s", n, buf.String())
|
||||
log.Fatalf("static pod %d: no metadata\n%s", n, buf.String())
|
||||
}
|
||||
|
||||
md := podMap["metadata"].(map[interface{}]interface{})
|
||||
@ -63,7 +63,7 @@ func (ctx *renderContext) renderHostTemplates(setName string,
|
||||
|
||||
log.Print("rendering host templates in ", setName)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 16<<10))
|
||||
|
||||
for _, t := range templates {
|
||||
log.Print("- template: ", setName, ": ", t.Name)
|
||||
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
@ -23,7 +22,7 @@ func read(path string) (ba []byte, err error) {
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
return ioutil.ReadAll(r)
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
|
||||
|
@ -1,28 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
hostsToken = flag.String("hosts-token", "", "Token to give to access /hosts (open is none)")
|
||||
adminToken = flag.String("admin-token", "", "Token to give to access to admin actions (open is none)")
|
||||
)
|
||||
|
||||
func authorizeHosts(r *http.Request) bool {
|
||||
return authorizeToken(r, *hostsToken)
|
||||
}
|
||||
var adminToken string
|
||||
|
||||
func authorizeAdmin(r *http.Request) bool {
|
||||
return authorizeToken(r, *adminToken)
|
||||
return authorizeToken(r, adminToken)
|
||||
}
|
||||
|
||||
func authorizeToken(r *http.Request, token string) bool {
|
||||
if token == "" {
|
||||
// access is open
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
reqToken := r.Header.Get("Authorization")
|
||||
@ -34,13 +25,13 @@ func authorizeToken(r *http.Request, token string) bool {
|
||||
}
|
||||
|
||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("denied access to %s from %s", r.RequestURI, r.RemoteAddr)
|
||||
log.Printf("denied access to %s from %s", r.URL.Path, r.RemoteAddr)
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
|
||||
func requireToken(token string, handler http.Handler) http.Handler {
|
||||
func requireToken(token *string, handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if !authorizeToken(req, token) {
|
||||
if !authorizeToken(req, *token) {
|
||||
forbidden(w, req)
|
||||
return
|
||||
}
|
||||
@ -49,9 +40,5 @@ func requireToken(token string, handler http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func requireAdmin(handler http.Handler) http.Handler {
|
||||
return requireToken(*adminToken, handler)
|
||||
}
|
||||
|
||||
func requireHosts(handler http.Handler) http.Handler {
|
||||
return requireToken(*hostsToken, handler)
|
||||
return requireToken(&adminToken, handler)
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -18,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
||||
bootImg, err := ioutil.TempFile(os.TempDir(), "boot.img-")
|
||||
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -30,7 +29,7 @@ func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
|
||||
}
|
||||
|
||||
// send the result
|
||||
bootImg.Seek(0, os.SEEK_SET)
|
||||
bootImg.Seek(0, io.SeekStart)
|
||||
io.Copy(out, bootImg)
|
||||
return
|
||||
}
|
||||
@ -57,10 +56,60 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var grubSupportVersion = flag.String("grub-support", "1.0.1", "GRUB support version")
|
||||
func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (err error) {
|
||||
imgPath, err := func() (imgPath string, err error) {
|
||||
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rmTempFile(bootImg)
|
||||
|
||||
err = setupBootImage(bootImg, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = bootImg.Sync()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
imgPath = bootImg.Name() + "." + format
|
||||
|
||||
err = run("qemu-img", "convert", "-f", "raw", "-O", format, bootImg.Name(), imgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
if imgPath != "" {
|
||||
defer os.Remove(imgPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send the result
|
||||
img, err := os.Open(imgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
io.Copy(out, img)
|
||||
return
|
||||
}
|
||||
func qemuImgBootImg(format string) func(out io.Writer, ctx *renderContext) (err error) {
|
||||
return func(out io.Writer, ctx *renderContext) (err error) {
|
||||
return buildBootImgQemuConvert(out, ctx, format)
|
||||
}
|
||||
}
|
||||
|
||||
var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version")
|
||||
|
||||
func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||
path, err := ctx.distFetch("grub-support", *grubSupportVersion)
|
||||
path, err := distFetch("grub-support", *grubSupportVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -99,6 +148,7 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||
}()
|
||||
|
||||
log.Print("device: ", dev)
|
||||
syncSysToDev()
|
||||
|
||||
tempDir := bootImg.Name() + ".p1.mount"
|
||||
|
||||
@ -112,9 +162,10 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||
os.RemoveAll(tempDir)
|
||||
}()
|
||||
|
||||
err = syscall.Mount(dev+"p1", tempDir, "vfat", 0, "")
|
||||
devp1 := dev + "p1"
|
||||
err = syscall.Mount(devp1, tempDir, "vfat", 0, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount %s to %s: %v", dev+"p1", tempDir, err)
|
||||
return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -13,189 +12,8 @@ import (
|
||||
"github.com/cespare/xxhash"
|
||||
)
|
||||
|
||||
// deprecated
|
||||
func buildBootISO(out io.Writer, ctx *renderContext) error {
|
||||
tempDir, err := ioutil.TempDir("/tmp", "iso-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
cp := func(src, dst string) error {
|
||||
log.Printf("iso: adding %s as %s", src, dst)
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer in.Close()
|
||||
|
||||
outPath := filepath.Join(tempDir, dst)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
// grub
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tempDir, "grub"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||
search --set=root --file /config.yaml
|
||||
|
||||
insmod all_video
|
||||
set timeout=3
|
||||
|
||||
menuentry "Direktil" {
|
||||
linux /vmlinuz direktil.boot=DEVNAME=sr0 direktil.boot.fs=iso9660 `+ctx.CmdLine+`
|
||||
initrd /initrd
|
||||
}
|
||||
`), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coreImgPath := filepath.Join(tempDir, "grub", "core.img")
|
||||
grubCfgPath := filepath.Join(tempDir, "grub", "grub.cfg")
|
||||
|
||||
cmd := exec.Command("grub-mkstandalone",
|
||||
"--format=i386-pc",
|
||||
"--output="+coreImgPath,
|
||||
"--install-modules=linux normal iso9660 biosdisk memdisk search tar ls",
|
||||
"--modules=linux normal iso9660 biosdisk search",
|
||||
"--locales=",
|
||||
"--fonts=",
|
||||
"boot/grub/grub.cfg="+grubCfgPath,
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.Remove(coreImgPath)
|
||||
defer os.Remove(grubCfgPath)
|
||||
|
||||
out, err := os.Create(filepath.Join(tempDir, "grub", "bios.img"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
b, err := ioutil.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := out.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadFile(coreImgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := out.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// config
|
||||
cfgBytes, cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ioutil.WriteFile(filepath.Join(tempDir, "config.yaml"), cfgBytes, 0600)
|
||||
|
||||
// kernel and initrd
|
||||
type distCopy struct {
|
||||
Src []string
|
||||
Dst string
|
||||
}
|
||||
|
||||
copies := []distCopy{
|
||||
{Src: []string{"kernels", ctx.Host.Kernel}, Dst: "vmlinuz"},
|
||||
{Src: []string{"initrd", ctx.Host.Initrd}, Dst: "initrd"},
|
||||
}
|
||||
|
||||
// layers
|
||||
for _, layer := range cfg.Layers {
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
if layerVersion == "" {
|
||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
||||
}
|
||||
|
||||
copies = append(copies,
|
||||
distCopy{
|
||||
Src: []string{"layers", layer, layerVersion},
|
||||
Dst: filepath.Join("current", "layers", layer+".fs"),
|
||||
})
|
||||
}
|
||||
|
||||
for _, copy := range copies {
|
||||
outPath, err := ctx.distFetch(copy.Src...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cp(outPath, copy.Dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build the ISO
|
||||
mkisofs, err := exec.LookPath("genisoimage")
|
||||
if err != nil {
|
||||
mkisofs, err = exec.LookPath("mkisofs")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(mkisofs,
|
||||
"-quiet",
|
||||
"-joliet",
|
||||
"-joliet-long",
|
||||
"-rock",
|
||||
"-translation-table",
|
||||
"-no-emul-boot",
|
||||
"-boot-load-size", "4",
|
||||
"-boot-info-table",
|
||||
"-eltorito-boot", "grub/bios.img",
|
||||
"-eltorito-catalog", "grub/boot.cat",
|
||||
tempDir,
|
||||
)
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func buildBootISOv2(out io.Writer, ctx *renderContext) (err error) {
|
||||
tempDir, err := ioutil.TempDir("/tmp", "iso-v2-")
|
||||
func buildBootISO(out io.Writer, ctx *renderContext) (err error) {
|
||||
tempDir, err := os.MkdirTemp("/tmp", "iso-v2-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -251,7 +69,7 @@ func buildBootISOv2(out io.Writer, ctx *renderContext) (err error) {
|
||||
f.Write([]byte("direktil marker file\n"))
|
||||
f.Close()
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||
err = os.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||
search --set=root --file /`+tag+`
|
||||
|
||||
insmod all_video
|
||||
@ -294,7 +112,7 @@ menuentry "Direktil" {
|
||||
|
||||
defer out.Close()
|
||||
|
||||
b, err := ioutil.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
||||
b, err := os.ReadFile("/usr/lib/grub/i386-pc/cdboot.img")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -303,7 +121,7 @@ menuentry "Direktil" {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadFile(coreImgPath)
|
||||
b, err = os.ReadFile(coreImgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -320,7 +138,7 @@ menuentry "Direktil" {
|
||||
|
||||
// kernel and initrd
|
||||
buildRes(fetchKernel, "vmlinuz")
|
||||
buildRes(buildInitrdV2, "initrd")
|
||||
buildRes(buildInitrd, "initrd")
|
||||
|
||||
// build the ISO
|
||||
mkisofs, err := exec.LookPath("genisoimage")
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@ -32,12 +31,12 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
||||
}
|
||||
|
||||
// kernel
|
||||
kernelPath, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kernelBytes, err := ioutil.ReadFile(kernelPath)
|
||||
kernelBytes, err := os.ReadFile(kernelPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -49,7 +48,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
||||
|
||||
// initrd
|
||||
initrd := new(bytes.Buffer)
|
||||
err = buildInitrdV2(initrd, ctx)
|
||||
err = buildInitrd(initrd, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -93,12 +92,12 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
||||
}
|
||||
|
||||
// kernel
|
||||
kernelPath, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kernelBytes, err := ioutil.ReadFile(kernelPath)
|
||||
kernelBytes, err := os.ReadFile(kernelPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -110,7 +109,7 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
||||
|
||||
// initrd
|
||||
initrd := new(bytes.Buffer)
|
||||
err = buildInitrdV2(initrd, ctx)
|
||||
err = buildInitrd(initrd, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -2,21 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.tech/direktil/pkg/cpiocat"
|
||||
)
|
||||
|
||||
var initrdV2 = flag.String("initrd-v2", "2.1.0", "initrd V2 version (temporary flag)") // FIXME
|
||||
|
||||
func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
||||
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
||||
|
||||
@ -34,16 +34,21 @@ func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderCo
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
||||
func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
||||
_, cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cat := cpiocat.New(out)
|
||||
zout, err := zstd.NewWriter(out, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(12)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("zstd writer setup failed: %w", err)
|
||||
}
|
||||
|
||||
cat := cpiocat.New(zout)
|
||||
|
||||
// initrd
|
||||
initrdPath, err := ctx.distFetch("initrd", *initrdV2)
|
||||
initrdPath, err := distFetch("initrd", ctx.Host.Initrd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -55,7 +60,7 @@ func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
||||
case "modules":
|
||||
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
modulesPath, err := ctx.distFetch("layers", layer, layerVersion)
|
||||
modulesPath, err := distFetch("layers", layer, layerVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -69,26 +74,82 @@ func buildInitrdV2(out io.Writer, ctx *renderContext) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cat.AppendBytes(cfgBytes, "config.yaml", 0600)
|
||||
cat.AppendBytes(cfgBytes, "config.yaml", 0o600)
|
||||
|
||||
// ssh keys
|
||||
// FIXME we want a bootstrap-stage key instead of the real host key
|
||||
cat.AppendBytes(cfg.FileContent("/etc/ssh/ssh_host_rsa_key"), "id_rsa", 0600)
|
||||
cat.AppendDir("/etc", 0o755)
|
||||
cat.AppendDir("/etc/ssh", 0o700)
|
||||
|
||||
return cat.Close()
|
||||
// XXX do we want bootstrap-stage keys instead of the real host key?
|
||||
for _, format := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
|
||||
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
||||
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
||||
}
|
||||
|
||||
// ssh user CA
|
||||
userCA, err := sshCAPubKey(ctx.Host.ClusterName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get SSH user CA: %w", err)
|
||||
}
|
||||
|
||||
cat.AppendBytes(userCA, "user_ca.pub", 0600)
|
||||
|
||||
if err = cat.Close(); err != nil {
|
||||
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) {
|
||||
arch := tar.NewWriter(out)
|
||||
defer arch.Close()
|
||||
|
||||
ca, err := getUsableClusterCA(ctx.Host.ClusterName, "boot-signer")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signer, err := ca.ParseKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hash := crypto.SHA512
|
||||
|
||||
sign := func(name string, digest []byte) (err error) {
|
||||
sigBytes, err := signer.Sign(nil, digest, hash)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signing to %s failed: %w", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = arch.WriteHeader(&tar.Header{
|
||||
Name: name,
|
||||
Size: int64(len(sigBytes)),
|
||||
Mode: 0o644,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(arch, bytes.NewReader(sigBytes))
|
||||
return
|
||||
}
|
||||
|
||||
// config
|
||||
cfgBytes, cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = arch.WriteHeader(&tar.Header{Name: "config.yaml", Size: int64(len(cfgBytes))})
|
||||
err = arch.WriteHeader(&tar.Header{
|
||||
Name: "config.yaml",
|
||||
Size: int64(len(cfgBytes)),
|
||||
Mode: 0o600,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -98,10 +159,19 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
h := hash.New()
|
||||
h.Write(cfgBytes)
|
||||
err = sign("config.yaml.sig", h.Sum(nil))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// layers
|
||||
for _, layer := range cfg.Layers {
|
||||
if layer == "modules" {
|
||||
continue // modules are with the kernel in boot v2
|
||||
continue // modules are in the initrd with boot v2
|
||||
}
|
||||
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
@ -109,7 +179,7 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
||||
}
|
||||
|
||||
outPath, err := ctx.distFetch("layers", layer, layerVersion)
|
||||
outPath, err := distFetch("layers", layer, layerVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -126,14 +196,24 @@ func buildBootstrap(out io.Writer, ctx *renderContext) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
h := hash.New()
|
||||
reader := io.TeeReader(f, h)
|
||||
|
||||
if err = arch.WriteHeader(&tar.Header{
|
||||
Name: layer + ".fs",
|
||||
Size: stat.Size(),
|
||||
Mode: 0o600,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(arch, f)
|
||||
_, err = io.Copy(arch, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
digest := h.Sum(nil)
|
||||
err = sign(layer+".fs.sig", digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
cacheCleanDelay = flag.Duration("cache-clean-delay", 10*time.Minute, "Time between cache cleanups")
|
||||
)
|
||||
|
||||
func casCleaner() {
|
||||
for range time.Tick(*cacheCleanDelay) {
|
||||
if !wPublicState.Get().Store.Open {
|
||||
continue
|
||||
}
|
||||
|
||||
err := cleanCAS()
|
||||
if err != nil {
|
||||
log.Print("warn: couldn't clean cache: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanCAS() error {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
activeTags := make([]string, len(cfg.Hosts))
|
||||
|
||||
for i, host := range cfg.Hosts {
|
||||
// FIXME ugly hack, same as in dir2config
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err := newRenderContext(host, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := ctx.Tag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activeTags[i] = tag
|
||||
}
|
||||
|
||||
tags, err := casStore.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(activeTags)
|
||||
|
||||
for _, tag := range tags {
|
||||
idx := sort.SearchStrings(activeTags, tag)
|
||||
|
||||
if idx < len(activeTags) && activeTags[idx] == tag {
|
||||
continue
|
||||
}
|
||||
|
||||
// tag is not present in active tags
|
||||
log.Print("cache cleaner: removing tag ", tag)
|
||||
if err := casStore.Remove(tag); err != nil {
|
||||
log.Printf("cache cleaner: failed to remove tag %s: %v", tag, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,19 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.tech/direktil/pkg/bootstrapconfig"
|
||||
"novit.tech/direktil/pkg/config"
|
||||
)
|
||||
|
||||
func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
||||
getKey := func(cluster, caName string) (key crypto.Signer, err error) {
|
||||
ca, err := getUsableClusterCA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key, err = helpers.ParsePrivateKeyPEM(ca.Key)
|
||||
return
|
||||
}
|
||||
getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc KeyCert, err error) {
|
||||
certReq := &csr.CertificateRequest{
|
||||
KeyRequest: csr.NewKeyRequest(),
|
||||
@ -28,16 +45,50 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
||||
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg)
|
||||
}
|
||||
|
||||
hash := func(plain, seed []byte, hashAlg string) (hashed string, err error) {
|
||||
switch hashAlg {
|
||||
case "sha512crypt":
|
||||
return sha512crypt(plain, seed)
|
||||
|
||||
case "bootstrap":
|
||||
return bootstrapconfig.JoinSeedAndHash(seed, bootstrapconfig.PasswordHashFromSeed(seed, plain)), nil
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unknown hash alg: %q", hashAlg)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"password": func(cluster, name string) (password string, err error) {
|
||||
password, _, err = clusterPasswords.Get(cluster + "/" + name)
|
||||
"quote": strconv.Quote,
|
||||
|
||||
"password": func(cluster, name, hashAlg string) (password string, err error) {
|
||||
key := cluster + "/" + name
|
||||
|
||||
seed, err := seeds.GetOrCreate(key, func() (seed []byte, err error) {
|
||||
seed = make([]byte, 16)
|
||||
_, err = rand.Read(seed)
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get seed: %w", err)
|
||||
}
|
||||
|
||||
password, err = clusterPasswords.GetOrCreate(key, func() (password string, err error) {
|
||||
raw := make([]byte, 10)
|
||||
_, err = rand.Read(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate password: %w", err)
|
||||
}
|
||||
|
||||
password = strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(raw))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(password) == 0 {
|
||||
err = fmt.Errorf("password %q not defined for cluster %q", name, cluster)
|
||||
}
|
||||
return
|
||||
|
||||
return hash([]byte(password), seed, hashAlg)
|
||||
},
|
||||
|
||||
"token": getOrCreateClusterToken,
|
||||
@ -94,6 +145,22 @@ func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
|
||||
return
|
||||
},
|
||||
|
||||
"tls_pubkey": func(cluster, caName string) (s string, err error) {
|
||||
priv, err := getKey(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ba, err := x509.MarshalPKIXPublicKey(priv.Public())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("marshal public key failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
s = base64.StdEncoding.EncodeToString(ba)
|
||||
return
|
||||
},
|
||||
|
||||
"tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
|
15
cmd/dkl-local-server/html.go
Normal file
15
cmd/dkl-local-server/html.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
func htmlHeader(title string) string {
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + title + `</title>
|
||||
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
||||
</head>
|
||||
<body><h1>` + title + `</h1>
|
||||
`
|
||||
}
|
||||
|
||||
var htmlFooter = `</body>
|
||||
</html>`
|
@ -10,5 +10,7 @@ var (
|
||||
ErrNotFound = httperr.NotFound
|
||||
ErrUnauthorized = httperr.StdStatus(http.StatusUnauthorized)
|
||||
ErrForbidden = httperr.StdStatus(http.StatusForbidden)
|
||||
ErrInternal = httperr.StdStatus(http.StatusInternalServerError)
|
||||
ErrInvalidToken = httperr.NewStd(1000, http.StatusForbidden, "invalid token")
|
||||
ErrStoreLocked = httperr.NewStd(1001, http.StatusServiceUnavailable, "store is locked")
|
||||
)
|
||||
|
@ -2,13 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
cpio "github.com/cavaliergopher/cpio"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@ -28,84 +24,3 @@ func renderConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, as
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildInitrd(out io.Writer, ctx *renderContext) error {
|
||||
_, cfg, err := ctx.Config()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send initrd basis
|
||||
initrdPath, err := ctx.distFetch("initrd", ctx.Host.Initrd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeFile(out, initrdPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and our extra archive
|
||||
archive := cpio.NewWriter(out)
|
||||
|
||||
// - required dirs
|
||||
for _, dir := range []string{
|
||||
"boot",
|
||||
"boot/current",
|
||||
"boot/current/layers",
|
||||
} {
|
||||
archive.WriteHeader(&cpio.Header{
|
||||
Name: dir,
|
||||
Mode: cpio.FileMode(0600 | os.ModeDir),
|
||||
})
|
||||
}
|
||||
|
||||
// - the layers
|
||||
for _, layer := range cfg.Layers {
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
if layerVersion == "" {
|
||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
||||
}
|
||||
|
||||
path, err := ctx.distFetch("layers", layer, layerVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archive.WriteHeader(&cpio.Header{
|
||||
Name: "boot/current/layers/" + layer + ".fs",
|
||||
Mode: 0600,
|
||||
Size: stat.Size(),
|
||||
})
|
||||
|
||||
if err = writeFile(archive, path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// - the configuration
|
||||
ba, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archive.WriteHeader(&cpio.Header{
|
||||
Name: "boot/config.yaml",
|
||||
Mode: 0600,
|
||||
Size: int64(len(ba)),
|
||||
})
|
||||
|
||||
archive.Write(ba)
|
||||
|
||||
// finalize the archive
|
||||
archive.Flush()
|
||||
archive.Close()
|
||||
return nil
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error {
|
||||
path, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||
path, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
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) {
|
||||
path, err := ctx.distFetch("kernels", ctx.Host.Kernel)
|
||||
path, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -5,14 +5,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
swaggerui "github.com/mcluseau/go-swagger-ui"
|
||||
"m.cluseau.fr/go/watchable/streamsse"
|
||||
|
||||
"novit.tech/direktil/pkg/cas"
|
||||
|
||||
dlshtml "novit.tech/direktil/local-server/html"
|
||||
"novit.tech/direktil/local-server/pkg/apiutils"
|
||||
)
|
||||
@ -21,15 +18,15 @@ const (
|
||||
etcDir = "/etc/direktil"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
var (
|
||||
address = flag.String("address", ":7606", "HTTP listen address")
|
||||
tlsAddress = flag.String("tls-address", "", "HTTPS listen address")
|
||||
certFile = flag.String("tls-cert", etcDir+"/server.crt", "Server TLS certificate")
|
||||
keyFile = flag.String("tls-key", etcDir+"/server.key", "Server TLS key")
|
||||
|
||||
autoUnlock = flag.String("auto-unlock", "", "Auto-unlock store (testing only!)")
|
||||
|
||||
casStore cas.Store
|
||||
autoUnlock = flag.String("auto-unlock", "", "Auto-unlock store (testing only!) env: DLS_AUTO_UNLOCK")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -41,6 +38,9 @@ func main() {
|
||||
log.Fatal("no listen address given")
|
||||
}
|
||||
|
||||
log.Print("Direktil local-server version ", Version)
|
||||
wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
|
||||
|
||||
computeUIHash()
|
||||
|
||||
openSecretStore()
|
||||
@ -52,20 +52,17 @@ func main() {
|
||||
}
|
||||
if autoUnlock != "" {
|
||||
log.Printf("auto-unlocking the store")
|
||||
err := unlockSecretStore([]byte(autoUnlock))
|
||||
err := unlockSecretStore("test", []byte(autoUnlock))
|
||||
if err.Any() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Print("store auto-unlocked, admin token is ", *adminToken)
|
||||
log.Print("store auto-unlocked")
|
||||
}
|
||||
|
||||
os.Setenv("DLS_AUTO_UNLOCK", "")
|
||||
}
|
||||
|
||||
casStore = cas.NewDir(filepath.Join(*dataDir, "cache"))
|
||||
go casCleaner()
|
||||
|
||||
apiutils.Setup(func() {
|
||||
registerWS(restful.DefaultContainer)
|
||||
})
|
||||
@ -75,6 +72,7 @@ func main() {
|
||||
staticHandler := http.FileServer(http.FS(dlshtml.FS))
|
||||
http.Handle("/favicon.ico", staticHandler)
|
||||
http.Handle("/ui/", staticHandler)
|
||||
http.Handle("/dist/", http.StripPrefix("/dist/", upstreamServer{}))
|
||||
|
||||
http.Handle("/public-state", streamsse.StreamHandler(wPublicState))
|
||||
http.Handle("/state", requireAdmin(streamsse.StreamHandler(wState)))
|
||||
|
23
cmd/dkl-local-server/parsers_test.go
Normal file
23
cmd/dkl-local-server/parsers_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Example_parseCertDuration() {
|
||||
now := time.Date(2020, time.April, 28, 12, 30, 0, 0, time.UTC)
|
||||
|
||||
fmt.Println(parseCertDuration("", now))
|
||||
fmt.Println(parseCertDuration("hi!", now))
|
||||
fmt.Println(parseCertDuration("-2d3h", now))
|
||||
fmt.Println(parseCertDuration("2d3h", now))
|
||||
fmt.Println(parseCertDuration("+1y-1s", now))
|
||||
|
||||
// output:
|
||||
// 0001-01-01 00:00:00 +0000 UTC <nil>
|
||||
// 0001-01-01 00:00:00 +0000 UTC invalid duration: "hi!"
|
||||
// 2020-04-26 09:30:00 +0000 UTC <nil>
|
||||
// 2020-04-30 15:30:00 +0000 UTC <nil>
|
||||
// 2021-04-28 12:29:59 +0000 UTC <nil>
|
||||
}
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@ -9,9 +10,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
@ -34,12 +37,7 @@ type renderContext struct {
|
||||
}
|
||||
|
||||
func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string,
|
||||
create func(out io.Writer, ctx *renderContext) error) error {
|
||||
|
||||
tag, err := ctx.Tag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
create func(out io.Writer, ctx *renderContext) error) (err error) {
|
||||
|
||||
ctx.CmdLine = r.URL.Query().Get(cmdlineParam.Data().Name)
|
||||
|
||||
@ -48,19 +46,26 @@ func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what
|
||||
}
|
||||
|
||||
// get it or create it
|
||||
content, meta, err := casStore.GetOrCreate(tag, what, func(out io.Writer) error {
|
||||
log.Printf("building %s for %q", what, ctx.Host.Name)
|
||||
return create(out, ctx)
|
||||
})
|
||||
|
||||
outfile, err := os.CreateTemp("/tmp", "dls."+what+".")
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Remove(outfile.Name())
|
||||
|
||||
log.Printf("building %s for %q", what, ctx.Host.Name)
|
||||
err = create(outfile, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// serve it
|
||||
log.Printf("sending %s for %q", what, ctx.Host.Name)
|
||||
http.ServeContent(w, r, what, meta.ModTime(), content)
|
||||
return nil
|
||||
|
||||
outfile.Seek(0, io.SeekStart)
|
||||
io.Copy(w, outfile)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sslConfigFromLocalConfig(cfg *localconfig.Config) (sslCfg *cfsslconfig.Config, err error) {
|
||||
@ -109,6 +114,7 @@ func (ctx *renderContext) BootstrapConfig() (ba []byte, cfg *bsconfig.Config, er
|
||||
|
||||
cfg = &bsconfig.Config{}
|
||||
if err = yaml.Unmarshal(ba, cfg); err != nil {
|
||||
log.Print("invalid bootstrap config yaml:\n", string(ba))
|
||||
return
|
||||
}
|
||||
|
||||
@ -133,7 +139,7 @@ func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *renderContext) distFilePath(path ...string) string {
|
||||
func distFilePath(path ...string) string {
|
||||
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
||||
}
|
||||
|
||||
@ -147,7 +153,7 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
|
||||
enc := yaml.NewEncoder(h)
|
||||
|
||||
for _, o := range []interface{}{cfg, ctx} {
|
||||
for _, o := range []any{cfg, ctx} {
|
||||
if err := enc.Encode(o); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -156,25 +162,29 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func asMap(v interface{}) map[string]interface{} {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
if err := yaml.Unmarshal(ba, result); err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
funcs := templateFuncs(ctx.SSLConfig)
|
||||
|
||||
for name, method := range map[string]any{
|
||||
"host_ip": func() (s string) {
|
||||
return ctx.Host.IPs[0]
|
||||
},
|
||||
"host_name": func() (s string) {
|
||||
return ctx.Host.Name
|
||||
},
|
||||
"machine_id": func() (s string) {
|
||||
ba := sha1.Sum([]byte(ctx.Host.ClusterName + "/" + ctx.Host.Name))
|
||||
return hex.EncodeToString(ba[:])
|
||||
},
|
||||
|
||||
"ssh_user_ca": func(path, cluster string) (s string, err error) {
|
||||
userCA, err := sshCAPubKey(cluster)
|
||||
return asYaml([]config.FileDef{{
|
||||
Path: path,
|
||||
Mode: 0644,
|
||||
Content: string(userCA),
|
||||
}})
|
||||
},
|
||||
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||
if host == "" {
|
||||
host = ctx.Host.Name
|
||||
@ -228,6 +238,32 @@ func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
"asset_download_token": func(asset string, params ...string) (token string, err error) {
|
||||
now := time.Now()
|
||||
exp := now.Add(24 * time.Hour) // expire in 24h by default
|
||||
if len(params) != 0 {
|
||||
exp, err = parseCertDuration(params[0], now)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
set := DownloadSet{
|
||||
Expiry: exp,
|
||||
Items: []DownloadSetItem{
|
||||
{
|
||||
Kind: "host",
|
||||
Name: ctx.Host.Name,
|
||||
Assets: []string{asset},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
privKey, _ := dlsSigningKeys()
|
||||
token = set.Signed(privKey)
|
||||
|
||||
return
|
||||
},
|
||||
} {
|
||||
|
@ -59,11 +59,11 @@ func openSecretStore() {
|
||||
var (
|
||||
unlockMutex = sync.Mutex{}
|
||||
|
||||
ErrStoreAlreadyUnlocked = httperr.NewStd(http.StatusConflict, 1, "store already unlocked")
|
||||
ErrInvalidPassphrase = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase")
|
||||
ErrStoreAlreadyUnlocked = httperr.NewStd(1, http.StatusConflict, "store already unlocked")
|
||||
ErrInvalidPassphrase = httperr.NewStd(2, http.StatusBadRequest, "invalid passphrase")
|
||||
)
|
||||
|
||||
func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
||||
func unlockSecretStore(name string, passphrase []byte) (err httperr.Error) {
|
||||
unlockMutex.Lock()
|
||||
defer unlockMutex.Unlock()
|
||||
|
||||
@ -72,7 +72,7 @@ func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
||||
}
|
||||
|
||||
if secStore.IsNew() {
|
||||
err := secStore.Init(passphrase)
|
||||
err := secStore.Init(name, passphrase)
|
||||
if err != nil {
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
@ -117,7 +117,7 @@ func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
||||
log.Print("wrote new admin token")
|
||||
}
|
||||
|
||||
*adminToken = token
|
||||
adminToken = token
|
||||
|
||||
{
|
||||
token, err := newToken(16)
|
||||
@ -247,6 +247,31 @@ func (s KVSecrets[T]) Data() (kvs map[string]T, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type KV[T any] struct {
|
||||
K string
|
||||
V T
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) List(prefix string) (list []KV[T], err error) {
|
||||
data, err := s.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
list = make([]KV[T], 0, len(data))
|
||||
|
||||
for k, v := range data {
|
||||
if !strings.HasPrefix(k, prefix) {
|
||||
continue
|
||||
}
|
||||
list = append(list, KV[T]{k, v})
|
||||
}
|
||||
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].K < list[j].K })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
|
||||
kvs, err := s.Data()
|
||||
if err != nil {
|
||||
@ -291,6 +316,38 @@ func (s KVSecrets[T]) Put(key string, v T) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) Del(key string) (err error) {
|
||||
secL.Lock()
|
||||
defer secL.Unlock()
|
||||
|
||||
kvs, err := s.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(kvs, key)
|
||||
err = writeSecret(s.Name, kvs)
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) GetOrCreate(key string, create func() (T, error)) (v T, err error) {
|
||||
v, found, err := s.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
v, err = create()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Put(key, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) WsList(resp *restful.Response, prefix string) {
|
||||
keys, err := s.Keys(prefix)
|
||||
if err != nil {
|
||||
@ -330,3 +387,11 @@ func (s KVSecrets[T]) WsPut(req *restful.Request, resp *restful.Response, key st
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) WsDel(req *restful.Request, resp *restful.Response, key string) {
|
||||
err := s.Del(key)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/certinfo"
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/helpers/derhelpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
)
|
||||
|
||||
@ -43,7 +44,7 @@ func loadSecretData(config *config.Config) (sd *SecretData, err error) {
|
||||
config: config,
|
||||
}
|
||||
|
||||
ba, err := ioutil.ReadFile(secretDataPath())
|
||||
ba, err := os.ReadFile(secretDataPath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
@ -74,3 +75,33 @@ func checkCertUsable(certPEM []byte) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
39
cmd/dkl-local-server/sha512crypt.go
Normal file
39
cmd/dkl-local-server/sha512crypt.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
crypthash "github.com/sergeymakinen/go-crypt/hash"
|
||||
"github.com/sergeymakinen/go-crypt/sha512"
|
||||
)
|
||||
|
||||
// for some reason, no implementation of crypt's sha512 is clean enough :(
|
||||
|
||||
func sha512crypt(password, seed []byte) (string, error) {
|
||||
// loose salt entropy because of character restriction in the salt
|
||||
salt := []byte(base64.RawStdEncoding.EncodeToString(seed))[:sha512.MaxSaltLength]
|
||||
// - base64 allows '+' where the salt accepts '.'
|
||||
for i, c := range salt {
|
||||
if c == '+' {
|
||||
salt[i] = '.'
|
||||
}
|
||||
}
|
||||
|
||||
scheme := struct {
|
||||
HashPrefix string
|
||||
Rounds uint32 `hash:"param:rounds,omitempty"`
|
||||
Salt []byte
|
||||
Sum [86]byte
|
||||
}{
|
||||
HashPrefix: sha512.Prefix,
|
||||
Rounds: sha512.DefaultRounds,
|
||||
Salt: salt,
|
||||
}
|
||||
|
||||
key, err := sha512.Key([]byte(password), scheme.Salt, scheme.Rounds)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
crypthash.LittleEndianEncoding.Encode(scheme.Sum[:], key)
|
||||
return crypthash.Marshal(scheme)
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"}
|
||||
@ -42,7 +43,7 @@ genLoop:
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
outFile, err := ioutil.TempFile("/tmp", "dls-key.")
|
||||
outFile, err := os.CreateTemp("/tmp", "dls-key.")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -70,12 +71,12 @@ genLoop:
|
||||
return
|
||||
}
|
||||
|
||||
privKey, err = ioutil.ReadFile(outPath)
|
||||
privKey, err = os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey, err = ioutil.ReadFile(outPath + ".pub")
|
||||
pubKey, err = os.ReadFile(outPath + ".pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -105,64 +106,106 @@ genLoop:
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenDSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey := &dsa.PrivateKey{}
|
||||
var sshCAKeys = KVSecrets[string]{"ssh-ca-keys"}
|
||||
|
||||
err = dsa.GenerateParameters(&privKey.Parameters, rand.Reader, dsa.L1024N160)
|
||||
func sshCAKey(cluster string) (caKeyPem string, err error) {
|
||||
storeKey := "clusters/" + cluster
|
||||
caKeyPem, _, err = sshCAKeys.Get(storeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = dsa.GenerateKey(privKey, rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if caKeyPem == "" {
|
||||
_, pk, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err = asn1.Marshal(*privKey)
|
||||
//data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pemBlock, err := ssh.MarshalPrivateKey(crypto.PrivateKey(pk), "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubKey = privKey.PublicKey
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenRSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = x509.MarshalPKCS1PrivateKey(privKey)
|
||||
pubKey = privKey.Public()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenECDSA() (data []byte, pubKey interface{}, err error) {
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey = privKey.Public()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshKeyGenED25519() (data []byte, pubKey interface{}, err error) {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
|
||||
data, err = x509.MarshalPKCS8PrivateKey(privKey)
|
||||
if err != nil {
|
||||
return
|
||||
caKeyPem = string(pem.EncodeToMemory(pemBlock))
|
||||
sshCAKeys.Put(storeKey, caKeyPem)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sshCAPubKey(cluster string) (pubKey []byte, err error) {
|
||||
keyPem, err := sshCAKey(cluster)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k, err := ssh.ParsePrivateKey([]byte(keyPem))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey = ssh.MarshalAuthorizedKey(k.PublicKey())
|
||||
return
|
||||
}
|
||||
|
||||
// principal: user (login) to allow (ie: "root")
|
||||
// validity: ssh-keygen validity string (ie: "+1h", "202506280811:202506281011", ""=forever)
|
||||
// options: ssh-keygen options (ie: "force-command=/bin/date +\"%F %T\"", "source-address=192.168.1.0/24,192.168.42.0/24"
|
||||
func sshCASign(cluster string, userPubKey []byte, principal, validity string, options ...string) (cert []byte, err error) {
|
||||
caKey, err := sshCAKey(cluster)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(userPubKeyFile.Name())
|
||||
|
||||
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey))
|
||||
userPubKeyFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serial := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)
|
||||
|
||||
if validity != "" {
|
||||
cmd.Args = append(cmd.Args, "-V", validity)
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
cmd.Args = append(cmd.Args, "-O", opt)
|
||||
}
|
||||
|
||||
cmd.Args = append(cmd.Args, userPubKeyFile.Name())
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd.Stdin = bytes.NewBuffer([]byte(caKey))
|
||||
cmd.Stderr = stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssh-keygen sign failed: %s", strings.TrimSpace(stderr.String()))
|
||||
return
|
||||
}
|
||||
|
||||
certFile := userPubKeyFile.Name() + "-cert.pub"
|
||||
cert, err = os.ReadFile(certFile)
|
||||
|
||||
os.Remove(certFile)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type PublicState struct {
|
||||
UIHash string
|
||||
Store struct {
|
||||
ServerVersion string
|
||||
UIHash string
|
||||
Store struct {
|
||||
New bool
|
||||
Open bool
|
||||
}
|
||||
@ -22,6 +23,7 @@ type State struct {
|
||||
|
||||
Store struct {
|
||||
DownloadToken string
|
||||
KeyNames []string
|
||||
}
|
||||
|
||||
Clusters []ClusterState
|
||||
@ -29,6 +31,8 @@ type State struct {
|
||||
Config *localconfig.Config
|
||||
|
||||
Downloads map[string]DownloadSpec
|
||||
|
||||
HostTemplates []string
|
||||
}
|
||||
|
||||
type ClusterState struct {
|
||||
@ -43,6 +47,8 @@ type HostState struct {
|
||||
Name string
|
||||
Cluster string
|
||||
IPs []string
|
||||
|
||||
Template string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type CAState struct {
|
||||
@ -59,14 +65,21 @@ func init() {
|
||||
func updateState() {
|
||||
log.Print("updating state")
|
||||
|
||||
// store key names
|
||||
keyNames := make([]string, 0, len(secStore.Keys))
|
||||
for _, key := range secStore.Keys {
|
||||
keyNames = append(keyNames, key.Name)
|
||||
}
|
||||
|
||||
// config
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||
return
|
||||
}
|
||||
|
||||
if secStore.IsNew() || !secStore.Unlocked() {
|
||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil })
|
||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||
return
|
||||
}
|
||||
|
||||
@ -108,22 +121,44 @@ func updateState() {
|
||||
clusters = append(clusters, c)
|
||||
}
|
||||
|
||||
hosts := make([]HostState, 0, len(cfg.Hosts))
|
||||
hfts, err := hostsFromTemplate.List("")
|
||||
if err != nil {
|
||||
log.Print("failed to read hosts from template: ", err)
|
||||
}
|
||||
|
||||
hosts := make([]HostState, 0, len(cfg.Hosts)+len(hfts))
|
||||
for _, host := range cfg.Hosts {
|
||||
h := HostState{
|
||||
Name: host.Name,
|
||||
Cluster: host.ClusterName,
|
||||
IPs: host.IPs,
|
||||
}
|
||||
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
|
||||
for _, kv := range hfts {
|
||||
name, hft := kv.K, kv.V
|
||||
h := HostState{
|
||||
Name: name,
|
||||
Cluster: hft.ClusterName(cfg),
|
||||
IPs: []string{hft.IP},
|
||||
|
||||
Template: hft.Template,
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
|
||||
hostTemplates := make([]string, len(cfg.HostTemplates))
|
||||
for i, ht := range cfg.HostTemplates {
|
||||
hostTemplates[i] = ht.Name
|
||||
}
|
||||
|
||||
// done
|
||||
wState.Change(func(v *State) {
|
||||
v.HasConfig = true
|
||||
//v.Config = cfg
|
||||
v.Store.KeyNames = keyNames
|
||||
v.Clusters = clusters
|
||||
v.Hosts = hosts
|
||||
v.HostTemplates = hostTemplates
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -70,13 +72,20 @@ func (_ CA) newReq() *csr.CertificateRequest {
|
||||
}
|
||||
}
|
||||
|
||||
func (ca CA) ParseKey() (key crypto.Signer, err error) {
|
||||
return helpers.ParsePrivateKeyPEM(ca.Key)
|
||||
}
|
||||
func (ca CA) ParseCert() (cert *x509.Certificate, err error) {
|
||||
return helpers.ParseCertificatePEM(ca.Cert)
|
||||
}
|
||||
|
||||
func (ca CA) Signer(policy *config.Signing) (result *local.Signer, err error) {
|
||||
caCert, err := helpers.ParseCertificatePEM(ca.Cert)
|
||||
caCert, err := ca.ParseCert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
caKey, err := helpers.ParsePrivateKeyPEM(ca.Key)
|
||||
caKey, err := ca.ParseKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -129,7 +138,26 @@ func getUsableKeyCert(cluster, caName, name, profile, label string, req *csr.Cer
|
||||
|
||||
if found {
|
||||
if rh == kc.ReqHash {
|
||||
err = checkCertUsable(kc.Cert)
|
||||
err = func() (err error) {
|
||||
err = checkCertUsable(kc.Cert)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(ca.Cert) {
|
||||
panic("unexpected invalid CA certificate at this point")
|
||||
}
|
||||
|
||||
certBlock, _ := pem.Decode(kc.Cert)
|
||||
cert, err := x509.ParseCertificate(certBlock.Bytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cert.Verify(x509.VerifyOptions{Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}})
|
||||
return
|
||||
}()
|
||||
if err == nil {
|
||||
return // all good, no need to create or renew
|
||||
}
|
||||
|
63
cmd/dkl-local-server/udev.go
Normal file
63
cmd/dkl-local-server/udev.go
Normal file
@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Simulate a udev run for our needs
|
||||
func syncSysToDev() {
|
||||
// loop devices
|
||||
sysPaths, _ := filepath.Glob("/sys/devices/virtual/block/loop*/**/dev")
|
||||
for _, sysPath := range sysPaths {
|
||||
mknodBlk(sysPath)
|
||||
}
|
||||
}
|
||||
|
||||
func mknodBlk(sysPath string) {
|
||||
devPath := "/dev/" + filepath.Base(filepath.Dir(sysPath))
|
||||
if _, err := os.Stat(devPath); os.IsNotExist(err) {
|
||||
// ok
|
||||
} else if err != nil {
|
||||
log.Printf("stat %s failed: %v", devPath, err)
|
||||
return
|
||||
} else {
|
||||
return // exists
|
||||
}
|
||||
|
||||
devBytes, err := os.ReadFile(sysPath)
|
||||
if err != nil {
|
||||
log.Printf("read %s failed: %v", sysPath, err)
|
||||
return
|
||||
}
|
||||
devBytes = bytes.TrimSpace(devBytes)
|
||||
|
||||
// rust: let Some(dev) = devBytes.split_once(':').filter_map(|a,b| Some(mkdev(a.parse().ok()?, b.parse().ok()?)));
|
||||
majorStr, minorStr, ok := strings.Cut(string(devBytes), ":")
|
||||
if !ok {
|
||||
log.Printf("%s: invalid dev string: %s", sysPath, string(devBytes))
|
||||
return
|
||||
}
|
||||
major, err := strconv.ParseUint(majorStr, 10, 32)
|
||||
if err != nil {
|
||||
log.Printf("%s: invalid major: %q", sysPath, majorStr)
|
||||
return
|
||||
}
|
||||
minor, err := strconv.ParseUint(minorStr, 10, 32)
|
||||
if err != nil {
|
||||
log.Printf("%s: invalid minor: %q", sysPath, minorStr)
|
||||
return
|
||||
}
|
||||
|
||||
devMajMin := int(unix.Mkdev(uint32(major), uint32(minor)))
|
||||
|
||||
log.Printf("mknod %s b %d %d", devPath, major, minor)
|
||||
unix.Mknod(devPath, syscall.S_IFBLK|0o0600, devMajMin)
|
||||
}
|
@ -9,9 +9,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
@ -22,8 +24,22 @@ var (
|
||||
upstreamURL = flag.String("upstream", "https://dkl.novit.io/dist", "Upstream server for dist elements")
|
||||
)
|
||||
|
||||
func (ctx *renderContext) distFetch(path ...string) (outPath string, err error) {
|
||||
outPath = ctx.distFilePath(path...)
|
||||
type upstreamServer struct{}
|
||||
|
||||
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 {
|
||||
return
|
||||
|
@ -7,18 +7,14 @@ import (
|
||||
)
|
||||
|
||||
func adminAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
tokenAuth(req, resp, chain, *adminToken)
|
||||
}
|
||||
|
||||
func hostsAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
tokenAuth(req, resp, chain, *hostsToken, *adminToken)
|
||||
tokenAuth(req, resp, chain, adminToken)
|
||||
}
|
||||
|
||||
func tokenAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain, allowedTokens ...string) {
|
||||
token := getToken(req)
|
||||
|
||||
for _, allowedToken := range allowedTokens {
|
||||
if allowedToken == "" || token == allowedToken {
|
||||
if allowedToken != "" && token == allowedToken {
|
||||
chain.ProcessFilter(req, resp)
|
||||
return
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
@ -55,11 +57,22 @@ func getUsableClusterCA(cluster, name string) (ca CA, err error) {
|
||||
if checkErr != nil {
|
||||
log.Infof("cluster %s: CA %s: regenerating certificate: %v", cluster, name, checkErr)
|
||||
|
||||
prevCerts, _ := helpers.ParseCertificatesPEM(ca.Cert)
|
||||
|
||||
err = ca.RenewCert()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("renew: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, cert := range prevCerts {
|
||||
if cert.NotAfter.After(now) {
|
||||
continue
|
||||
}
|
||||
certPEM := helpers.EncodeCertificatePEM(cert)
|
||||
ca.Cert = append(ca.Cert, certPEM...)
|
||||
}
|
||||
|
||||
err = clusterCAs.Put(key, ca)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
var seeds = newClusterSecretKV[[]byte]("seeds")
|
||||
|
||||
var clusterPasswords = newClusterSecretKV[string]("passwords")
|
||||
|
||||
func wsClusterPasswords(req *restful.Request, resp *restful.Response) {
|
||||
|
@ -1,10 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/mime"
|
||||
@ -121,3 +129,203 @@ func wsClusterSignedCert(req *restful.Request, resp *restful.Response) {
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(clusterName+"_"+caName+"_"+url.PathEscape(name)+".crt"))
|
||||
resp.Write(kc.Cert)
|
||||
}
|
||||
|
||||
type SSHSignReq struct {
|
||||
PubKey string
|
||||
Principal string
|
||||
Validity string
|
||||
Options []string
|
||||
}
|
||||
|
||||
func wsClusterSSHUserCAPubKey(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
pubkey, err := sshCAPubKey(clusterName)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(pubkey)
|
||||
}
|
||||
|
||||
func wsClusterSSHUserCASign(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
signReq := SSHSignReq{}
|
||||
err := req.ReadEntity(&signReq)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Truncate(time.Second)
|
||||
notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
|
||||
if err != nil {
|
||||
wsError(resp, fmt.Errorf("invalid validity: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
const sshTimestamp = "20060102150405Z"
|
||||
|
||||
validity := notBefore.Format(sshTimestamp) + ":"
|
||||
if notAfter.IsZero() {
|
||||
validity += "forever"
|
||||
} else {
|
||||
validity += notAfter.Format(sshTimestamp)
|
||||
}
|
||||
|
||||
log.Printf("sign ssh public key, validity %s -> %s", signReq.Validity, validity)
|
||||
|
||||
cert, err := sshCASign(clusterName, []byte(signReq.PubKey), signReq.Principal, validity, signReq.Options...)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(cert)
|
||||
}
|
||||
|
||||
type KubeSignReq struct {
|
||||
CSR string
|
||||
User string
|
||||
Group string
|
||||
Validity string
|
||||
}
|
||||
|
||||
func wsClusterKubeCASign(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
signReq := KubeSignReq{}
|
||||
err := req.ReadEntity(&signReq)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Truncate(time.Second)
|
||||
notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
|
||||
if err != nil {
|
||||
wsError(resp, fmt.Errorf("invalid validity: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var names []csr.Name
|
||||
if signReq.Group != "" {
|
||||
names = []csr.Name{{O: signReq.Group}}
|
||||
}
|
||||
|
||||
ca, err := getUsableClusterCA(clusterName, "cluster")
|
||||
if err != nil {
|
||||
wsError(resp, fmt.Errorf("get cluster CA failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
caSigner, err := ca.Signer(&config.Signing{
|
||||
Default: &config.SigningProfile{
|
||||
Usage: []string{"client auth"},
|
||||
Expiry: notAfter.Sub(now),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
csr := signer.SignRequest{
|
||||
Request: signReq.CSR,
|
||||
Subject: &signer.Subject{
|
||||
CN: signReq.User,
|
||||
Names: names,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
}
|
||||
|
||||
cert, err := caSigner.Sign(csr)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(cert)
|
||||
}
|
||||
|
||||
func parseCertDurationRange(d string, now time.Time) (notBefore, notAfter time.Time, err error) {
|
||||
if d == "" {
|
||||
return
|
||||
}
|
||||
|
||||
d1, d2, ok := strings.Cut(d, ":")
|
||||
|
||||
if ok {
|
||||
notBefore, err = parseCertDuration(d1, now)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
notAfter, err = parseCertDuration(d2, now)
|
||||
} else {
|
||||
notAfter, err = parseCertDuration(d, now)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if notBefore.IsZero() {
|
||||
notBefore = now.Add(-5 * time.Minute)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var durRegex = regexp.MustCompile("^([+-]?)([0-9]+)([yMdwhms])")
|
||||
|
||||
func parseCertDuration(d string, now time.Time) (t time.Time, err error) {
|
||||
if d == "" {
|
||||
return
|
||||
}
|
||||
|
||||
direction := 1
|
||||
t = now
|
||||
|
||||
for d != "" {
|
||||
match := durRegex.FindStringSubmatch(d)
|
||||
if match == nil {
|
||||
t = time.Time{}
|
||||
err = errors.New("invalid duration: " + strconv.Quote(d))
|
||||
return
|
||||
}
|
||||
|
||||
d = d[len(match[0]):]
|
||||
|
||||
switch match[1] {
|
||||
case "+":
|
||||
direction = 1
|
||||
case "-":
|
||||
direction = -1
|
||||
}
|
||||
|
||||
qty, _ := strconv.Atoi(match[2])
|
||||
unit := match[3]
|
||||
|
||||
switch unit {
|
||||
case "y":
|
||||
t = t.AddDate(qty*direction, 0, 0)
|
||||
case "M":
|
||||
t = t.AddDate(0, qty*direction, 0)
|
||||
case "d":
|
||||
t = t.AddDate(0, 0, qty*direction)
|
||||
case "w":
|
||||
t = t.AddDate(0, 0, 7*qty*direction)
|
||||
case "h":
|
||||
t = t.Add(time.Duration(qty*direction) * time.Hour)
|
||||
case "m":
|
||||
t = t.Add(time.Duration(qty*direction) * time.Minute)
|
||||
case "s":
|
||||
t = t.Add(time.Duration(qty*direction) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -25,7 +24,7 @@ func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
}
|
||||
|
||||
func writeNewConfig(reader io.Reader) (err error) {
|
||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
||||
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
292
cmd/dkl-local-server/ws-download-set.go
Normal file
292
cmd/dkl-local-server/ws-download-set.go
Normal file
@ -0,0 +1,292 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/pierrec/lz4"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
)
|
||||
|
||||
func globMatch(pattern, value string) bool {
|
||||
ok, _ := filepath.Match(pattern, value)
|
||||
return ok
|
||||
}
|
||||
|
||||
type DownloadSet struct {
|
||||
Expiry time.Time
|
||||
Items []DownloadSetItem
|
||||
}
|
||||
|
||||
func (s DownloadSet) Contains(kind, name, asset string) bool {
|
||||
for _, item := range s.Items {
|
||||
if item.Kind == kind && globMatch(item.Name, name) &&
|
||||
slices.Contains(item.Assets, asset) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s DownloadSet) Encode() string {
|
||||
buf := new(strings.Builder)
|
||||
s.EncodeTo(buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (s DownloadSet) EncodeTo(buf *strings.Builder) {
|
||||
buf.WriteString(strconv.FormatInt(s.Expiry.Unix(), 16))
|
||||
|
||||
for _, item := range s.Items {
|
||||
buf.WriteByte('|')
|
||||
item.EncodeTo(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DownloadSet) Decode(encoded string) (err error) {
|
||||
exp, rem, _ := strings.Cut(encoded, "|")
|
||||
|
||||
expUnix, err := strconv.ParseInt(exp, 16, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Expiry = time.Unix(expUnix, 0)
|
||||
|
||||
if rem == "" {
|
||||
s.Items = nil
|
||||
} else {
|
||||
itemStrs := strings.Split(rem, "|")
|
||||
s.Items = make([]DownloadSetItem, len(itemStrs))
|
||||
for i, itemStr := range itemStrs {
|
||||
s.Items[i].Decode(itemStr)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s DownloadSet) Signed(privKey ed25519.PrivateKey) string {
|
||||
buf := new(bytes.Buffer)
|
||||
{
|
||||
setBytes := []byte(s.Encode())
|
||||
|
||||
w := lz4.NewWriter(buf)
|
||||
w.Write(setBytes)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
setBytes := buf.Bytes()
|
||||
sig := ed25519.Sign(privKey, setBytes)
|
||||
|
||||
buf = bytes.NewBuffer(make([]byte, 0, 1+len(sig)+len(setBytes)))
|
||||
buf.WriteByte(byte(len(sig)))
|
||||
buf.Write(sig)
|
||||
buf.Write(setBytes)
|
||||
|
||||
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
return enc.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
type DownloadSetItem struct {
|
||||
Kind string
|
||||
Name string
|
||||
Assets []string
|
||||
}
|
||||
|
||||
func (i DownloadSetItem) EncodeTo(buf *strings.Builder) {
|
||||
kind := i.Kind
|
||||
switch kind {
|
||||
case "host":
|
||||
kind = "h"
|
||||
case "cluster":
|
||||
kind = "c"
|
||||
}
|
||||
|
||||
buf.WriteString(kind)
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(i.Name)
|
||||
|
||||
for _, asset := range i.Assets {
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(asset)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DownloadSetItem) Decode(encoded string) {
|
||||
rem := encoded
|
||||
i.Kind, rem, _ = strings.Cut(rem, ":")
|
||||
|
||||
switch i.Kind {
|
||||
case "h":
|
||||
i.Kind = "host"
|
||||
case "c":
|
||||
i.Kind = "cluster"
|
||||
}
|
||||
|
||||
i.Name, rem, _ = strings.Cut(rem, ":")
|
||||
|
||||
if rem == "" {
|
||||
i.Assets = nil
|
||||
} else {
|
||||
i.Assets = strings.Split(rem, ":")
|
||||
}
|
||||
}
|
||||
|
||||
type DownloadSetReq struct {
|
||||
Expiry string
|
||||
Items []DownloadSetItem
|
||||
}
|
||||
|
||||
func wsSignDownloadSet(req *restful.Request, resp *restful.Response) {
|
||||
setReq := DownloadSetReq{}
|
||||
if err := req.ReadEntity(&setReq); err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
exp, err := parseCertDuration(setReq.Expiry, time.Now())
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
set := DownloadSet{
|
||||
Expiry: exp,
|
||||
Items: setReq.Items,
|
||||
}
|
||||
|
||||
privKey, _ := dlsSigningKeys()
|
||||
resp.WriteEntity(set.Signed(privKey))
|
||||
}
|
||||
|
||||
func getDlSet(req *restful.Request) (*DownloadSet, *httperr.Error) {
|
||||
setStr := req.QueryParameter("set")
|
||||
|
||||
setBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(setStr)
|
||||
if err != nil {
|
||||
err := httperr.BadRequest("invalid set")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
if len(setBytes) == 0 {
|
||||
err := httperr.BadRequest("invalid set")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
sigLen := int(setBytes[0])
|
||||
setBytes = setBytes[1:]
|
||||
|
||||
if len(setBytes) < sigLen {
|
||||
err := httperr.BadRequest("invalid set")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
sig := setBytes[:sigLen]
|
||||
setBytes = setBytes[sigLen:]
|
||||
|
||||
_, pubkey := dlsSigningKeys()
|
||||
if !ed25519.Verify(pubkey, setBytes, sig) {
|
||||
err := httperr.BadRequest("invalid signature")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
setBytes, err = io.ReadAll(lz4.NewReader(bytes.NewBuffer(setBytes)))
|
||||
if err != nil {
|
||||
err := httperr.BadRequest("invalid data")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
fmt.Println(string(setBytes))
|
||||
|
||||
set := DownloadSet{}
|
||||
if err := set.Decode(string(setBytes)); err != nil {
|
||||
err := httperr.BadRequest("invalid set: " + err.Error())
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
if time.Now().After(set.Expiry) {
|
||||
err := httperr.BadRequest("set expired")
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
return &set, nil
|
||||
}
|
||||
|
||||
func wsDownloadSetAsset(req *restful.Request, resp *restful.Response) {
|
||||
set, err := getDlSet(req)
|
||||
if err != nil {
|
||||
wsError(resp, *err)
|
||||
return
|
||||
}
|
||||
|
||||
kind := req.PathParameter("kind")
|
||||
name := req.PathParameter("name")
|
||||
asset := req.PathParameter("asset")
|
||||
|
||||
if !set.Contains(kind, name, asset) {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
downloadAsset(req, resp, kind, name, asset)
|
||||
}
|
||||
|
||||
func wsDownloadSet(req *restful.Request, resp *restful.Response) {
|
||||
setStr := req.QueryParameter("set")
|
||||
set, err := getDlSet(req)
|
||||
if err != nil {
|
||||
resp.WriteHeader(err.Status)
|
||||
resp.Write([]byte(htmlHeader(err.Error())))
|
||||
resp.Write([]byte(htmlFooter))
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString(htmlHeader("Download set"))
|
||||
|
||||
cfg, err2 := readConfig()
|
||||
if err2 != nil {
|
||||
wsError(resp, err2)
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range set.Items {
|
||||
names := make([]string, 0)
|
||||
switch item.Kind {
|
||||
case "cluster":
|
||||
for _, c := range cfg.Clusters {
|
||||
if globMatch(item.Name, c.Name) {
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
}
|
||||
case "host":
|
||||
for _, h := range cfg.Hosts {
|
||||
if globMatch(item.Name, h.Name) {
|
||||
names = append(names, h.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(buf, "<h2>%s %s</h2>", strings.Title(item.Kind), name)
|
||||
fmt.Fprintf(buf, "<p class=\"download-links\">\n")
|
||||
for _, asset := range item.Assets {
|
||||
fmt.Fprintf(buf, " <a href=\"/public/download-set/%s/%s/%s?set=%s\" download>%s</a>\n", item.Kind, name, asset, setStr, asset)
|
||||
}
|
||||
fmt.Fprintf(buf, `</p>`)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(htmlFooter)
|
||||
buf.WriteTo(resp)
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
@ -53,7 +56,7 @@ func wsAuthorizeDownload(req *restful.Request, resp *restful.Response) {
|
||||
resp.WriteAsJson(token)
|
||||
}
|
||||
|
||||
func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||
func wsDownloadAsset(req *restful.Request, resp *restful.Response) {
|
||||
token := req.PathParameter("token")
|
||||
asset := req.PathParameter("asset")
|
||||
|
||||
@ -102,6 +105,10 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
@ -109,12 +116,12 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||
}
|
||||
|
||||
setHeader := func(ext string) {
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(spec.Kind+"_"+spec.Name+"_"+asset+ext))
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(kind+"_"+name+"_"+asset+ext))
|
||||
}
|
||||
|
||||
switch spec.Kind {
|
||||
switch kind {
|
||||
case "cluster":
|
||||
cluster := cfg.ClusterByName(spec.Name)
|
||||
cluster := cfg.ClusterByName(name)
|
||||
if cluster == nil {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
@ -130,7 +137,7 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||
}
|
||||
|
||||
case "host":
|
||||
host := cfg.Host(spec.Name)
|
||||
host := hostOrTemplate(cfg, name)
|
||||
if host == nil {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
@ -149,3 +156,44 @@ func wsDownload(req *restful.Request, resp *restful.Response) {
|
||||
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)
|
||||
}
|
||||
|
@ -13,21 +13,24 @@ import (
|
||||
"novit.tech/direktil/local-server/pkg/mime"
|
||||
)
|
||||
|
||||
var trustXFF = flag.Bool("trust-xff", true, "Trust the X-Forwarded-For header")
|
||||
var (
|
||||
allowDetectedHost = flag.Bool("allow-detected-host", false, "Allow access to host assets from its IP (insecure but enables unattended netboot)")
|
||||
trustXFF = flag.Bool("trust-xff", false, "Trust the X-Forwarded-For header")
|
||||
)
|
||||
|
||||
type wsHost struct {
|
||||
prefix string
|
||||
hostDoc string
|
||||
getHost func(req *restful.Request) (hostName string, err error)
|
||||
}
|
||||
|
||||
func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
|
||||
func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
|
||||
b := func(what string) *restful.RouteBuilder {
|
||||
return rws.GET(ws.prefix + "/" + what).To(ws.render)
|
||||
return rws.GET("/" + what).To(ws.render)
|
||||
}
|
||||
|
||||
for _, rb := range []*restful.RouteBuilder{
|
||||
rws.GET(ws.prefix).To(ws.get).
|
||||
rws.GET("").To(ws.get).
|
||||
Produces(mime.JSON).
|
||||
Doc("Get the "+ws.hostDoc+"'s details").
|
||||
Returns(200, "OK", localconfig.Host{}),
|
||||
|
||||
@ -44,13 +47,30 @@ func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteB
|
||||
Produces(mime.DISK).
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image"),
|
||||
|
||||
// - raw + compressed
|
||||
b("boot.img.gz").
|
||||
Produces(mime.DISK + "+gzip").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image (gzip compressed)"),
|
||||
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, gzip compressed"),
|
||||
b("boot.img.lz4").
|
||||
Produces(mime.DISK + "+lz4").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image (lz4 compressed)"),
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, lz4 compressed"),
|
||||
|
||||
// - other formats
|
||||
b("boot.qcow2").
|
||||
Produces(mime.DISK + "+qcow2").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, QCOW2 (KVM, Xen)"),
|
||||
b("boot.qed").
|
||||
Produces(mime.DISK + "+qed").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, QED (KVM)"),
|
||||
b("boot.vmdk").
|
||||
Produces(mime.DISK + "+vdi").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, VDI (VirtualBox)"),
|
||||
b("boot.qcow2").
|
||||
Produces(mime.DISK + "+vpc").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, VHD (Hyper-V)"),
|
||||
b("boot.vmdk").
|
||||
Produces(mime.DISK + "+vmdk").
|
||||
Doc("Get the " + ws.hostDoc + "'s boot disk image, VMDK (VMware)"),
|
||||
|
||||
// metal/local HDD upgrades
|
||||
b("boot.tar").
|
||||
@ -71,40 +91,31 @@ func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteB
|
||||
Produces(mime.IPXE).
|
||||
Doc("Get the " + ws.hostDoc + "'s IPXE code (for netboot)"),
|
||||
|
||||
// boot support
|
||||
b("kernel").
|
||||
Produces(mime.OCTET).
|
||||
Doc("Get the " + ws.hostDoc + "'s kernel (ie: for netboot)"),
|
||||
|
||||
b("initrd").
|
||||
Produces(mime.OCTET).
|
||||
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"),
|
||||
|
||||
// boot v2
|
||||
// - bootstrap config
|
||||
b("bootstrap-config").
|
||||
Produces(mime.YAML).
|
||||
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration"),
|
||||
b("bootstrap-config.json").
|
||||
Doc("Get the " + ws.hostDoc + "'s bootstrap configuration (as JSON)"),
|
||||
// - initrd
|
||||
b("initrd-v2").
|
||||
Produces(mime.OCTET).
|
||||
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (v2)"),
|
||||
// - bootstrap
|
||||
b("bootstrap.tar").
|
||||
Produces(mime.TAR).
|
||||
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
||||
b("boot-v2.iso").
|
||||
Produces(mime.ISO).
|
||||
Param(cmdlineParam).
|
||||
Doc("Get the " + ws.hostDoc + "'s boot CD-ROM image (v2)"),
|
||||
} {
|
||||
alterRB(rb)
|
||||
rws.Route(rb)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
|
||||
func (ws wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
|
||||
hostname, err := ws.getHost(req)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
@ -121,16 +132,16 @@ func (ws *wsHost) host(req *restful.Request, resp *restful.Response) (host *loca
|
||||
return
|
||||
}
|
||||
|
||||
host = cfg.Host(hostname)
|
||||
host = hostOrTemplate(cfg, hostname)
|
||||
if host == nil {
|
||||
log.Print("no host named ", hostname)
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ws *wsHost) get(req *restful.Request, resp *restful.Response) {
|
||||
func (ws wsHost) get(req *restful.Request, resp *restful.Response) {
|
||||
host, _ := ws.host(req, resp)
|
||||
if host == nil {
|
||||
return
|
||||
@ -139,7 +150,7 @@ func (ws *wsHost) get(req *restful.Request, resp *restful.Response) {
|
||||
resp.WriteEntity(host)
|
||||
}
|
||||
|
||||
func (ws *wsHost) render(req *restful.Request, resp *restful.Response) {
|
||||
func (ws wsHost) render(req *restful.Request, resp *restful.Response) {
|
||||
host, cfg := ws.host(req, resp)
|
||||
if host == nil {
|
||||
return
|
||||
@ -161,7 +172,6 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
||||
switch what {
|
||||
case "config":
|
||||
err = renderConfig(w, r, ctx, false)
|
||||
|
||||
case "config.json":
|
||||
err = renderConfig(w, r, ctx, true)
|
||||
|
||||
@ -170,10 +180,27 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
||||
|
||||
case "kernel":
|
||||
err = renderKernel(w, r, ctx)
|
||||
|
||||
case "initrd":
|
||||
err = renderCtx(w, r, ctx, what, buildInitrd)
|
||||
case "bootstrap.tar":
|
||||
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
||||
|
||||
case "boot.img":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImg)
|
||||
case "boot.img.gz":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImgGZ)
|
||||
case "boot.img.lz4":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
|
||||
case "boot.qcow2":
|
||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2"))
|
||||
case "boot.qed":
|
||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed"))
|
||||
case "boot.vdi":
|
||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi"))
|
||||
case "boot.vmdk":
|
||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk"))
|
||||
case "boot.vpc":
|
||||
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc"))
|
||||
case "boot.iso":
|
||||
err = renderCtx(w, r, ctx, what, buildBootISO)
|
||||
|
||||
@ -182,26 +209,11 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
||||
case "boot-efi.tar":
|
||||
err = renderCtx(w, r, ctx, what, buildBootEFITar)
|
||||
|
||||
case "boot.img":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImg)
|
||||
|
||||
case "boot.img.gz":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImgGZ)
|
||||
|
||||
case "boot.img.lz4":
|
||||
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
|
||||
|
||||
// boot v2
|
||||
case "bootstrap-config":
|
||||
err = renderBootstrapConfig(w, r, ctx, false)
|
||||
case "bootstrap-config.json":
|
||||
err = renderBootstrapConfig(w, r, ctx, true)
|
||||
case "initrd-v2":
|
||||
err = renderCtx(w, r, ctx, what, buildInitrdV2)
|
||||
case "bootstrap.tar":
|
||||
err = renderCtx(w, r, ctx, what, buildBootstrap)
|
||||
case "boot-v2.iso":
|
||||
err = renderCtx(w, r, ctx, what, buildBootISOv2)
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
116
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
116
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/netip"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
var hostsFromTemplate = KVSecrets[HostFromTemplate]{"hosts-from-template"}
|
||||
|
||||
type HostFromTemplate struct {
|
||||
Template string
|
||||
IP string
|
||||
}
|
||||
|
||||
func (hft HostFromTemplate) ClusterName(cfg *localconfig.Config) string {
|
||||
for _, ht := range cfg.HostTemplates {
|
||||
if ht.Name == hft.Template {
|
||||
return ht.ClusterName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hostOrTemplate(cfg *localconfig.Config, name string) (host *localconfig.Host) {
|
||||
host = cfg.Host(name)
|
||||
if host != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hft, found, err := hostsFromTemplate.Get(name)
|
||||
if err != nil {
|
||||
log.Print("failed to read store: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Print("no host named ", name)
|
||||
return
|
||||
}
|
||||
|
||||
ht := cfg.HostTemplate(hft.Template)
|
||||
if ht == nil {
|
||||
log.Print("host ", name, " found but no template named ", hft.Template)
|
||||
return
|
||||
}
|
||||
|
||||
host = &localconfig.Host{}
|
||||
*host = *ht
|
||||
host.Name = name
|
||||
host.IPs = []string{hft.IP}
|
||||
return
|
||||
}
|
||||
|
||||
func wsHostsFromTemplateList(req *restful.Request, resp *restful.Response) {
|
||||
hostsFromTemplate.WsList(resp, "")
|
||||
}
|
||||
|
||||
func wsHostsFromTemplateSet(req *restful.Request, resp *restful.Response) {
|
||||
name := req.PathParameter("name")
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
v := HostFromTemplate{}
|
||||
if err := req.ReadEntity(&v); err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if v.Template == "" {
|
||||
wsBadRequest(resp, "template is required")
|
||||
return
|
||||
}
|
||||
if v.IP == "" {
|
||||
wsBadRequest(resp, "ip is required")
|
||||
return
|
||||
}
|
||||
if _, err := netip.ParseAddr(v.IP); err != nil {
|
||||
wsBadRequest(resp, "bad IP: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, ht := range cfg.HostTemplates {
|
||||
if ht.Name != v.Template {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
if !found {
|
||||
wsBadRequest(resp, "no host template with this name")
|
||||
return
|
||||
}
|
||||
|
||||
if err := hostsFromTemplate.Put(name, v); err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
updateState()
|
||||
}
|
||||
|
||||
func wsHostsFromTemplateDelete(req *restful.Request, resp *restful.Response) {
|
||||
name := req.PathParameter("name")
|
||||
hostsFromTemplate.WsDel(req, resp, name)
|
||||
updateState()
|
||||
}
|
@ -5,26 +5,58 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
"novit.tech/direktil/local-server/secretstore"
|
||||
)
|
||||
|
||||
type NamedPassphrase struct {
|
||||
Name string
|
||||
Passphrase []byte
|
||||
}
|
||||
|
||||
func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
||||
var passphrase string
|
||||
err := req.ReadEntity(&passphrase)
|
||||
np := NamedPassphrase{}
|
||||
err := req.ReadEntity(&np)
|
||||
if err != nil {
|
||||
resp.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := unlockSecretStore([]byte(passphrase)); err.Any() {
|
||||
defer secretstore.Memzero(np.Passphrase)
|
||||
|
||||
if secStore.IsNew() {
|
||||
if len(np.Name) == 0 {
|
||||
wsBadRequest(resp, "no name given")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(np.Passphrase) == 0 {
|
||||
wsBadRequest(resp, "no passphrase given")
|
||||
return
|
||||
}
|
||||
|
||||
if secStore.Unlocked() {
|
||||
if secStore.HasKey(np.Passphrase) {
|
||||
resp.WriteEntity(adminToken)
|
||||
} else {
|
||||
wsError(resp, ErrUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := unlockSecretStore(np.Name, np.Passphrase); err.Any() {
|
||||
err.WriteJSON(resp.ResponseWriter)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(*adminToken)
|
||||
resp.WriteEntity(adminToken)
|
||||
}
|
||||
|
||||
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
||||
@ -96,3 +128,68 @@ func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
||||
|
||||
buf.WriteTo(resp)
|
||||
}
|
||||
|
||||
func wsStoreUpload(req *restful.Request, resp *restful.Response) {
|
||||
if !secStore.IsNew() {
|
||||
wsError(resp, httperr.BadRequest("store is not new"))
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
_, err := io.Copy(buf, req.Request.Body)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
arch := tar.NewReader(buf)
|
||||
|
||||
root := secStoreRoot()
|
||||
|
||||
for {
|
||||
hdr, err := arch.Next()
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
break
|
||||
} else if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Print(hdr.Name)
|
||||
|
||||
fullPath := filepath.Join(root, hdr.Name)
|
||||
|
||||
switch {
|
||||
case hdr.FileInfo().IsDir():
|
||||
err = os.MkdirAll(fullPath, 0700)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
content, err := io.ReadAll(io.LimitReader(arch, hdr.Size))
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(fullPath, content, 0600)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
openSecretStore()
|
||||
|
||||
resp.WriteEntity(map[string]any{"ok": true})
|
||||
}
|
||||
|
@ -1,24 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"novit.tech/direktil/local-server/secretstore"
|
||||
)
|
||||
|
||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
||||
var passphrase string
|
||||
np := NamedPassphrase{}
|
||||
|
||||
err := req.ReadEntity(&passphrase)
|
||||
err := req.ReadEntity(&np)
|
||||
if err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
np.Name = strings.TrimSpace(np.Name)
|
||||
|
||||
if len(np.Name) == 0 {
|
||||
wsBadRequest(resp, "no name given")
|
||||
return
|
||||
}
|
||||
|
||||
if len(np.Passphrase) == 0 {
|
||||
wsBadRequest(resp, "no passphrase given")
|
||||
return
|
||||
}
|
||||
|
||||
secStore.AddKey([]byte(passphrase))
|
||||
for _, k := range secStore.Keys {
|
||||
if k.Name == np.Name {
|
||||
wsBadRequest(resp, "there's already a passphrase named "+strconv.Quote(np.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
secStore.AddKey(np.Name, np.Passphrase)
|
||||
defer updateState()
|
||||
|
||||
err = secStore.SaveTo(secKeysStorePath())
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func wsStoreDelKey(req *restful.Request, resp *restful.Response) {
|
||||
name := ""
|
||||
|
||||
err := req.ReadEntity(&name)
|
||||
if err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
newKeys := make([]secretstore.KeyEntry, 0, len(secStore.Keys))
|
||||
for _, k := range secStore.Keys {
|
||||
if k.Name == name {
|
||||
continue
|
||||
}
|
||||
|
||||
newKeys = append(newKeys, k)
|
||||
}
|
||||
|
||||
if len(newKeys) == 0 {
|
||||
wsBadRequest(resp, "can't remove the last key from the store")
|
||||
return
|
||||
}
|
||||
|
||||
secStore.Keys = newKeys
|
||||
defer updateState()
|
||||
|
||||
err = secStore.SaveTo(secKeysStorePath())
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
|
@ -27,38 +27,51 @@ func registerWS(rest *restful.Container) {
|
||||
Produces(mime.JSON).
|
||||
Consumes(mime.JSON).
|
||||
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
||||
Reads("").
|
||||
Reads(NamedPassphrase{}).
|
||||
Writes("").
|
||||
Doc("Try to unlock the store")).
|
||||
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
||||
Produces(mime.TAR).
|
||||
Param(ws.QueryParameter("token", "the download token")).
|
||||
Doc("Fetch the encrypted store")).
|
||||
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
|
||||
Route(ws.POST("/store.tar").To(wsStoreUpload).
|
||||
Consumes(mime.TAR).
|
||||
Doc("Upload an existing store")).
|
||||
Route(ws.GET("/downloads/{token}").To(wsDownload)).
|
||||
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownloadAsset).
|
||||
Param(ws.PathParameter("token", "the download token")).
|
||||
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)
|
||||
}
|
||||
|
||||
// Admin-level APIs
|
||||
ws := &restful.WebService{}
|
||||
ws.
|
||||
ws := (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Filter(adminAuth).
|
||||
Param(ws.HeaderParameter("Authorization", "Admin bearer token").Required(true)).
|
||||
Param(restful.HeaderParameter("Authorization", "Admin bearer token").Required(true)).
|
||||
Produces(mime.JSON)
|
||||
|
||||
// - store management
|
||||
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
||||
Consumes(mime.JSON).Reads("").
|
||||
Consumes(mime.JSON).Reads(NamedPassphrase{}).
|
||||
Doc("Add an unlock key to the store"))
|
||||
ws.Route(ws.POST("/store/delete-key").To(wsStoreDelKey).
|
||||
Consumes(mime.JSON).Reads("").
|
||||
Doc("Remove an unlock key to the store (by its name)"))
|
||||
|
||||
// - downloads
|
||||
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
||||
Consumes(mime.JSON).Reads(DownloadSpec{}).
|
||||
Produces(mime.JSON).
|
||||
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
|
||||
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
||||
@ -70,9 +83,19 @@ func registerWS(rest *restful.Container) {
|
||||
ws.Route(ws.GET("/clusters").To(wsListClusters).
|
||||
Doc("List clusters"))
|
||||
|
||||
ws.Route(ws.GET("/hosts-from-template").To(wsHostsFromTemplateList).
|
||||
Doc("List host template instances"))
|
||||
ws.Route(ws.POST("/hosts-from-template/{name}").To(wsHostsFromTemplateSet).
|
||||
Reads(HostFromTemplate{}).
|
||||
Doc("Create or update a host template instance"))
|
||||
ws.Route(ws.DELETE("/hosts-from-template/{name}").To(wsHostsFromTemplateDelete).
|
||||
Reads(HostFromTemplate{}).
|
||||
Doc("Delete a host template instance"))
|
||||
|
||||
const (
|
||||
GET = http.MethodGet
|
||||
PUT = http.MethodPut
|
||||
GET = http.MethodGet
|
||||
PUT = http.MethodPut
|
||||
POST = http.MethodPost
|
||||
)
|
||||
|
||||
cluster := func(method, subPath string) *restful.RouteBuilder {
|
||||
@ -111,6 +134,16 @@ func registerWS(rest *restful.Container) {
|
||||
Produces(mime.CERT).
|
||||
Param(ws.QueryParameter("name", "signed reference name").Required(true)).
|
||||
Doc("Get cluster's certificate signed by the CA"),
|
||||
|
||||
cluster(GET, "/ssh/user-ca").To(wsClusterSSHUserCAPubKey).
|
||||
Produces(mime.OCTET).
|
||||
Doc("User CA public key for this cluster"),
|
||||
cluster(POST, "/ssh/user-ca/sign").To(wsClusterSSHUserCASign).
|
||||
Produces(mime.OCTET).
|
||||
Doc("Sign a user's SSH public key for this cluster"),
|
||||
cluster(POST, "/kube/sign").To(wsClusterKubeCASign).
|
||||
Produces(mime.OCTET).
|
||||
Doc("Sign a user's public key for this cluster's Kubernetes API server"),
|
||||
} {
|
||||
ws.Route(builder)
|
||||
}
|
||||
@ -118,16 +151,6 @@ func registerWS(rest *restful.Container) {
|
||||
ws.Route(ws.GET("/hosts").To(wsListHosts).
|
||||
Doc("List hosts"))
|
||||
|
||||
(&wsHost{
|
||||
prefix: "/hosts/{host-name}",
|
||||
hostDoc: "given host",
|
||||
getHost: func(req *restful.Request) (string, error) {
|
||||
return req.PathParameter("host-name"), nil
|
||||
},
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Param(ws.PathParameter("host-name", "host's name"))
|
||||
})
|
||||
|
||||
ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List))
|
||||
ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get))
|
||||
ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set))
|
||||
@ -135,10 +158,27 @@ func registerWS(rest *restful.Container) {
|
||||
rest.Add(ws)
|
||||
|
||||
// Hosts API
|
||||
ws = &restful.WebService{}
|
||||
ws.Produces(mime.JSON).
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Filter(adminAuth).
|
||||
Path("/hosts/{host-name}").
|
||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||
|
||||
(&wsHost{
|
||||
hostDoc: "given host",
|
||||
getHost: func(req *restful.Request) (string, error) {
|
||||
return req.PathParameter("host-name"), nil
|
||||
},
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Param(ws.PathParameter("host-name", "host's name"))
|
||||
})
|
||||
|
||||
rest.Add(ws)
|
||||
|
||||
// Detected host API
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Path("/me").
|
||||
Filter(hostsAuth).
|
||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||
|
||||
(&wsHost{
|
||||
@ -149,8 +189,10 @@ func registerWS(rest *restful.Container) {
|
||||
})
|
||||
|
||||
// Hosts by token API
|
||||
ws = &restful.WebService{}
|
||||
ws.Path("/hosts-by-token/{host-token}").Param(ws.PathParameter("host-token", "host's download token"))
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Path("/hosts-by-token/{host-token}").
|
||||
Param(ws.PathParameter("host-token", "host's download token"))
|
||||
|
||||
(&wsHost{
|
||||
hostDoc: "token's host",
|
||||
@ -178,7 +220,19 @@ func registerWS(rest *restful.Container) {
|
||||
rest.Add(ws)
|
||||
}
|
||||
|
||||
func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
if !secStore.Unlocked() {
|
||||
wsError(resp, ErrStoreLocked)
|
||||
return
|
||||
}
|
||||
chain.ProcessFilter(req, resp)
|
||||
}
|
||||
|
||||
func detectHost(req *restful.Request) (hostName string, err error) {
|
||||
if !*allowDetectedHost {
|
||||
return
|
||||
}
|
||||
|
||||
r := req.Request
|
||||
remoteAddr := r.RemoteAddr
|
||||
|
||||
|
82
go.mod
82
go.mod
@ -1,78 +1,82 @@
|
||||
module novit.tech/direktil/local-server
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/cavaliergopher/cpio v1.0.1
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/cloudflare/cfssl v1.6.4
|
||||
github.com/cloudflare/cfssl v1.6.5
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/emicklei/go-restful v2.16.0+incompatible
|
||||
github.com/emicklei/go-restful-openapi v1.4.1
|
||||
github.com/go-git/go-git/v5 v5.6.1
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34
|
||||
github.com/miolini/datacounter v1.0.3
|
||||
github.com/oklog/ulid v1.3.1
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible
|
||||
golang.org/x/crypto v0.8.0
|
||||
github.com/sergeymakinen/go-crypt v1.0.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/apimachinery v0.27.1
|
||||
m.cluseau.fr/go v0.0.0-20230406121934-7940120eb250
|
||||
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e
|
||||
k8s.io/apimachinery v0.33.2
|
||||
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
|
||||
novit.tech/direktil/pkg v0.0.0-20250706092353-d857af8032a1
|
||||
)
|
||||
|
||||
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||
github.com/cloudflare/circl v1.3.2 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/frankban/quicktest v1.5.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gobuffalo/envy v1.10.2 // indirect
|
||||
github.com/gobuffalo/packd v1.0.2 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.5 // indirect
|
||||
github.com/imdario/mergo v0.3.15 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kisielk/sqlstruct v0.0.0-20210630145711-dae28ed37023 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/skeema/knownhosts v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/src-d/gcfg v1.4.0 // indirect
|
||||
github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db // indirect
|
||||
github.com/weppos/publicsuffix-go v0.40.3-0.20250617082559-9b2e24a9e482 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect
|
||||
github.com/zmap/zlint/v3 v3.1.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20250627161936-38850a079d72 // indirect
|
||||
github.com/zmap/zlint/v3 v3.5.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
)
|
||||
|
323
go.sum
323
go.sum
@ -1,17 +1,19 @@
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
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 v0.0.0-20230426101702-58e86b294756 h1:L6S7kR7SlhQKplIBpkra3s6yhcZV51lhRnXmYc4HohI=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
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/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
||||
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
@ -20,27 +22,30 @@ 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/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/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
|
||||
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/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/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
|
||||
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||
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/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
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/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/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.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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
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/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
@ -49,46 +54,40 @@ 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.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
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 v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
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/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
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.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
|
||||
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
|
||||
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
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/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-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/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
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-openapi/jsonpointer v0.0.0-20180322222829-3a0015ad55fa/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
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/jsonreference v0.0.0-20180322222742-3fb327e6747d/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
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/spec v0.0.0-20180415031709-bcff419492ee/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
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/swag v0.0.0-20180405201759-811b1089cde9/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
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-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/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
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.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
|
||||
@ -100,33 +99,23 @@ github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/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.5 h1:EVfYyOiMSdwwXd6FJxnh0jYgYj/Dh5n9sXtgIr5+Vj0=
|
||||
github.com/google/certificate-transparency-go v1.1.5/go.mod h1:CnNCSPt9ptZQ8jDSrqyTmh2dT2MQLKymfGYwXqjQ7YY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/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/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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v50 v50.1.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
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/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
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/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
@ -141,29 +130,31 @@ 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/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/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.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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
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/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
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/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.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-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/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/go.mod h1:lcyE8C83VRamH/oTpikU4+yVCCxLthWgDOqjHSsu+ZY=
|
||||
github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA=
|
||||
@ -171,49 +162,47 @@ github.com/miolini/datacounter v1.0.3/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzO
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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/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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
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/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/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/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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.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.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.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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
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/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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
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/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
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/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.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
|
||||
github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
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/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
@ -228,91 +217,55 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
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.1-0.20230422193905-8fecedd899db h1:/WcxBne+5CbtbgWd/sV2wbravmr4sT7y52ifQaCgoLs=
|
||||
github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db/go.mod h1:aiQaH1XpzIfgrJq3S1iw7w+3EDbRP7mF5fmwUhWyRUs=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
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/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
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/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/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-20230422215203-9a665e1e9968 h1:YOQ1vXEwE4Rnj+uQ/3oCuJk5wgVsvUyW+glsndwYuyA=
|
||||
github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968/go.mod h1:xIuOvYCZX21S5Z9bK1BMrertTGX/F8hgAPw7ERJRNS0=
|
||||
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/zlint/v3 v3.3.1 h1:IrIY2Qd2Wr9ZHhdQ3mszehSydz+x6OROClztMEK+2bU=
|
||||
github.com/zmap/zlint/v3 v3.3.1/go.mod h1:fPCW5acxhqw4HU1Vm0t9oFEPo1/uH9hI0sci/Z++hEI=
|
||||
golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
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-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-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-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-20210817164053-32db794688a5/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-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/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-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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/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-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-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-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-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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/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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -321,68 +274,52 @@ 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-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-20200302150141-5c8b2ff67527/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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/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-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-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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
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.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
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.3/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.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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
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.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
|
||||
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@ -396,24 +333,18 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/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/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-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc=
|
||||
k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
|
||||
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
m.cluseau.fr/go v0.0.0-20230406121934-7940120eb250 h1:vSITZdoUVqlJBAOzCuC/Pz2GNM6Mgi/YOJ/ALmlPFd0=
|
||||
m.cluseau.fr/go v0.0.0-20230406121934-7940120eb250/go.mod h1:dOJ2bB14TFegivjXAccmGm+S9ZlNBRtuQq8tEqPPvJM=
|
||||
novit.nc/direktil/pkg v0.0.0-20220221171542-fd3ce3a1491b/go.mod h1:zwTVO6U0tXFEaga73megQIBK7yVIKZJVePaIh/UtdfU=
|
||||
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e h1:eQFbzcuB4wOSrnOhkcN30hFDCIack40VkIoqVRbWnWc=
|
||||
novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e/go.mod h1:2Mir5x1eT/e295WeFGzzXa4siunKX4z+rmNPfVsXS0k=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
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/go.mod h1:BMv3aOSYpupuiiG3Ch3ND88aB5CfAks3YZuRLE8j1ls=
|
||||
novit.tech/direktil/pkg v0.0.0-20250706092353-d857af8032a1 h1:hKj9qhbTAoTxYIj6KaMLJp9I+bvZfkSM/QwK8Bd496o=
|
||||
novit.tech/direktil/pkg v0.0.0-20250706092353-d857af8032a1/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
||||
|
8
govc.env
8
govc.env
@ -1,8 +0,0 @@
|
||||
export GOVC_DATACENTER=
|
||||
export GOVC_PASSWORD=
|
||||
export GOVC_URL=
|
||||
export GOVC_USERNAME=
|
||||
export GOVC_INSECURE=1
|
||||
export GOVC_DATASTORE=
|
||||
export NOVIT_VM_FOLDER=
|
||||
export NOVIT_ISO_FOLDER=
|
3
hack/build
Executable file
3
hack/build
Executable file
@ -0,0 +1,3 @@
|
||||
#! /bin/sh
|
||||
set -ex
|
||||
go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)}" $*
|
11
hack/docker-build
Executable file
11
hack/docker-build
Executable file
@ -0,0 +1,11 @@
|
||||
#! /bin/bash
|
||||
set -ex
|
||||
|
||||
GIT_TAG=$(git describe --always --dirty)
|
||||
|
||||
case "$1" in
|
||||
commit) tag=$GIT_TAG ;;
|
||||
"") tag=latest ;;
|
||||
*) tag=$1 ;;
|
||||
esac
|
||||
docker build -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG
|
4
hack/install
Executable file
4
hack/install
Executable file
@ -0,0 +1,4 @@
|
||||
#! /bin/sh
|
||||
set -ex
|
||||
go install -trimpath -ldflags "-X main.Version=$(git describe --always --dirty)" \
|
||||
./cmd/dkl-dir2config
|
@ -1,41 +1,29 @@
|
||||
|
||||
.downloads {
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
}
|
||||
|
||||
.downloads > * {
|
||||
margin-left: 6pt;
|
||||
}
|
||||
.downloads > *:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.downloads > div {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-height: 100pt;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cluster {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
#store-infos {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-content: center;
|
||||
justify-content: flex-start;
|
||||
border-bottom: dashed 1pt;
|
||||
.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;
|
||||
}
|
||||
#store-infos > * {
|
||||
display: block;
|
||||
font-size: medium;
|
||||
padding: 2pt 1ex;
|
||||
margin: 0 0 0 1ex;
|
||||
|
||||
.downloads, .download-links {
|
||||
& > * {
|
||||
display: inline-block;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1px solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
#store-infos > *:first-child {
|
||||
margin-left: 0;
|
||||
|
||||
.downloads, .view-links {
|
||||
& > .selected {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,8 @@
|
||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||
</span>
|
||||
|
||||
<span id="uiHash">ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
|
||||
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||
</div>
|
||||
@ -37,59 +38,100 @@
|
||||
<template v-if="!publicState">
|
||||
<p>Not connected.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="publicState.Store.New">
|
||||
<p>Store is new.</p>
|
||||
<form @submit="unlockStore" action="/public/unlock-store">
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" required />
|
||||
<input type="password" v-model="forms.store.pass2" required />
|
||||
<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" action="/public/unlock-store">
|
||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required />
|
||||
<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="setToken">
|
||||
<input type="password" v-model="forms.setToken" required />
|
||||
<input type="submit" value="set token"/>
|
||||
<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 id="store-infos">
|
||||
<h2>Store</h2>
|
||||
<a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">download</a>
|
||||
<form @submit="storeAddKey" action="/store/add-key">
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required />
|
||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required />
|
||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||
</form>
|
||||
<div 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="state.Clusters" id="clusters">
|
||||
|
||||
<h2>Clusters</h2>
|
||||
|
||||
<div class="sheets">
|
||||
<Cluster v-for="c in state.Clusters" :cluster="c" :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="state.Hosts" id="hosts">
|
||||
<h2>Hosts</h2>
|
||||
<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>
|
||||
|
||||
<div class="sheets">
|
||||
<Host v-for="h in state.Hosts" :host="h" :token="session.token" :state="state" />
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<pre v-if="false">{{ state }}</pre>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
@ -5,22 +5,59 @@ import GetCopy from './GetCopy.js';
|
||||
export default {
|
||||
components: { Downloads, GetCopy },
|
||||
props: [ 'cluster', 'token', 'state' ],
|
||||
data() {
|
||||
return {
|
||||
signReqValidity: "1d",
|
||||
sshSignReq: {
|
||||
PubKey: "",
|
||||
Principal: "root",
|
||||
},
|
||||
sshUserCert: null,
|
||||
kubeSignReq: {
|
||||
CSR: "",
|
||||
User: "anonymous",
|
||||
Group: "",
|
||||
},
|
||||
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) => resp.blob())
|
||||
.then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
||||
.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) => resp.blob())
|
||||
.then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="cluster">
|
||||
<div class="title">Cluster {{ cluster.Name }}</div>
|
||||
<div class="section">Tokens</div>
|
||||
<h3>Tokens</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
|
||||
</section>
|
||||
<div class="section">Passwords</div>
|
||||
|
||||
<h3>Passwords</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Passwords" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/passwords/'+n" />
|
||||
</section>
|
||||
<div class="section">Downloads</div>
|
||||
<section class="downloads">
|
||||
<Downloads :token="token" :state="state" kind="cluster" :name="cluster.Name" />
|
||||
</section>
|
||||
<div class="section">CAs</div>
|
||||
|
||||
<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>
|
||||
@ -30,6 +67,35 @@ export default {
|
||||
<GetCopy :token="token" :name="signed" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/signed?name='+signed" />
|
||||
</template></td>
|
||||
</tr></table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
`
|
||||
}
|
||||
|
@ -10,33 +10,32 @@ export default {
|
||||
cluster: ['addons'],
|
||||
host: [
|
||||
"kernel",
|
||||
"initrd-v2",
|
||||
"initrd",
|
||||
"bootstrap.tar",
|
||||
"boot-v2.iso",
|
||||
"config",
|
||||
"bootstrap-config",
|
||||
"boot.img.lz4",
|
||||
"boot.img.gz",
|
||||
"boot.qcow2",
|
||||
"boot.vmdk",
|
||||
"boot.img",
|
||||
"boot.iso",
|
||||
"boot.tar",
|
||||
"boot-efi.tar",
|
||||
"boot.img",
|
||||
"boot.img.gz",
|
||||
"boot.img.lz4",
|
||||
"initrd",
|
||||
"config",
|
||||
"bootstrap-config",
|
||||
"ipxe",
|
||||
],
|
||||
}[this.kind]
|
||||
},
|
||||
downloads() {
|
||||
let ret = []
|
||||
Object.entries(this.state.Downloads)
|
||||
return Object.entries(this.state.Downloads)
|
||||
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
||||
.forEach(e => {
|
||||
let token= e[0], d = e[1]
|
||||
d.Assets.forEach(asset => {
|
||||
ret.push({name: asset, url: '/public/downloads/'+token+'/'+asset})
|
||||
})
|
||||
.map(e => {
|
||||
const token= e[0];
|
||||
return {
|
||||
text: token.substring(0, 5) + '...',
|
||||
url: '/public/downloads/'+token+"/",
|
||||
}
|
||||
})
|
||||
return ret
|
||||
},
|
||||
assets() {
|
||||
return this.availableAssets.filter(a => this.selectedAssets[a])
|
||||
@ -56,11 +55,17 @@ export default {
|
||||
.catch((e) => { alert('failed to create link'); this.createDisabled = false })
|
||||
},
|
||||
},
|
||||
template: `<div class="downloads">
|
||||
<div class="options">
|
||||
<span v-for="asset in availableAssets"><label><input type="checkbox" v-model="selectedAssets[asset]" /> {{ asset }}</label></span>
|
||||
</div>
|
||||
<button :disabled="createDisabled || assets.length==0" @click="createToken">+</button>
|
||||
<div><a v-for="d in downloads" target="_blank" :href="d.url">{{ d.name }}</a></div>
|
||||
</div>`
|
||||
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]" /> {{ 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>`
|
||||
}
|
||||
|
@ -5,17 +5,13 @@ export default {
|
||||
components: { Downloads },
|
||||
props: [ 'host', 'token', 'state' ],
|
||||
template: `
|
||||
<div class="host">
|
||||
<div class="title">Host {{ host.Name }}</div>
|
||||
<section>
|
||||
<template v-for="ip in host.IPs">
|
||||
{{ ip }}
|
||||
</template>
|
||||
</section>
|
||||
<div class="section">Downloads</div>
|
||||
<section>
|
||||
<Downloads :token="token" :state="state" kind="host" :name="host.Name" />
|
||||
</section>
|
||||
</div>
|
||||
<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" />
|
||||
`
|
||||
}
|
||||
|
@ -9,11 +9,18 @@ createApp({
|
||||
data() {
|
||||
return {
|
||||
forms: {
|
||||
store: { },
|
||||
store: {},
|
||||
storeUpload: {},
|
||||
delKey: {},
|
||||
hostFromTemplate: {},
|
||||
hostFromTemplateDel: "",
|
||||
},
|
||||
view: "",
|
||||
viewFilter: "",
|
||||
session: {},
|
||||
error: null,
|
||||
publicState: null,
|
||||
serverVersion: null,
|
||||
uiHash: null,
|
||||
watchingState: false,
|
||||
state: null,
|
||||
@ -39,6 +46,7 @@ createApp({
|
||||
deep: true,
|
||||
handler(v) {
|
||||
if (v) {
|
||||
this.serverVersion = v.ServerVersion
|
||||
if (this.uiHash && v.UIHash != this.uiHash) {
|
||||
console.log("reloading")
|
||||
location.reload()
|
||||
@ -50,7 +58,35 @@ createApp({
|
||||
}
|
||||
},
|
||||
|
||||
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.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)
|
||||
@ -60,13 +96,34 @@ createApp({
|
||||
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.forms.store.pass1, (v) => {
|
||||
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.forms.store.pass1, (v) => {
|
||||
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
|
||||
this.forms.store = {}
|
||||
|
||||
if (v) {
|
||||
@ -78,7 +135,23 @@ createApp({
|
||||
}
|
||||
})
|
||||
},
|
||||
apiPost(action, data, onload) {
|
||||
uploadConfig() {
|
||||
this.apiPost('/configs', this.$refs.configUpload.files[0], (v) => {}, "text/vnd.yaml")
|
||||
},
|
||||
hostFromTemplateAdd() {
|
||||
let v = this.forms.hostFromTemplate;
|
||||
this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} });
|
||||
},
|
||||
hostFromTemplateDel() {
|
||||
event.preventDefault()
|
||||
|
||||
let v = this.forms.hostFromTemplateDel;
|
||||
if (!confirm("delete host template instance "+v+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
|
||||
},
|
||||
apiPost(action, data, onload, contentType = 'application/json') {
|
||||
event.preventDefault()
|
||||
|
||||
if (data === undefined) {
|
||||
@ -97,7 +170,7 @@ createApp({
|
||||
var xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.responseType = 'json'
|
||||
// TODO spinner, pending aciton notification, or something
|
||||
// TODO spinner, pending action notification, or something
|
||||
xhr.onerror = () => {
|
||||
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
|
||||
}
|
||||
@ -115,11 +188,37 @@ createApp({
|
||||
|
||||
xhr.open("POST", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.setRequestHeader('Content-Type', contentType)
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
xhr.send(JSON.stringify(data))
|
||||
|
||||
if (contentType == "application/json") {
|
||||
xhr.send(JSON.stringify(data))
|
||||
} else {
|
||||
xhr.send(data)
|
||||
}
|
||||
},
|
||||
apiDelete(action, data, onload) {
|
||||
event.preventDefault()
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onload = (r) => {
|
||||
if (xhr.status != 200) {
|
||||
this.error = xhr.response
|
||||
return
|
||||
}
|
||||
this.error = null
|
||||
if (onload) {
|
||||
onload(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.open("DELETE", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
xhr.send()
|
||||
},
|
||||
download(url) {
|
||||
event.target.target = '_blank'
|
||||
|
@ -1,5 +1,30 @@
|
||||
:root {
|
||||
--bg: #eee;
|
||||
--color: black;
|
||||
--bevel-dark: darkgray;
|
||||
--bevel-light: lightgray;
|
||||
--link: blue;
|
||||
--input-bg: #ddd;
|
||||
--input-text: white;
|
||||
--btn-bg: #eee;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: black;
|
||||
--color: orange;
|
||||
--bevel-dark: #402900;
|
||||
--bevel-light: #805300;
|
||||
--link: #31b0fa;
|
||||
--input-bg: #111;
|
||||
--input-text: #ddd;
|
||||
--btn-bg: #222;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
@ -8,7 +33,7 @@ button[disabled] {
|
||||
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: blue;
|
||||
color: var(--link);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
@ -37,20 +62,38 @@ th, tr:last-child > td {
|
||||
.red { color: red; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: black;
|
||||
color: orange;
|
||||
}
|
||||
button, input[type=submit] {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: #31b0fa;
|
||||
.red { color: #c00; }
|
||||
}
|
||||
|
||||
textarea, 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);
|
||||
}
|
||||
}
|
||||
|
||||
.red { color: #c00; }
|
||||
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 {
|
||||
|
20
modd.conf
20
modd.conf
@ -3,24 +3,10 @@ modd.conf {}
|
||||
**/*.go go.mod go.sum {
|
||||
prep: go test ./...
|
||||
prep: mkdir -p dist
|
||||
prep: go build -o dist/ -trimpath ./...
|
||||
#prep: docker build --build-arg GOPROXY=$GOPROXY -t dls .
|
||||
#daemon +sigterm: /var/lib/direktil/test-run
|
||||
prep: hack/build ./...
|
||||
#daemon +sigterm: bash test-run
|
||||
}
|
||||
|
||||
html/**/* {
|
||||
prep: go build -o dist/ -trimpath ./cmd/dkl-local-server
|
||||
}
|
||||
|
||||
dist/dkl-local-server {
|
||||
prep: mkdir -p tmp
|
||||
daemon +sigterm: dist/dkl-local-server -data tmp -auto-unlock test
|
||||
}
|
||||
|
||||
dist/dkl-dir2config {
|
||||
prep: dist/dkl-dir2config --debug --in test-dir2config
|
||||
}
|
||||
|
||||
**/*.proto !dist/**/* {
|
||||
prep: for mod in @mods; do protoc -I ./ --go_out=plugins=grpc,paths=source_relative:. $mod; done
|
||||
prep: hack/build ./cmd/dkl-local-server
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@ -24,14 +23,13 @@ var (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Hosts []*Host
|
||||
Clusters []*Cluster
|
||||
Configs []*Template
|
||||
StaticPods []*Template `yaml:"static_pods"`
|
||||
BootstrapPods map[string][]*Template `yaml:"bootstrap_pods"`
|
||||
Addons map[string][]*Template
|
||||
SSLConfig string `yaml:"ssl_config"`
|
||||
CertRequests []*CertRequest `yaml:"cert_requests"`
|
||||
Hosts []*Host
|
||||
Clusters []*Cluster
|
||||
Configs []*Template
|
||||
StaticPods map[string][]*Template `yaml:"static_pods"`
|
||||
Addons map[string][]*Template
|
||||
SSLConfig string `yaml:"ssl_config"`
|
||||
CertRequests []*CertRequest `yaml:"cert_requests"`
|
||||
}
|
||||
|
||||
func FromBytes(data []byte) (*Config, error) {
|
||||
@ -43,7 +41,7 @@ func FromBytes(data []byte) (*Config, error) {
|
||||
}
|
||||
|
||||
func FromFile(path string) (*Config, error) {
|
||||
ba, err := ioutil.ReadFile(path)
|
||||
ba, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -121,7 +119,7 @@ func (c *Config) SaveTo(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, ba, 0600)
|
||||
return os.WriteFile(path, ba, 0600)
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
@ -162,7 +160,7 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
||||
base += string(filepath.Separator)
|
||||
log.Print("writing template details: ", base, "{in,data,out}")
|
||||
|
||||
if err := ioutil.WriteFile(base+"in", []byte(t.Template), 0600); err != nil {
|
||||
if err := os.WriteFile(base+"in", []byte(t.Template), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -171,7 +169,7 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(base+"data", yamlBytes, 0600); err != nil {
|
||||
if err := os.WriteFile(base+"data", yamlBytes, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -192,24 +190,29 @@ func (t *Template) Execute(contextName, elementName string, wr io.Writer, data i
|
||||
type Host struct {
|
||||
WithRev
|
||||
|
||||
Name string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
Template bool `json:",omitempty"`
|
||||
|
||||
MAC string
|
||||
Name string
|
||||
Labels map[string]string `json:",omitempty"`
|
||||
Annotations map[string]string `json:",omitempty"`
|
||||
|
||||
MAC string `json:",omitempty"`
|
||||
IP string
|
||||
IPs []string
|
||||
IPs []string `json:",omitempty"`
|
||||
Cluster string
|
||||
Group string
|
||||
|
||||
IPXE string
|
||||
Net string
|
||||
IPFrom map[string]string `json:",omitempty" yaml:"ip_from"`
|
||||
|
||||
IPXE string `json:",omitempty"`
|
||||
Kernel string
|
||||
Initrd string
|
||||
BootstrapConfig string `yaml:"bootstrap_config"`
|
||||
Config string
|
||||
Versions map[string]string
|
||||
|
||||
BootstrapPods string `yaml:"bootstrap_pods"`
|
||||
StaticPods string `yaml:"static_pods"`
|
||||
|
||||
Vars Vars
|
||||
}
|
||||
|
@ -34,8 +34,8 @@ func FromDir(
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Addons: make(map[string][]*Template),
|
||||
BootstrapPods: make(map[string][]*Template),
|
||||
Addons: make(map[string][]*Template),
|
||||
StaticPods: make(map[string][]*Template),
|
||||
}
|
||||
|
||||
// load clusters
|
||||
@ -121,23 +121,23 @@ func FromDir(
|
||||
}
|
||||
}
|
||||
|
||||
// cluster bootstrap pods
|
||||
// cluster static pods
|
||||
for _, host := range config.Hosts {
|
||||
bpSet := host.BootstrapPods
|
||||
bpSet := host.StaticPods
|
||||
if bpSet == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := config.BootstrapPods[bpSet]; ok {
|
||||
if _, ok := config.StaticPods[bpSet]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
templates := make([]*Template, 0)
|
||||
if err = loadTemplates(path.Join("bootstrap-pods", bpSet), &templates); err != nil {
|
||||
if err = loadTemplates(path.Join("static-pods", bpSet), &templates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.BootstrapPods[bpSet] = templates
|
||||
config.StaticPods[bpSet] = templates
|
||||
}
|
||||
|
||||
// load SSL configuration
|
||||
|
@ -1,5 +1,7 @@
|
||||
package secretstore
|
||||
|
||||
func Memzero(ba []byte) { memzero(ba) }
|
||||
|
||||
func memzero(ba []byte) {
|
||||
for i := range ba {
|
||||
ba[i] = 0
|
||||
|
@ -2,29 +2,34 @@ package secretstore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
Salt [aes.BlockSize]byte
|
||||
Keys []KeyEntry
|
||||
|
||||
unlocked bool
|
||||
key [32]byte
|
||||
salt [aes.BlockSize]byte
|
||||
keys []keyEntry
|
||||
}
|
||||
|
||||
type keyEntry struct {
|
||||
hash [64]byte
|
||||
encKey [32]byte
|
||||
type KeyEntry struct {
|
||||
Name string
|
||||
Hash [64]byte
|
||||
EncKey [32]byte
|
||||
}
|
||||
|
||||
func New() (s *Store) {
|
||||
@ -77,30 +82,32 @@ func (s *Store) Close() {
|
||||
}
|
||||
|
||||
func (s *Store) IsNew() bool {
|
||||
return len(s.keys) == 0
|
||||
return len(s.Keys) == 0
|
||||
}
|
||||
|
||||
func (s *Store) Unlocked() bool {
|
||||
return s.unlocked
|
||||
}
|
||||
|
||||
func (s *Store) Init(passphrase []byte) (err error) {
|
||||
func (s *Store) Init(name string, passphrase []byte) (err error) {
|
||||
err = randRead(s.key[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = randRead(s.salt[:])
|
||||
err = randRead(s.Salt[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.AddKey(passphrase)
|
||||
s.AddKey(name, passphrase)
|
||||
|
||||
s.unlocked = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var jsonFormatHdr = []byte("{json}")
|
||||
|
||||
func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
||||
memzero(s.key[:])
|
||||
s.unlocked = false
|
||||
@ -117,69 +124,77 @@ func (s *Store) ReadFrom(in io.Reader) (n int64, err error) {
|
||||
n += int64(nr)
|
||||
}
|
||||
|
||||
// read the salt
|
||||
readFull(s.salt[:])
|
||||
// read the file's start (json header or start of salt)
|
||||
|
||||
readFull(s.Salt[:len(jsonFormatHdr)])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// read the (encrypted) keys
|
||||
s.keys = make([]keyEntry, 0)
|
||||
for {
|
||||
k := keyEntry{}
|
||||
readFull(k.hash[:])
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
readFull(k.encKey[:])
|
||||
if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
|
||||
// old key file
|
||||
|
||||
// finish reading the salt
|
||||
readFull(s.Salt[len(jsonFormatHdr):])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.keys = append(s.keys, k)
|
||||
// read the (encrypted) keys
|
||||
s.Keys = make([]KeyEntry, 0)
|
||||
for {
|
||||
k := KeyEntry{Name: "key-" + strconv.Itoa(len(s.Keys))}
|
||||
readFull(k.Hash[:])
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
readFull(k.EncKey[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Keys = append(s.Keys, k)
|
||||
}
|
||||
}
|
||||
|
||||
err = json.NewDecoder(in).Decode(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) WriteTo(out io.Writer) (n int64, err error) {
|
||||
write := func(ba []byte) {
|
||||
var nr int
|
||||
nr, err = out.Write(ba)
|
||||
n += int64(nr)
|
||||
}
|
||||
|
||||
write(s.salt[:])
|
||||
_, err = out.Write(jsonFormatHdr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, k := range s.keys {
|
||||
write(k.hash[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
write(k.encKey[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = json.NewEncoder(out).Encode(s)
|
||||
return
|
||||
}
|
||||
|
||||
var ErrNoSuchKey = errors.New("no such key")
|
||||
|
||||
func (s *Store) HasKey(passphrase []byte) bool {
|
||||
key, hash := s.keyPairFromPassword(passphrase)
|
||||
defer memzero(key[:])
|
||||
|
||||
for _, k := range s.Keys {
|
||||
if k.Hash == hash {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
||||
key, hash := s.keyPairFromPassword(passphrase)
|
||||
memzero(passphrase)
|
||||
defer memzero(key[:])
|
||||
|
||||
var idx = -1
|
||||
for i := range s.keys {
|
||||
if hash == s.keys[i].hash {
|
||||
for i := range s.Keys {
|
||||
if hash == s.Keys[i].Hash {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
@ -189,28 +204,28 @@ func (s *Store) Unlock(passphrase []byte) (ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
s.decryptTo(s.key[:], s.keys[idx].encKey[:], &key)
|
||||
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
|
||||
|
||||
s.unlocked = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) AddKey(passphrase []byte) {
|
||||
func (s *Store) AddKey(name string, passphrase []byte) {
|
||||
key, hash := s.keyPairFromPassword(passphrase)
|
||||
memzero(passphrase)
|
||||
|
||||
defer memzero(key[:])
|
||||
|
||||
k := keyEntry{hash: hash}
|
||||
k := KeyEntry{Name: name, Hash: hash}
|
||||
|
||||
encKey := s.encrypt(s.key[:], &key)
|
||||
copy(k.encKey[:], encKey)
|
||||
copy(k.EncKey[:], encKey)
|
||||
|
||||
s.keys = append(s.keys, k)
|
||||
s.Keys = append(s.Keys, k)
|
||||
}
|
||||
|
||||
func (s *Store) keyPairFromPassword(password []byte) (key [32]byte, hash [64]byte) {
|
||||
keySlice := argon2.IDKey(password, s.salt[:], 1, 64*1024, 4, 32)
|
||||
keySlice := argon2.IDKey(password, s.Salt[:], 1, 64*1024, 4, 32)
|
||||
|
||||
copy(key[:], keySlice)
|
||||
memzero(keySlice)
|
||||
@ -236,12 +251,12 @@ func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
||||
|
||||
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
||||
dst = make([]byte, len(src))
|
||||
newEncrypter(s.salt, key).XORKeyStream(dst, src)
|
||||
newEncrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) decryptTo(dst []byte, src []byte, key *[32]byte) {
|
||||
newDecrypter(s.salt, key).XORKeyStream(dst, src)
|
||||
newDecrypter(s.Salt, key).XORKeyStream(dst, src)
|
||||
}
|
||||
|
||||
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
||||
|
@ -1,27 +0,0 @@
|
||||
set -e
|
||||
|
||||
dir=/var/lib/direktil/
|
||||
PATH=$PATH:$dir
|
||||
cd $dir
|
||||
|
||||
if [ ! -f govc.env ]; then
|
||||
echo ERROR: govc.env file not found in dir $dir ; exit 1
|
||||
fi
|
||||
source govc.env
|
||||
|
||||
if [ $# != 2 ]; then
|
||||
echo "Usage: $0 <VM_NAME> <NOVIT_HOST>" ; exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $NOVIT_VM_FOLDER || -z $NOVIT_ISO_FOLDER ]]; then
|
||||
echo "ERROR: All GOVC env vars (including NOVIT_VM_FOLDER and NOVIT_ISO_FOLDER) must be provided" ; exit 1
|
||||
fi
|
||||
|
||||
VM=$1
|
||||
HOST=$2
|
||||
|
||||
govc vm.power -off "$NOVIT_VM_FOLDER/$VM" || true
|
||||
sleep 5
|
||||
curl localhost:7606/hosts/$HOST/boot.iso | govc datastore.upload - "$NOVIT_ISO_FOLDER/$VM.iso"
|
||||
sleep 5
|
||||
govc vm.power -on "$NOVIT_VM_FOLDER/$VM"
|
Reference in New Issue
Block a user