Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab136be68 | |||
| 0dbab431a0 | |||
| 183099f7da | |||
| 47f695a8cd | |||
| f57aa581f3 | |||
| 63a206f1ac | |||
| f2b23df97b | |||
| 3085dac359 | |||
| 2af7ff85c1 | |||
| 512177cab0 | |||
| 26d3d0faeb | |||
| 834510760f | |||
| a2a970f93b | |||
| 350e753ae0 | |||
| 436be67bfd | |||
| 8ae52501c9 | |||
| 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 | |||
| 50bb60823f | |||
| 482d3c83ba | |||
| 74abbf9eda | |||
| 76c1861017 | |||
| 0d0494b825 | |||
| c6320049ff | |||
| 9e56acfc9a | |||
| 6197369e04 | |||
| d950bc6996 | |||
| 18dc85d6fb | |||
| 26953cf703 | |||
| 1f03315897 | |||
| 5a6c0fa3d8 | |||
| 4acdf88785 | |||
| bde41c9859 | |||
| 1e3ac9a0fb | |||
| 1672b901d4 | |||
| 11f3c953e2 | |||
| 3bc20e95cc | |||
| 1aefc5d2b7 | |||
| 5c432e3b42 | |||
| b6c714fac7 | |||
| e44303eab9 | |||
| 2a9295e8e8 | |||
| 52ffbe9727 | |||
| 811a3bddfd | |||
| 227c341f6b | |||
| 153c37b591 | |||
| 4ff85eaeb3 | |||
| 76e02c6f31 | |||
| 93b32eb52a | |||
| 0fcd219268 | |||
| 18d3c42fc7 | |||
| 645c617956 | |||
| dacfc8c6ce | |||
| 16a0ff0823 | |||
| 0d298c9951 | |||
| 3673a2f361 | |||
| 4d92925170 | |||
| a66d54d238 | |||
| 748a028161 | |||
| 5e667295ac | |||
| a54d4bc15e | |||
| 1ee5d1c15a | |||
| 49a16fe550 | |||
| 984b2e0057 | |||
| 863a436915 | |||
| 55c72aefa8 | |||
| 8ce4e97922 | |||
| f43f4fcec4 | |||
| 6ef93489bd | |||
| 85af5ccc36 | |||
| 051b42fda8 | |||
| 39ea639cc3 | |||
| b5b8514c59 | |||
| fec03e0a7e | |||
| 840824d438 | |||
| 59fe6373dc | |||
| 43304de2ca | |||
| 4ad32c64a6 | |||
| 49c73be97a | |||
| c0fc7bbe3d | |||
| 6ddc4d6da4 | |||
| 6c8835c5ab | |||
| 4b0f5dca84 | |||
| 48ab32f319 | |||
| a7158e9e56 | |||
| daa919e953 | |||
| 50ee480caf | |||
| d27c4ed7a3 | |||
| 36e1367522 | |||
| 4679da1c1e | |||
| dde0ad6975 | |||
| ee2779cc9d | |||
| a1fcd4093c | |||
| 9b62d598bb | |||
| 456722a616 | |||
| 6a0cd6da02 | |||
| 676c4bc21b | |||
| 201bca587e | |||
| 024fcdd35c | |||
| 663b42ed47 | |||
| c21b07572d | |||
| 1391108d60 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
tmp
|
||||
dist
|
||||
test-run
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,8 @@
|
||||
*.sw[po]
|
||||
modd-local.conf
|
||||
/tmp
|
||||
/test-dir2config
|
||||
/config.yaml
|
||||
/dist
|
||||
/go.work
|
||||
/go.work.sum
|
||||
|
||||
40
Dockerfile
40
Dockerfile
@ -1,23 +1,35 @@
|
||||
from novit.tech/direktil/dkl:bbea9b9 as dkl
|
||||
# ------------------------------------------------------------------------
|
||||
from golang:1.11.5 as build
|
||||
from golang:1.25.5-trixie as build
|
||||
|
||||
env pkg novit.nc/direktil/local-server
|
||||
copy vendor /go/src/${pkg}/vendor
|
||||
copy pkg /go/src/${pkg}/pkg
|
||||
copy cmd /go/src/${pkg}/cmd
|
||||
workdir /go/src/${pkg}
|
||||
run go test ./... \
|
||||
&& go install ./cmd/...
|
||||
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:trixie
|
||||
entrypoint ["/bin/dkl-local-server"]
|
||||
|
||||
env _uncache=1
|
||||
run apt-get update \
|
||||
&& apt-get install -y genisoimage gdisk dosfstools util-linux udev \
|
||||
&& yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \
|
||||
grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \
|
||||
&& apt-get clean
|
||||
|
||||
run yes |apt-get install -y grub2 grub-pc-bin grub-efi-amd64-bin \
|
||||
&& apt-get clean
|
||||
|
||||
copy --from=build /go/bin/ /bin/
|
||||
copy --from=dkl /bin/dkl /bin/dls /bin/
|
||||
copy --from=build /src/dist/ /bin/
|
||||
|
||||
155
cmd/dkl-dir2config/assemble.go
Normal file
155
cmd/dkl-dir2config/assemble.go
Normal file
@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func mergeIn(tgt, add map[any]any) {
|
||||
mergeLoop:
|
||||
for k, v := range add {
|
||||
switch v := v.(type) {
|
||||
case map[any]any:
|
||||
if tgtV, ok := tgt[k]; ok {
|
||||
switch tgtV := tgtV.(type) {
|
||||
case map[any]any:
|
||||
mergeIn(tgtV, v)
|
||||
continue mergeLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tgt[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func assemble(path string) (yamlBytes []byte, err error) {
|
||||
obj := map[any]any{}
|
||||
|
||||
if Debug {
|
||||
log.Printf("assemble %q", path)
|
||||
}
|
||||
|
||||
err = eachFragment(path, searchList, func(r io.Reader) (err error) {
|
||||
m := map[any]any{}
|
||||
err = yaml.NewDecoder(r).Decode(&m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mergeIn(obj, m)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to assemble %q: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
yamlBytes, err = yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Printf("assemble %q result:\n%s", path, yamlBytes)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func eachFragment(path string, searchList []FS, walk func(io.Reader) error) (err error) {
|
||||
var r io.ReadCloser
|
||||
|
||||
for len(searchList) != 0 {
|
||||
fs := searchList[0]
|
||||
|
||||
r, err = fs.Open(path + ".yaml")
|
||||
if os.IsNotExist(err) {
|
||||
searchList = searchList[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// found and open
|
||||
break
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
ba, err := io.ReadAll(r)
|
||||
r.Close()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Print("fragment:\n", string(ba))
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(ba)
|
||||
|
||||
for {
|
||||
var line string
|
||||
line, err = in.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
genCmd, found := strings.CutPrefix(line, "#!gen ")
|
||||
if found {
|
||||
cmdArgs := strings.Fields(genCmd)
|
||||
if Debug {
|
||||
log.Print("#!gen ", cmdArgs)
|
||||
}
|
||||
|
||||
cmd := *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
|
||||
}
|
||||
|
||||
includePath = strings.TrimSpace(includePath)
|
||||
if Debug {
|
||||
log.Print("#!include ", includePath)
|
||||
}
|
||||
err = eachFragment(includePath, searchList, walk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("include %q: %w", includePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
in = bytes.NewBuffer(ba)
|
||||
err = walk(in)
|
||||
return
|
||||
}
|
||||
34
cmd/dkl-dir2config/fs.go
Normal file
34
cmd/dkl-dir2config/fs.go
Normal file
@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
)
|
||||
|
||||
type FS interface {
|
||||
Open(path string) (io.ReadCloser, error)
|
||||
List(path string) ([]string, error)
|
||||
}
|
||||
|
||||
type fsFS struct{ iofs.FS }
|
||||
|
||||
func (fs fsFS) Open(path string) (io.ReadCloser, error) {
|
||||
return fs.FS.Open(path)
|
||||
}
|
||||
|
||||
func (fs fsFS) List(path string) (entries []string, err error) {
|
||||
dirEnts, err := iofs.ReadDir(fs.FS, path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
entries = make([]string, 0, len(dirEnts))
|
||||
for _, ent := range dirEnts {
|
||||
if ent.IsDir() {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, ent.Name())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
38
cmd/dkl-dir2config/git.go
Normal file
38
cmd/dkl-dir2config/git.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
type gitFS struct{ *object.Tree }
|
||||
|
||||
func (fs gitFS) Open(path string) (r io.ReadCloser, err error) {
|
||||
f, err := fs.Tree.File(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return f.Reader()
|
||||
}
|
||||
|
||||
func (fs gitFS) List(path string) (entries []string, err error) {
|
||||
tree, err := fs.Tree.Tree(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
entries = make([]string, 0, len(tree.Entries))
|
||||
|
||||
for _, ent := range tree.Entries {
|
||||
if !ent.Mode.IsFile() {
|
||||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, ent.Name)
|
||||
}
|
||||
|
||||
sort.Strings(entries)
|
||||
return
|
||||
}
|
||||
@ -1,30 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
|
||||
"novit.nc/direktil/local-server/pkg/clustersconfig"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
var (
|
||||
dir = flag.String("in", ".", "Source directory")
|
||||
outPath = flag.String("out", "config.yaml", "Output file")
|
||||
defaultsPath = flag.String("defaults", "defaults", "Path to the defaults")
|
||||
Debug = false
|
||||
|
||||
dir = flag.String("in", ".", "Source directory")
|
||||
outPath = flag.String("out", "config.yaml", "Output file")
|
||||
|
||||
base fs.FS
|
||||
|
||||
src *clustersconfig.Config
|
||||
dst *localconfig.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&Debug, "debug", Debug, "debug")
|
||||
}
|
||||
|
||||
func loadSrc() {
|
||||
var err error
|
||||
src, err = clustersconfig.FromDir(*dir, *defaultsPath)
|
||||
src, err = clustersconfig.FromDir(read, assemble, listBase, listMerged)
|
||||
if err != nil {
|
||||
log.Fatal("failed to load config from dir: ", err)
|
||||
}
|
||||
@ -35,6 +49,11 @@ func main() {
|
||||
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
||||
|
||||
base = os.DirFS(*dir)
|
||||
searchList = append(searchList, fsFS{base})
|
||||
|
||||
openIncludes()
|
||||
|
||||
loadSrc()
|
||||
|
||||
dst = &localconfig.Config{
|
||||
@ -51,8 +70,6 @@ func main() {
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
for _, host := range src.Hosts {
|
||||
loadSrc() // FIXME ugly fix of some template caching or something
|
||||
|
||||
log.Print("rendering host ", host.Name)
|
||||
ctx, err := newRenderContext(host, src)
|
||||
|
||||
@ -71,24 +88,37 @@ func main() {
|
||||
}
|
||||
ips = append(ips, host.IPs...)
|
||||
|
||||
if ctx.Group.Versions["modules"] == "" {
|
||||
if ctx.Host.Versions["modules"] == "" {
|
||||
// default modules' version to kernel's version
|
||||
ctx.Group.Versions["modules"] = ctx.Group.Kernel
|
||||
ctx.Host.Versions["modules"] = ctx.Host.Kernel
|
||||
}
|
||||
|
||||
dst.Hosts = append(dst.Hosts, &localconfig.Host{
|
||||
renderedHost := &localconfig.Host{
|
||||
Name: host.Name,
|
||||
|
||||
ClusterName: ctx.Cluster.Name,
|
||||
|
||||
Labels: ctx.Labels,
|
||||
Annotations: ctx.Annotations,
|
||||
|
||||
MACs: macs,
|
||||
IPs: ips,
|
||||
|
||||
IPXE: ctx.Group.IPXE, // TODO render
|
||||
IPXE: ctx.Host.IPXE, // TODO render
|
||||
|
||||
Kernel: ctx.Group.Kernel,
|
||||
Initrd: ctx.Group.Initrd,
|
||||
Versions: ctx.Group.Versions,
|
||||
Kernel: ctx.Host.Kernel,
|
||||
Initrd: ctx.Host.Initrd,
|
||||
Versions: ctx.Host.Versions,
|
||||
|
||||
Config: ctx.Config(),
|
||||
})
|
||||
BootstrapConfig: ctx.BootstrapConfig(),
|
||||
Config: ctx.Config(),
|
||||
}
|
||||
|
||||
if host.Template {
|
||||
dst.HostTemplates = append(dst.HostTemplates, renderedHost)
|
||||
} else {
|
||||
dst.Hosts = append(dst.Hosts, renderedHost)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@ -99,39 +129,93 @@ func main() {
|
||||
|
||||
defer out.Close()
|
||||
|
||||
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
||||
log.Fatal("failed to render output: ", err)
|
||||
}
|
||||
switch ext := path.Ext(*outPath); ext {
|
||||
case ".yaml":
|
||||
out.Write([]byte("# dkl-dir2config " + Version + "\n"))
|
||||
|
||||
}
|
||||
|
||||
func renderAddons(cluster *clustersconfig.Cluster) string {
|
||||
if len(cluster.Addons) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
addons := src.Addons[cluster.Addons]
|
||||
if addons == nil {
|
||||
log.Fatalf("cluster %q: no addons with name %q", cluster.Name, cluster.Addons)
|
||||
}
|
||||
|
||||
clusterAsMap := asMap(cluster)
|
||||
clusterAsMap["kubernetes_svc_ip"] = cluster.KubernetesSvcIP().String()
|
||||
clusterAsMap["dns_svc_ip"] = cluster.DNSSvcIP().String()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for _, addon := range addons {
|
||||
fmt.Fprintf(buf, "---\n# addon: %s\n", addon.Name)
|
||||
err := addon.Execute(buf, clusterAsMap, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("cluster %q: addons %q: failed to render %q: %v",
|
||||
cluster.Name, cluster.Addons, addon.Name, err)
|
||||
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
||||
log.Fatal("failed to render output: ", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(buf)
|
||||
case ".json":
|
||||
if err = json.NewEncoder(out).Encode(dst); err != nil {
|
||||
log.Fatal("failed to render output: ", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal("unknown output file extension: ", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func cfgPath(subPath string) string { return filepath.Join(*dir, subPath) }
|
||||
|
||||
func openIncludes() {
|
||||
includesFile, err := base.Open("includes.yaml")
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal("failed to open includes: ", err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
includes := make([]struct {
|
||||
Path string
|
||||
Branch string
|
||||
Tag string
|
||||
}, 0)
|
||||
|
||||
err = yaml.NewDecoder(includesFile).Decode(&includes)
|
||||
if err != nil {
|
||||
log.Fatal("failed to parse includes: ", err)
|
||||
}
|
||||
|
||||
for _, include := range includes {
|
||||
switch {
|
||||
case include.Branch != "" || include.Tag != "":
|
||||
p := cfgPath(include.Path) // FIXME parse git path to allow remote repos
|
||||
|
||||
var rev plumbing.Revision
|
||||
|
||||
switch {
|
||||
case include.Branch != "":
|
||||
log.Printf("opening include path %q as git, branch %q", p, include.Branch)
|
||||
rev = plumbing.Revision(plumbing.NewBranchReferenceName(include.Branch))
|
||||
|
||||
case include.Tag != "":
|
||||
log.Printf("opening include path %q as git, tag %q", p, include.Branch)
|
||||
rev = plumbing.Revision(plumbing.NewTagReferenceName(include.Branch))
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(p)
|
||||
if err != nil {
|
||||
log.Fatal("failed to open: ", err)
|
||||
}
|
||||
|
||||
revH, err := repo.ResolveRevision(rev)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to resolve revision %s: %v", rev, err)
|
||||
}
|
||||
|
||||
log.Print(" -> resolved to commit ", *revH)
|
||||
|
||||
commit, err := repo.CommitObject(*revH)
|
||||
if err != nil {
|
||||
log.Fatal("failed to get commit object: ", err)
|
||||
}
|
||||
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
log.Fatal("failed to open git tree: ", err)
|
||||
}
|
||||
|
||||
searchList = append(searchList, gitFS{tree})
|
||||
|
||||
default:
|
||||
p := cfgPath(include.Path)
|
||||
log.Printf("opening include path %q as raw dir", p)
|
||||
|
||||
searchList = append(searchList, fsFS{os.DirFS(p)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
cmd/dkl-dir2config/render-cluster.go
Normal file
142
cmd/dkl-dir2config/render-cluster.go
Normal file
@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
func clusterFuncs(clusterSpec *clustersconfig.Cluster) map[string]any {
|
||||
cluster := clusterSpec.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) {
|
||||
return fmt.Sprintf("{{ token %q %q }}", cluster, name)
|
||||
},
|
||||
|
||||
"ca_key": func(name string) (s string, err error) {
|
||||
// TODO check CA exists
|
||||
// ?ctx.clusterConfig.CA(name)
|
||||
return fmt.Sprintf("{{ ca_key %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"ca_crt": func(name string) (s string, err error) {
|
||||
// TODO check CA exists
|
||||
return fmt.Sprintf("{{ ca_crt %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"ca_dir": func(name string) (s string, err error) {
|
||||
return fmt.Sprintf("{{ ca_dir %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"hosts_by_cluster": func(cluster string) (hosts []any) {
|
||||
for _, host := range src.Hosts {
|
||||
if host.Cluster == cluster {
|
||||
hosts = append(hosts, asMap(host))
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Printf("WARNING: no hosts in cluster %q", cluster)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
|
||||
"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))
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Printf("WARNING: no hosts in group %q", group)
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func renderClusterTemplates(cluster *clustersconfig.Cluster, setName string,
|
||||
templates []*clustersconfig.Template) []byte {
|
||||
clusterAsMap := asMap(cluster)
|
||||
clusterAsMap["kubernetes_svc_ip"] = cluster.KubernetesSvcIP().String()
|
||||
clusterAsMap["dns_svc_ip"] = cluster.DNSSvcIP().String()
|
||||
|
||||
funcs := clusterFuncs(cluster)
|
||||
|
||||
log.Print("rendering cluster templates in ", setName)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
contextName := "cluster:" + cluster.Name
|
||||
|
||||
for _, t := range templates {
|
||||
log.Print("- template: ", setName, ": ", t.Name)
|
||||
fmt.Fprintf(buf, "---\n# %s: %s\n", setName, t.Name)
|
||||
err := t.Execute(contextName, path.Join(setName, t.Name), buf, clusterAsMap, funcs)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("cluster %q: %s: failed to render %q: %v",
|
||||
cluster.Name, setName, t.Name, err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(buf)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func renderAddons(cluster *clustersconfig.Cluster) string {
|
||||
if len(cluster.Addons) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for _, addonSet := range cluster.Addons {
|
||||
addons := src.Addons[addonSet]
|
||||
if addons == nil {
|
||||
log.Fatalf("cluster %q: no addons with name %q", cluster.Name, addonSet)
|
||||
}
|
||||
|
||||
buf.Write(renderClusterTemplates(cluster, "addons", addons))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type namePod struct {
|
||||
Namespace string
|
||||
Name string
|
||||
Pod map[string]interface{}
|
||||
}
|
||||
@ -3,20 +3,33 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.nc/direktil/local-server/pkg/clustersconfig"
|
||||
"novit.tech/direktil/pkg/config"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
type renderContext struct {
|
||||
Host *clustersconfig.Host
|
||||
Group *clustersconfig.Group
|
||||
Cluster *clustersconfig.Cluster
|
||||
Vars map[string]interface{}
|
||||
ConfigTemplate *clustersconfig.Template
|
||||
StaticPodsTemplate *clustersconfig.Template
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
|
||||
Host *clustersconfig.Host
|
||||
Cluster *clustersconfig.Cluster
|
||||
Vars map[string]any
|
||||
|
||||
BootstrapConfigTemplate *clustersconfig.Template
|
||||
ConfigTemplate *clustersconfig.Template
|
||||
StaticPodsTemplate *clustersconfig.Template
|
||||
|
||||
clusterConfig *clustersconfig.Config
|
||||
}
|
||||
@ -28,98 +41,170 @@ func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (ct
|
||||
return
|
||||
}
|
||||
|
||||
group := cfg.Group(host.Group)
|
||||
if group == nil {
|
||||
err = fmt.Errorf("no group named %q", host.Group)
|
||||
return
|
||||
}
|
||||
vars := make(map[string]any)
|
||||
|
||||
vars := make(map[string]interface{})
|
||||
|
||||
for _, oVars := range []map[string]interface{}{
|
||||
for _, oVars := range []map[string]any{
|
||||
cluster.Vars,
|
||||
group.Vars,
|
||||
host.Vars,
|
||||
} {
|
||||
for k, v := range oVars {
|
||||
vars[k] = v
|
||||
}
|
||||
mapMerge(vars, oVars)
|
||||
}
|
||||
|
||||
return &renderContext{
|
||||
Host: host,
|
||||
Group: group,
|
||||
Cluster: cluster,
|
||||
Vars: vars,
|
||||
ConfigTemplate: cfg.ConfigTemplate(group.Config),
|
||||
StaticPodsTemplate: cfg.StaticPodsTemplate(group.StaticPods),
|
||||
Labels: mergeLabels(cluster.Labels, host.Labels),
|
||||
Annotations: mergeLabels(cluster.Annotations, host.Annotations),
|
||||
|
||||
Host: host,
|
||||
Cluster: cluster,
|
||||
Vars: vars,
|
||||
|
||||
BootstrapConfigTemplate: cfg.ConfigTemplate(host.BootstrapConfig),
|
||||
ConfigTemplate: cfg.ConfigTemplate(host.Config),
|
||||
|
||||
clusterConfig: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctx *renderContext) Config() string {
|
||||
if ctx.ConfigTemplate == nil {
|
||||
log.Fatalf("no such config: %q", ctx.Group.Config)
|
||||
func mergeLabels(sources ...map[string]string) map[string]string {
|
||||
ret := map[string]string{}
|
||||
|
||||
for _, src := range sources {
|
||||
for k, v := range src {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
ctxMap := ctx.asMap()
|
||||
return ret
|
||||
}
|
||||
|
||||
templateFuncs := ctx.templateFuncs(ctxMap)
|
||||
func mapMerge(target, source map[string]interface{}) {
|
||||
for k, v := range source {
|
||||
target[k] = genericMerge(target[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
render := func(what string, t *clustersconfig.Template) (s string, err error) {
|
||||
buf := &bytes.Buffer{}
|
||||
err = t.Execute(buf, ctxMap, templateFuncs)
|
||||
if err != nil {
|
||||
log.Printf("host %s: failed to render %s [%q]: %v", ctx.Host.Name, what, t.Name, err)
|
||||
return
|
||||
func genericMerge(target, source interface{}) (result interface{}) {
|
||||
srcV := reflect.ValueOf(source)
|
||||
tgtV := reflect.ValueOf(target)
|
||||
|
||||
if srcV.Kind() == reflect.Map && tgtV.Kind() == reflect.Map {
|
||||
// XXX maybe more specific later
|
||||
result = map[interface{}]interface{}{}
|
||||
resultV := reflect.ValueOf(result)
|
||||
|
||||
tgtIt := tgtV.MapRange()
|
||||
for tgtIt.Next() {
|
||||
sv := srcV.MapIndex(tgtIt.Key())
|
||||
if sv.Kind() == 0 {
|
||||
resultV.SetMapIndex(tgtIt.Key(), tgtIt.Value())
|
||||
continue
|
||||
}
|
||||
|
||||
merged := genericMerge(tgtIt.Value().Interface(), sv.Interface())
|
||||
resultV.SetMapIndex(tgtIt.Key(), reflect.ValueOf(merged))
|
||||
}
|
||||
|
||||
srcIt := srcV.MapRange()
|
||||
for srcIt.Next() {
|
||||
if resultV.MapIndex(srcIt.Key()).Kind() != 0 {
|
||||
continue // already done
|
||||
}
|
||||
|
||||
resultV.SetMapIndex(srcIt.Key(), srcIt.Value())
|
||||
}
|
||||
|
||||
s = buf.String()
|
||||
return
|
||||
}
|
||||
|
||||
extraFuncs := ctx.templateFuncs(ctxMap)
|
||||
return source
|
||||
}
|
||||
|
||||
extraFuncs["static_pods"] = func() (string, error) {
|
||||
name := ctx.Group.StaticPods
|
||||
if len(name) == 0 {
|
||||
return "", fmt.Errorf("group %q has no static pods defined", ctx.Group.Name)
|
||||
}
|
||||
|
||||
t := ctx.clusterConfig.StaticPodsTemplate(name)
|
||||
if t == nil {
|
||||
return "", fmt.Errorf("no static pods template named %q", name)
|
||||
}
|
||||
|
||||
return render("static pods", t)
|
||||
func (ctx *renderContext) Name() string {
|
||||
switch {
|
||||
case ctx.Host != nil:
|
||||
return "host:" + ctx.Host.Name
|
||||
case ctx.Cluster != nil:
|
||||
return "cluster:" + ctx.Cluster.Name
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
if err := ctx.ConfigTemplate.Execute(buf, ctxMap, extraFuncs); err != nil {
|
||||
log.Fatalf("failed to render config %q for host %q: %v", ctx.Group.Config, ctx.Host.Name, err)
|
||||
func (ctx *renderContext) BootstrapConfig() string {
|
||||
if ctx.BootstrapConfigTemplate == nil {
|
||||
log.Fatalf("no such (bootstrap) config: %q", ctx.Host.BootstrapConfig)
|
||||
}
|
||||
return ctx.renderConfig(ctx.BootstrapConfigTemplate)
|
||||
}
|
||||
|
||||
func (ctx *renderContext) Config() string {
|
||||
if ctx.ConfigTemplate == nil {
|
||||
log.Fatalf("no such config: %q", ctx.Host.Config)
|
||||
}
|
||||
return ctx.renderConfig(ctx.ConfigTemplate)
|
||||
}
|
||||
|
||||
func (ctx *renderContext) renderConfig(configTemplate *clustersconfig.Template) string {
|
||||
buf := new(strings.Builder)
|
||||
ctx.renderConfigTo(buf, configTemplate)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (ctx *renderContext) StaticPods() (ba []byte, err error) {
|
||||
if ctx.StaticPodsTemplate == nil {
|
||||
log.Fatalf("no such static-pods: %q", ctx.Group.StaticPods)
|
||||
}
|
||||
func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clustersconfig.Template) {
|
||||
ctxName := ctx.Name()
|
||||
|
||||
ctxMap := ctx.asMap()
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
if err = ctx.StaticPodsTemplate.Execute(buf, ctxMap, ctx.templateFuncs(ctxMap)); err != nil {
|
||||
return
|
||||
extraFuncs := ctx.templateFuncs(ctxMap)
|
||||
|
||||
extraFuncs["static_pods_files"] = func(dir string) (string, error) {
|
||||
namePods := ctx.renderStaticPods()
|
||||
|
||||
defs := make([]config.FileDef, 0)
|
||||
|
||||
for _, namePod := range namePods {
|
||||
name := namePod.Namespace + "_" + namePod.Name
|
||||
|
||||
ba, err := yaml.Marshal(namePod.Pod)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("static pod %s: failed to render: %v", name, err)
|
||||
}
|
||||
|
||||
defs = append(defs, config.FileDef{
|
||||
Path: path.Join(dir, name+".yaml"),
|
||||
Mode: 0640,
|
||||
Content: string(ba),
|
||||
})
|
||||
}
|
||||
|
||||
ba, err := yaml.Marshal(defs)
|
||||
return string(ba), err
|
||||
}
|
||||
|
||||
ba = buf.Bytes()
|
||||
return
|
||||
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) {
|
||||
@ -135,53 +220,58 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err = req.Execute(buf, ctxMap, nil)
|
||||
err = req.Execute(ctx.Name(), "req:"+name, buf, ctxMap, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
reqJson := cleanJsonObject(buf.String())
|
||||
|
||||
key := name
|
||||
if req.PerHost {
|
||||
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())
|
||||
dir, cluster, req.CA, key, req.Profile, req.Label, reqJson)
|
||||
|
||||
} else {
|
||||
default:
|
||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
|
||||
cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||
cluster, req.CA, key, req.Profile, req.Label, reqJson)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"token": func(name string) (s string) {
|
||||
return fmt.Sprintf("{{ token %q %q }}", cluster, name)
|
||||
},
|
||||
funcs := clusterFuncs(ctx.Cluster)
|
||||
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
|
||||
}
|
||||
|
||||
"ca_key": func(name string) (s string, err error) {
|
||||
// TODO check CA exists
|
||||
// ?ctx.clusterConfig.CA(name)
|
||||
return fmt.Sprintf("{{ ca_key %q %q }}", cluster, name), nil
|
||||
default:
|
||||
if v != nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
},
|
||||
|
||||
"ca_crt": func(name string) (s string, err error) {
|
||||
// TODO check CA exists
|
||||
return fmt.Sprintf("{{ ca_crt %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"ca_dir": func(name string) (s string, err error) {
|
||||
return fmt.Sprintf("{{ ca_dir %q %q }}", cluster, name), nil
|
||||
},
|
||||
|
||||
"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")
|
||||
@ -191,11 +281,31 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
return getKeyCert(name, "tls_dir")
|
||||
},
|
||||
|
||||
"hosts_of_group": func() (hosts []interface{}) {
|
||||
hosts = make([]interface{}, 0)
|
||||
"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)
|
||||
},
|
||||
"host_download_token": func() (s string) {
|
||||
return "{{ host_download_token }}"
|
||||
},
|
||||
"asset_download_token": func(args ...string) (s string) {
|
||||
argsStr := new(strings.Builder)
|
||||
for _, arg := range args {
|
||||
argsStr.WriteByte(' ')
|
||||
argsStr.WriteString(strconv.Quote(arg))
|
||||
}
|
||||
return "{{ asset_download_token" + argsStr.String() + " }}"
|
||||
},
|
||||
|
||||
"hosts_of_group": func() (hosts []any) {
|
||||
hosts = make([]any, 0)
|
||||
|
||||
for _, host := range ctx.clusterConfig.Hosts {
|
||||
if host.Group != ctx.Host.Group {
|
||||
if host.Cluster == ctx.Cluster.Name && host.Group != ctx.Host.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -207,13 +317,35 @@ 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
|
||||
}
|
||||
return funcs
|
||||
}
|
||||
|
||||
func (ctx *renderContext) asMap() map[string]interface{} {
|
||||
|
||||
54
cmd/dkl-dir2config/render-context_test.go
Normal file
54
cmd/dkl-dir2config/render-context_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
if v := genericMerge("a", "b"); v != "b" {
|
||||
t.Errorf("got %q", v)
|
||||
}
|
||||
|
||||
if v := unparse(genericMerge(parse(`
|
||||
a: t
|
||||
b: t
|
||||
m:
|
||||
a1: t
|
||||
b1: t
|
||||
`), parse(`
|
||||
a: s
|
||||
c: s
|
||||
m:
|
||||
a1: s
|
||||
c1: s
|
||||
`))); "\n"+v != `
|
||||
a: s
|
||||
b: t
|
||||
c: s
|
||||
m:
|
||||
a1: s
|
||||
b1: t
|
||||
c1: s
|
||||
` {
|
||||
t.Errorf("got\n%s", v)
|
||||
}
|
||||
}
|
||||
|
||||
func parse(s string) (r interface{}) {
|
||||
r = map[string]interface{}{}
|
||||
err := yaml.Unmarshal([]byte(s), r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func unparse(v interface{}) (s string) {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(ba)
|
||||
}
|
||||
77
cmd/dkl-dir2config/render-host.go
Normal file
77
cmd/dkl-dir2config/render-host.go
Normal file
@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"novit.tech/direktil/local-server/pkg/clustersconfig"
|
||||
)
|
||||
|
||||
func (ctx *renderContext) renderStaticPods() (pods []namePod) {
|
||||
if ctx.Host.StaticPods == "" {
|
||||
return
|
||||
}
|
||||
|
||||
staticPods, ok := src.StaticPods[ctx.Host.StaticPods]
|
||||
if !ok {
|
||||
log.Fatalf("no static pods template named %q", ctx.Host.StaticPods)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
for n := 0; ; n++ {
|
||||
str := buf.String()
|
||||
|
||||
podMap := map[string]any{}
|
||||
err := dec.Decode(podMap)
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Fatalf("static pod %d: failed to parse: %v\n%s", n, err, str)
|
||||
}
|
||||
|
||||
if len(podMap) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if podMap["metadata"] == nil {
|
||||
log.Fatalf("static pod %d: no metadata\n%s", n, buf.String())
|
||||
}
|
||||
|
||||
md := podMap["metadata"].(map[any]any)
|
||||
|
||||
namespace := md["namespace"].(string)
|
||||
name := md["name"].(string)
|
||||
|
||||
pods = append(pods, namePod{namespace, name, podMap})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *renderContext) renderHostTemplates(setName string,
|
||||
templates []*clustersconfig.Template) []byte {
|
||||
|
||||
log.Print("rendering host templates in ", setName)
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 16<<10))
|
||||
|
||||
for _, t := range templates {
|
||||
log.Print("- template: ", setName, ": ", t.Name)
|
||||
fmt.Fprintf(buf, "---\n# %s: %s\n", setName, t.Name)
|
||||
|
||||
ctx.renderConfigTo(buf, t)
|
||||
fmt.Fprintln(buf)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
59
cmd/dkl-dir2config/search-list.go
Normal file
59
cmd/dkl-dir2config/search-list.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var searchList = make([]FS, 0)
|
||||
|
||||
// read the first file matching path in the search list
|
||||
func read(path string) (ba []byte, err error) {
|
||||
for _, fs := range searchList {
|
||||
var r io.ReadCloser
|
||||
r, err = fs.Open(path)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
func listBase(path string) ([]string, error) {
|
||||
return fsFS{base}.List(path)
|
||||
}
|
||||
|
||||
func listMerged(path string) (entries []string, err error) {
|
||||
seen := map[string]bool{}
|
||||
for _, fs := range searchList {
|
||||
var fsEnts []string
|
||||
fsEnts, err = fs.List(path)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ent := range fsEnts {
|
||||
if !seen[ent] {
|
||||
entries = append(entries, ent)
|
||||
seen[ent] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(entries)
|
||||
return
|
||||
}
|
||||
15
cmd/dkl-dir2config/utils.go
Normal file
15
cmd/dkl-dir2config/utils.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func cleanJsonObject(raw string) string {
|
||||
v := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||
panic(fmt.Errorf("invalid json: %w\n%s", err, raw))
|
||||
}
|
||||
clean, _ := json.Marshal(v)
|
||||
return string(clean)
|
||||
}
|
||||
@ -1,36 +1,44 @@
|
||||
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")
|
||||
if reqToken != "" {
|
||||
return reqToken == "Bearer "+token
|
||||
}
|
||||
|
||||
return reqToken == "Bearer "+token
|
||||
return r.URL.Query().Get("token") == token
|
||||
}
|
||||
|
||||
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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if !authorizeToken(req, *token) {
|
||||
forbidden(w, req)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func requireAdmin(handler http.Handler) http.Handler {
|
||||
return requireToken(&adminToken, handler)
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -17,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
|
||||
}
|
||||
@ -29,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
|
||||
}
|
||||
@ -56,8 +56,60 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
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", "1.0.0")
|
||||
path, err := distFetch("grub-support", *grubSupportVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -81,7 +133,10 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
devb, err := exec.Command("losetup", "--find", "--show", "--partscan", bootImg.Name()).CombinedOutput()
|
||||
log.Print("running losetup...")
|
||||
cmd := exec.Command("losetup", "--find", "--show", "--partscan", bootImg.Name())
|
||||
cmd.Stderr = os.Stderr
|
||||
devb, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -93,6 +148,7 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
|
||||
}()
|
||||
|
||||
log.Print("device: ", dev)
|
||||
syncSysToDev()
|
||||
|
||||
tempDir := bootImg.Name() + ".p1.mount"
|
||||
|
||||
@ -106,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,33 +3,29 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
)
|
||||
|
||||
func buildBootISO(out io.Writer, ctx *renderContext) error {
|
||||
tempDir, err := ioutil.TempDir("/tmp", "iso-")
|
||||
func buildBootISO(out io.Writer, ctx *renderContext) (err error) {
|
||||
tempDir, err := os.MkdirTemp("/tmp", "iso-v2-")
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
buildRes := func(build func(out io.Writer, ctx *renderContext) error, dst string) (err error) {
|
||||
log.Printf("iso-v2: building %s", dst)
|
||||
|
||||
outPath := filepath.Join(tempDir, dst)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
||||
if err = os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -37,32 +33,55 @@ func buildBootISO(out io.Writer, ctx *renderContext) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
err = build(out, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
err = func() (err error) {
|
||||
// grub
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tempDir, "grub"), 0755); err != nil {
|
||||
return err
|
||||
if err = os.MkdirAll(filepath.Join(tempDir, "grub"), 0755); err != nil {
|
||||
return
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||
search --set=root --file /config.yaml
|
||||
|
||||
// create a tag file
|
||||
bootstrapBytes, _, err := ctx.BootstrapConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := xxhash.New()
|
||||
fmt.Fprintln(h, ctx.Host.Kernel)
|
||||
h.Write(bootstrapBytes)
|
||||
|
||||
tag := "dkl-" + strconv.FormatUint(h.Sum64(), 32) + ".tag"
|
||||
|
||||
f, err := os.Create(filepath.Join(tempDir, tag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f.Write([]byte("direktil marker file\n"))
|
||||
f.Close()
|
||||
|
||||
err = os.WriteFile(filepath.Join(tempDir, "grub", "grub.cfg"), []byte(`
|
||||
search --set=root --file /`+tag+`
|
||||
|
||||
insmod all_video
|
||||
set timeout=3
|
||||
|
||||
menuentry "Direktil" {
|
||||
linux /vmlinuz direktil.boot=DEVNAME=sr0 direktil.boot.fs=iso9660
|
||||
linux /vmlinuz `+ctx.CmdLine+`
|
||||
initrd /initrd
|
||||
}
|
||||
`), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
coreImgPath := filepath.Join(tempDir, "grub", "core.img")
|
||||
@ -93,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
|
||||
}
|
||||
@ -102,7 +121,7 @@ menuentry "Direktil" {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadFile(coreImgPath)
|
||||
b, err = os.ReadFile(coreImgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -117,50 +136,9 @@ menuentry "Direktil" {
|
||||
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
|
||||
}
|
||||
}
|
||||
buildRes(fetchKernel, "vmlinuz")
|
||||
buildRes(buildInitrd, "initrd")
|
||||
|
||||
// build the ISO
|
||||
mkisofs, err := exec.LookPath("genisoimage")
|
||||
|
||||
@ -2,11 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/utf16"
|
||||
)
|
||||
|
||||
func rmTempFile(f *os.File) {
|
||||
@ -21,7 +22,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
||||
defer arch.Close()
|
||||
|
||||
archAdd := func(path string, ba []byte) (err error) {
|
||||
err = arch.WriteHeader(&tar.Header{Name: path, Size: int64(len(ba))})
|
||||
err = arch.WriteHeader(&tar.Header{Name: path, Mode: 0640, Size: int64(len(ba))})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -29,70 +30,95 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// config
|
||||
cfgBytes, cfg, err := ctx.Config()
|
||||
// kernel
|
||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
archAdd("config.yaml", cfgBytes)
|
||||
|
||||
// add "current" elements
|
||||
type distCopy struct {
|
||||
Src []string
|
||||
Dst string
|
||||
kernelBytes, err := os.ReadFile(kernelPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// kernel and initrd
|
||||
copies := []distCopy{
|
||||
{Src: []string{"kernels", ctx.Host.Kernel}, Dst: "current/vmlinuz"},
|
||||
{Src: []string{"initrd", ctx.Host.Initrd}, Dst: "current/initrd"},
|
||||
err = archAdd("current/vmlinuz", kernelBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 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"),
|
||||
})
|
||||
// initrd
|
||||
initrd := new(bytes.Buffer)
|
||||
err = buildInitrd(initrd, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, copy := range copies {
|
||||
outPath, err := ctx.distFetch(copy.Src...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Open(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = arch.WriteHeader(&tar.Header{
|
||||
Name: copy.Dst,
|
||||
Size: stat.Size(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(arch, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = archAdd("current/initrd", initrd.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// done
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
|
||||
arch := tar.NewWriter(out)
|
||||
defer arch.Close()
|
||||
|
||||
archAdd := func(path string, ba []byte) (err error) {
|
||||
err = arch.WriteHeader(&tar.Header{Name: path, Mode: 0640, Size: int64(len(ba))})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = arch.Write(ba)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
prefix = "EFI/dkl/"
|
||||
efiPrefix = "\\EFI\\dkl\\"
|
||||
)
|
||||
|
||||
// boot.csv
|
||||
// -> annoyingly it's UTF-16...
|
||||
bootCsvBytes := utf16.FromUTF8([]byte("" +
|
||||
"current_kernel.efi,dkl current,initrd=" + efiPrefix + "current_initrd.img,Direktil current\n" +
|
||||
"previous_kernel.efi,dkl previous,initrd=" + efiPrefix + "previous_initrd.img,Direktil previous\n"))
|
||||
|
||||
err = archAdd(prefix+"BOOT.CSV", []byte(bootCsvBytes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// kernel
|
||||
kernelPath, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kernelBytes, err := os.ReadFile(kernelPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = archAdd(prefix+"current_kernel.efi", kernelBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// initrd
|
||||
initrd := new(bytes.Buffer)
|
||||
err = buildInitrd(initrd, ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = archAdd(prefix+"current_initrd.img", initrd.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// done
|
||||
return nil
|
||||
}
|
||||
|
||||
225
cmd/dkl-local-server/bootv2.go
Normal file
225
cmd/dkl-local-server/bootv2.go
Normal file
@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.tech/direktil/pkg/cpiocat"
|
||||
)
|
||||
|
||||
func renderBootstrapConfig(w http.ResponseWriter, r *http.Request, ctx *renderContext, asJson bool) (err error) {
|
||||
log.Printf("sending bootstrap config for %q", ctx.Host.Name)
|
||||
|
||||
_, cfg, err := ctx.BootstrapConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if asJson {
|
||||
err = json.NewEncoder(w).Encode(cfg)
|
||||
} else {
|
||||
err = yaml.NewEncoder(w).Encode(cfg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildInitrd(out io.Writer, ctx *renderContext) (err error) {
|
||||
_, cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 := distFetch("initrd", ctx.Host.Initrd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cat.AppendArchFile(initrdPath)
|
||||
|
||||
// embedded layers (modules)
|
||||
for _, layer := range cfg.Layers {
|
||||
switch layer {
|
||||
case "modules":
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
if layerVersion == "" {
|
||||
layerVersion = ctx.Host.Kernel
|
||||
}
|
||||
modulesPath, err := distFetch("layers", layer, layerVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cat.AppendFile(modulesPath, "modules.sqfs")
|
||||
}
|
||||
}
|
||||
|
||||
// config
|
||||
cfgBytes, _, err := ctx.BootstrapConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cat.AppendBytes(cfgBytes, "config.yaml", 0o600)
|
||||
|
||||
// ssh keys
|
||||
cat.AppendDir("/etc", 0o755)
|
||||
cat.AppendDir("/etc/ssh", 0o700)
|
||||
|
||||
// XXX do we want bootstrap-stage keys instead of the real host key?
|
||||
for _, format := range []string{"rsa", "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)),
|
||||
Mode: 0o600,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = arch.Write(cfgBytes)
|
||||
if err != nil {
|
||||
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 in the initrd with boot v2
|
||||
}
|
||||
|
||||
layerVersion := ctx.Host.Versions[layer]
|
||||
if layerVersion == "" {
|
||||
return fmt.Errorf("layer %q not mapped to a version", layer)
|
||||
}
|
||||
|
||||
outPath, err := distFetch("layers", layer, layerVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Open(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
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, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
digest := h.Sum(nil)
|
||||
err = sign(layer+".fs.sig", digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,75 +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 {
|
||||
err := cleanCAS()
|
||||
if err != nil {
|
||||
log.Print("warn: couldn't clean cache: ", err)
|
||||
}
|
||||
|
||||
time.Sleep(*cacheCleanDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanCAS() error {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
191
cmd/dkl-local-server/cluster-render-context.go
Normal file
191
cmd/dkl-local-server/cluster-render-context.go
Normal file
@ -0,0 +1,191 @@
|
||||
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(),
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(reqJson), certReq)
|
||||
if err != nil {
|
||||
log.Print("CSR unmarshal failed on: ", reqJson)
|
||||
return
|
||||
}
|
||||
|
||||
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, 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{
|
||||
"quote": strconv.Quote,
|
||||
"yaml": asYaml,
|
||||
"indent": func(s, indent string) string {
|
||||
buf := new(strings.Builder)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
buf.WriteString(indent + line + "\n")
|
||||
}
|
||||
return buf.String()
|
||||
},
|
||||
|
||||
"password": func(cluster, name, hashAlg string) (password string, err error) {
|
||||
key := cluster + "/" + name
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return hash([]byte(password), seed, hashAlg)
|
||||
},
|
||||
|
||||
"token": getOrCreateClusterToken,
|
||||
|
||||
"ca_key": func(cluster, name string) (s string, err error) {
|
||||
ca, err := getUsableClusterCA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(ca.Key)
|
||||
return
|
||||
},
|
||||
|
||||
"ca_crt": func(cluster, name string) (s string, err error) {
|
||||
ca, err := getUsableClusterCA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(ca.Cert)
|
||||
return
|
||||
},
|
||||
|
||||
"ca_dir": func(cluster, name string) (s string, err error) {
|
||||
ca, err := getUsableClusterCA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dir := "/etc/tls-ca/" + name
|
||||
|
||||
return asYaml([]config.FileDef{
|
||||
{
|
||||
Path: path.Join(dir, "ca.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(ca.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "ca.key"),
|
||||
Mode: 0600,
|
||||
Content: string(ca.Key),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
"tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(kc.Key)
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(kc.Cert)
|
||||
return
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func asYaml(v any) (string, error) {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(ba), nil
|
||||
}
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"path/filepath"
|
||||
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
3
cmd/dkl-local-server/host-download-tokens.go
Normal file
3
cmd/dkl-local-server/host-download-tokens.go
Normal file
@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var hostDownloadTokens = KVSecrets[string]{"hosts/download-tokens"}
|
||||
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>`
|
||||
16
cmd/dkl-local-server/httperr.go
Normal file
16
cmd/dkl-local-server/httperr.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"m.cluseau.fr/go/httperr"
|
||||
)
|
||||
|
||||
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/cavaliercoder/go-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: 0600 | cpio.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
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@ -15,3 +17,19 @@ func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) er
|
||||
http.ServeFile(w, r, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchKernel(out io.Writer, ctx *renderContext) (err error) {
|
||||
path, err := distFetch("kernels", ctx.Host.Kernel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
return
|
||||
}
|
||||
|
||||
@ -4,44 +4,79 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"os"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/mcluseau/go-swagger-ui"
|
||||
"novit.nc/direktil/pkg/cas"
|
||||
swaggerui "github.com/mcluseau/go-swagger-ui"
|
||||
"m.cluseau.fr/go/watchable/streamsse"
|
||||
|
||||
"novit.nc/direktil/local-server/pkg/apiutils"
|
||||
dlshtml "novit.tech/direktil/local-server/html"
|
||||
"novit.tech/direktil/local-server/pkg/apiutils"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
casStore cas.Store
|
||||
autoUnlock = flag.String("auto-unlock", "", "Auto-unlock store (testing only!) env: DLS_AUTO_UNLOCK")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *address == "" && *tlsAddress == "" {
|
||||
log.Fatal("no listen address given")
|
||||
}
|
||||
|
||||
casStore = cas.NewDir(filepath.Join(*dataDir, "cache"))
|
||||
go casCleaner()
|
||||
log.Print("Direktil local-server version ", Version)
|
||||
wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
|
||||
|
||||
computeUIHash()
|
||||
|
||||
openSecretStore()
|
||||
|
||||
{
|
||||
autoUnlock := *autoUnlock
|
||||
if autoUnlock == "" {
|
||||
autoUnlock = os.Getenv("DLS_AUTO_UNLOCK")
|
||||
}
|
||||
if autoUnlock != "" {
|
||||
log.Printf("auto-unlocking the store")
|
||||
err := unlockSecretStore("test", []byte(autoUnlock))
|
||||
if err.Any() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Print("store auto-unlocked")
|
||||
}
|
||||
|
||||
os.Setenv("DLS_AUTO_UNLOCK", "")
|
||||
}
|
||||
|
||||
apiutils.Setup(func() {
|
||||
restful.Add(buildWS())
|
||||
registerWS(restful.DefaultContainer)
|
||||
})
|
||||
|
||||
swaggerui.HandleAt("/swagger-ui/")
|
||||
|
||||
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)))
|
||||
|
||||
if *address != "" {
|
||||
log.Print("HTTP listening on ", *address)
|
||||
go log.Fatal(http.ListenAndServe(*address, nil))
|
||||
|
||||
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,86 +2,137 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"novit.nc/direktil/pkg/config"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
"novit.tech/direktil/pkg/config"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
|
||||
bsconfig "novit.tech/direktil/pkg/bootstrapconfig"
|
||||
)
|
||||
|
||||
var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition")
|
||||
|
||||
var b64 = base64.StdEncoding.WithPadding(base64.NoPadding)
|
||||
|
||||
type renderContext struct {
|
||||
Host *localconfig.Host
|
||||
SSLConfig string
|
||||
SSLConfig *cfsslconfig.Config
|
||||
|
||||
// Linux kernel extra cmdline
|
||||
CmdLine string `yaml:"-"`
|
||||
}
|
||||
|
||||
func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string,
|
||||
create func(out io.Writer, ctx *renderContext) error) error {
|
||||
log.Printf("sending %s for %q", what, ctx.Host.Name)
|
||||
create func(out io.Writer, ctx *renderContext) error) (err error) {
|
||||
|
||||
tag, err := ctx.Tag()
|
||||
if err != nil {
|
||||
return err
|
||||
ctx.CmdLine = r.URL.Query().Get(cmdlineParam.Data().Name)
|
||||
|
||||
if ctx.CmdLine != "" {
|
||||
what = what + "?cmdline=" + url.QueryEscape(ctx.CmdLine)
|
||||
}
|
||||
|
||||
// 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
|
||||
http.ServeContent(w, r, what, meta.ModTime(), content)
|
||||
return nil
|
||||
log.Printf("sending %s for %q", what, ctx.Host.Name)
|
||||
|
||||
outfile.Seek(0, io.SeekStart)
|
||||
io.Copy(w, outfile)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var prevSSLConfig = "-"
|
||||
|
||||
func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
|
||||
if prevSSLConfig != cfg.SSLConfig {
|
||||
var sslCfg *cfsslconfig.Config
|
||||
|
||||
if len(cfg.SSLConfig) == 0 {
|
||||
sslCfg = &cfsslconfig.Config{}
|
||||
} else {
|
||||
sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = loadSecretData(sslCfg)
|
||||
func sslConfigFromLocalConfig(cfg *localconfig.Config) (sslCfg *cfsslconfig.Config, err error) {
|
||||
if len(cfg.SSLConfig) == 0 {
|
||||
sslCfg = &cfsslconfig.Config{}
|
||||
} else {
|
||||
sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
prevSSLConfig = cfg.SSLConfig
|
||||
func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
|
||||
sslCfg, err := sslConfigFromLocalConfig(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return &renderContext{
|
||||
SSLConfig: cfg.SSLConfig,
|
||||
Host: host,
|
||||
SSLConfig: sslCfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
|
||||
ba, err = ctx.render(ctx.Host.Config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// log.Print("rendered config:\n", string(ba))
|
||||
|
||||
cfg = &config.Config{}
|
||||
if err = yaml.Unmarshal(ba, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *renderContext) BootstrapConfig() (ba []byte, cfg *bsconfig.Config, err error) {
|
||||
ba, err = ctx.render(ctx.Host.BootstrapConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg = &bsconfig.Config{}
|
||||
if err = yaml.Unmarshal(ba, cfg); err != nil {
|
||||
log.Print("invalid bootstrap config yaml:\n", string(ba))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
|
||||
tmpl, err := template.New(ctx.Host.Name + "/config").
|
||||
Funcs(ctx.templateFuncs()).
|
||||
Parse(ctx.Host.Config)
|
||||
Funcs(ctx.TemplateFuncs()).
|
||||
Parse(templateText)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
@ -92,28 +143,40 @@ func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if secretData.Changed() {
|
||||
err = secretData.Save()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ba = buf.Bytes()
|
||||
|
||||
cfg = &config.Config{}
|
||||
|
||||
if err = yaml.Unmarshal(buf.Bytes(), cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx *renderContext) templateFuncs() map[string]interface{} {
|
||||
getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) {
|
||||
func distFilePath(path ...string) string {
|
||||
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
||||
}
|
||||
|
||||
func (ctx *renderContext) Tag() (string, error) {
|
||||
h := sha256.New()
|
||||
|
||||
_, cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
enc := yaml.NewEncoder(h)
|
||||
|
||||
for _, o := range []any{cfg, ctx} {
|
||||
if err := enc.Encode(o); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
funcs := templateFuncs(ctx.SSLConfig)
|
||||
|
||||
// FIXME duplicate from cluster-render-context
|
||||
getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc KeyCert, err error) {
|
||||
certReq := &csr.CertificateRequest{
|
||||
KeyRequest: csr.NewBasicKeyRequest(),
|
||||
KeyRequest: csr.NewKeyRequest(),
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(reqJson), certReq)
|
||||
@ -122,91 +185,34 @@ func (ctx *renderContext) templateFuncs() map[string]interface{} {
|
||||
return
|
||||
}
|
||||
|
||||
return secretData.KeyCert(cluster, caName, name, profile, label, certReq)
|
||||
return getUsableKeyCert(cluster, caName, name, profile, label, certReq, ctx.SSLConfig)
|
||||
}
|
||||
|
||||
asYaml := func(v interface{}) (string, error) {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(ba), nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"token": func(cluster, name string) (s string, err error) {
|
||||
return secretData.Token(cluster, name)
|
||||
for name, method := range map[string]any{
|
||||
"base64": func(input string) string {
|
||||
return b64.EncodeToString([]byte(input))
|
||||
},
|
||||
|
||||
"ca_key": func(cluster, name string) (s string, err error) {
|
||||
ca, err := secretData.CA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(ca.Key)
|
||||
return
|
||||
"host_ip": func() (s string) {
|
||||
return ctx.Host.IPs[0]
|
||||
},
|
||||
|
||||
"ca_crt": func(cluster, name string) (s string, err error) {
|
||||
ca, err := secretData.CA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(ca.Cert)
|
||||
return
|
||||
"host_name": func() (s string) {
|
||||
return ctx.Host.Name
|
||||
},
|
||||
|
||||
"ca_dir": func(cluster, name string) (s string, err error) {
|
||||
ca, err := secretData.CA(cluster, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dir := "/etc/tls-ca/" + name
|
||||
|
||||
return asYaml([]config.FileDef{
|
||||
{
|
||||
Path: path.Join(dir, "ca.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(ca.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "ca.key"),
|
||||
Mode: 0600,
|
||||
Content: string(ca.Key),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
"tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(kc.Key)
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
s = string(kc.Cert)
|
||||
return
|
||||
"machine_id": func() (s string) {
|
||||
ba := sha1.Sum([]byte(ctx.Host.ClusterName + "/" + ctx.Host.Name))
|
||||
return hex.EncodeToString(ba[:])
|
||||
},
|
||||
|
||||
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
ca, err := secretData.CA(cluster, caName)
|
||||
ca, err := getUsableClusterCA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reqJson = strings.ReplaceAll(reqJson, "${host_ip}", ctx.Host.IPs[0])
|
||||
reqJson = strings.ReplaceAll(reqJson, "${host_name}", ctx.Host.Name)
|
||||
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
return
|
||||
@ -230,43 +236,147 @@ func (ctx *renderContext) templateFuncs() map[string]interface{} {
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
"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_user_ca_pub": func(cluster string) (s string, err error) {
|
||||
userCA, err := sshCAPubKey(cluster)
|
||||
return string(userCA), err
|
||||
},
|
||||
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||
if host == "" {
|
||||
host = ctx.Host.Name
|
||||
}
|
||||
if host != ctx.Host.Name {
|
||||
err = fmt.Errorf("wrong host name")
|
||||
return
|
||||
}
|
||||
|
||||
pairs, err := getSSHKeyPairs(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
files := make([]config.FileDef, 0, len(pairs)*2)
|
||||
|
||||
for _, pair := range pairs {
|
||||
basePath := path.Join(dir, "ssh_host_"+pair.Type+"_key")
|
||||
files = append(files, []config.FileDef{
|
||||
{
|
||||
Path: basePath,
|
||||
Mode: 0600,
|
||||
Content: pair.Private,
|
||||
},
|
||||
{
|
||||
Path: basePath + ".pub",
|
||||
Mode: 0644,
|
||||
Content: pair.Public,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return asYaml(files)
|
||||
},
|
||||
"ssh_host_key": func(type_ string) (s string, err error) {
|
||||
pair, err := ctx.sshHostKeyPair(type_)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return pair.Private, nil
|
||||
},
|
||||
"ssh_host_pubkey": func(type_ string) (s string, err error) {
|
||||
pair, err := ctx.sshHostKeyPair(type_)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return pair.Public, nil
|
||||
},
|
||||
"host_download_token": func() (token string, err error) {
|
||||
key := ctx.Host.Name
|
||||
token, found, err := hostDownloadTokens.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
token, err = newToken(32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = hostDownloadTokens.Put(key, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
"asset_download_token": func(asset string, params ...string) (token string, err error) {
|
||||
now := time.Now()
|
||||
exp := now.Add(24 * time.Hour) // expire in 24h by default
|
||||
if len(params) != 0 {
|
||||
exp, err = parseCertDuration(params[0], now)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
set := DownloadSet{
|
||||
Expiry: exp,
|
||||
Items: []DownloadSetItem{
|
||||
{
|
||||
Kind: "host",
|
||||
Name: ctx.Host.Name,
|
||||
Assets: []string{asset},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
privKey, _ := dlsSigningKeys()
|
||||
token = set.Signed(privKey)
|
||||
|
||||
return
|
||||
},
|
||||
"wg_key": func(name string) (key string, err error) {
|
||||
return wgKey(name + "/hosts/" + ctx.Host.Name)
|
||||
},
|
||||
"wg_psk": func(name, peerName string) (key string, err error) {
|
||||
a := ctx.Host.Name
|
||||
b := peerName
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
return wgKey(name + "/psks/" + a + " " + b)
|
||||
},
|
||||
"wg_pubkey": func(name, host string) (key string, err error) {
|
||||
return wgKey(name + "/hosts/" + host)
|
||||
},
|
||||
} {
|
||||
funcs[name] = method
|
||||
}
|
||||
|
||||
return funcs
|
||||
}
|
||||
|
||||
func (ctx *renderContext) distFilePath(path ...string) string {
|
||||
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
||||
}
|
||||
|
||||
func (ctx *renderContext) Tag() (string, error) {
|
||||
h := sha256.New()
|
||||
|
||||
_, cfg, err := ctx.Config()
|
||||
func (ctx *renderContext) sshHostKeyPair(type_ string) (kp SSHKeyPair, err error) {
|
||||
pairs, err := getSSHKeyPairs(ctx.Host.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return
|
||||
}
|
||||
|
||||
enc := yaml.NewEncoder(h)
|
||||
|
||||
for _, o := range []interface{}{cfg, ctx} {
|
||||
if err := enc.Encode(o); err != nil {
|
||||
return "", err
|
||||
for _, pair := range pairs {
|
||||
if pair.Type == type_ {
|
||||
return pair, nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
err = fmt.Errorf("no key pair with type %q", type_)
|
||||
return
|
||||
}
|
||||
|
||||
397
cmd/dkl-local-server/secret-store.go
Normal file
397
cmd/dkl-local-server/secret-store.go
Normal file
@ -0,0 +1,397 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
|
||||
"novit.tech/direktil/local-server/secretstore"
|
||||
)
|
||||
|
||||
var secStore *secretstore.Store
|
||||
|
||||
func secStoreRoot() string { return filepath.Join(*dataDir, "secrets") }
|
||||
func secStorePath(name string) string { return filepath.Join(secStoreRoot(), name) }
|
||||
func secKeysStorePath() string { return secStorePath(".keys") }
|
||||
|
||||
func openSecretStore() {
|
||||
var err error
|
||||
|
||||
keysPath := secKeysStorePath()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Dir(keysPath)), 0755); err != nil {
|
||||
log.Fatal("failed to create dirs: ", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(keysPath), 0700); err != nil {
|
||||
log.Fatal("failed to secret store dir: ", err)
|
||||
}
|
||||
|
||||
secStore, err = secretstore.Open(keysPath)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
wPublicState.Change(func(v *PublicState) {
|
||||
v.Store.New = false
|
||||
v.Store.Open = false
|
||||
})
|
||||
|
||||
case os.IsNotExist(err):
|
||||
secStore = secretstore.New()
|
||||
wPublicState.Change(func(v *PublicState) {
|
||||
v.Store.New = true
|
||||
v.Store.Open = false
|
||||
})
|
||||
|
||||
default:
|
||||
log.Fatal("failed to open keys store: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
unlockMutex = sync.Mutex{}
|
||||
|
||||
ErrStoreAlreadyUnlocked = httperr.NewStd(1, http.StatusConflict, "store already unlocked")
|
||||
ErrInvalidPassphrase = httperr.NewStd(2, http.StatusBadRequest, "invalid passphrase")
|
||||
)
|
||||
|
||||
func unlockSecretStore(name string, passphrase []byte) (err httperr.Error) {
|
||||
unlockMutex.Lock()
|
||||
defer unlockMutex.Unlock()
|
||||
|
||||
if secStore.Unlocked() {
|
||||
return ErrStoreAlreadyUnlocked
|
||||
}
|
||||
|
||||
if secStore.IsNew() {
|
||||
err := secStore.Init(name, passphrase)
|
||||
if err != nil {
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
err = secStore.SaveTo(secKeysStorePath())
|
||||
if err != nil {
|
||||
log.Print("secret store save error: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !secStore.Unlock([]byte(passphrase)) {
|
||||
return ErrInvalidPassphrase
|
||||
}
|
||||
}
|
||||
|
||||
token := ""
|
||||
if err := readSecret("admin-token", &token); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Print("failed to read admin token: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
token, err = newToken(32)
|
||||
if err != nil {
|
||||
secStore.Close()
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
err = writeSecret("admin-token", token)
|
||||
if err != nil {
|
||||
log.Print("write error: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
log.Print("wrote new admin token")
|
||||
}
|
||||
|
||||
adminToken = token
|
||||
|
||||
{
|
||||
token, err := newToken(16)
|
||||
if err != nil {
|
||||
secStore.Close()
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
wState.Change(func(v *State) {
|
||||
v.Store.DownloadToken = token
|
||||
})
|
||||
}
|
||||
|
||||
wPublicState.Change(func(v *PublicState) {
|
||||
v.Store.New = false
|
||||
v.Store.Open = true
|
||||
})
|
||||
|
||||
go updateState()
|
||||
go migrateSecrets()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func readSecret(name string, value any) (err error) {
|
||||
f, err := os.Open(secStorePath(name + ".data"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
in, err := secStore.NewReader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return json.NewDecoder(in).Decode(value)
|
||||
}
|
||||
|
||||
func writeSecret(name string, value any) (err error) {
|
||||
path := secStorePath(name + ".data.new")
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
defer f.Close()
|
||||
|
||||
out, err := secStore.NewWriter(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return json.NewEncoder(out).Encode(value)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(f.Name(), secStorePath(name+".data"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go updateState()
|
||||
return
|
||||
}
|
||||
|
||||
var secL sync.Mutex
|
||||
|
||||
func updateSecret[T any](name string, update func(*T)) (err error) {
|
||||
secL.Lock()
|
||||
defer secL.Unlock()
|
||||
|
||||
v := new(T)
|
||||
err = readSecret(name, v)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
|
||||
update(v)
|
||||
|
||||
return writeSecret(name, *v)
|
||||
}
|
||||
|
||||
func updateSecretWithKey[T any](name, key string, update func(v *T)) (err error) {
|
||||
secL.Lock()
|
||||
defer secL.Unlock()
|
||||
|
||||
kvs := map[string]*T{}
|
||||
|
||||
err = readSecret(name, &kvs)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
|
||||
update(kvs[key])
|
||||
|
||||
return writeSecret(name, kvs)
|
||||
}
|
||||
|
||||
type KVSecrets[T any] struct{ Name string }
|
||||
|
||||
func (s KVSecrets[T]) Data() (kvs map[string]T, err error) {
|
||||
kvs = make(map[string]T)
|
||||
err = readSecret(s.Name, &kvs)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
keys = make([]string, 0, len(kvs))
|
||||
|
||||
for k := range kvs {
|
||||
if !strings.HasPrefix(k, prefix) {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k[len(prefix):])
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) Get(key string) (v T, found bool, err error) {
|
||||
kvs, err := s.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v, found = kvs[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) Put(key string, v T) (err error) {
|
||||
secL.Lock()
|
||||
defer secL.Unlock()
|
||||
|
||||
kvs, err := s.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kvs[key] = v
|
||||
err = writeSecret(s.Name, kvs)
|
||||
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 {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(keys)
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) WsGet(resp *restful.Response, key string) {
|
||||
keys, found, err := s.Get(key)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(keys)
|
||||
}
|
||||
|
||||
func (s KVSecrets[T]) WsPut(req *restful.Request, resp *restful.Response, key string) {
|
||||
v := new(T)
|
||||
err := req.ReadEntity(v)
|
||||
if err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Put(key, *v)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
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
|
||||
}
|
||||
}
|
||||
88
cmd/dkl-local-server/secrets-migrate.go
Normal file
88
cmd/dkl-local-server/secrets-migrate.go
Normal file
@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
)
|
||||
|
||||
func migrateSecrets() {
|
||||
if _, err := os.Stat(secretDataPath()); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("not migrating old secrets: ", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("migrating old secrets")
|
||||
|
||||
log := log.New(log.Default().Writer(), "secrets migration: ", log.Flags()|log.Lmsgprefix)
|
||||
|
||||
// load secrets
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var sslCfg *cfsslconfig.Config
|
||||
|
||||
if len(cfg.SSLConfig) == 0 {
|
||||
sslCfg = &cfsslconfig.Config{}
|
||||
} else {
|
||||
sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
secretData, err := loadSecretData(sslCfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
for clusterName, cluster := range secretData.clusters {
|
||||
for k, v := range cluster.Tokens {
|
||||
err = clusterTokens.Put(clusterName+"/"+k, v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range cluster.Passwords {
|
||||
err = clusterPasswords.Put(clusterName+"/"+k, v)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for caName, ca := range cluster.CAs {
|
||||
clusterCAs.Put(clusterName+"/"+caName, CA{Key: ca.Key, Cert: ca.Cert})
|
||||
|
||||
for signedName, signed := range ca.Signed {
|
||||
err = clusterCASignedKeys.Put(clusterName+"/"+caName+"/"+signedName, *signed)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for hostName, pairs := range cluster.SSHKeyPairs {
|
||||
err = sshHostKeys.Put(hostName, pairs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(secretDataPath(), secretDataPath()+".migrated"); err != nil {
|
||||
log.Fatal("failed to rename migrated secrets: ", err)
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/certinfo"
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/initca"
|
||||
"github.com/cloudflare/cfssl/helpers/derhelpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
"github.com/cloudflare/cfssl/signer/local"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
var (
|
||||
secretData *SecretData
|
||||
)
|
||||
|
||||
type SecretData struct {
|
||||
l sync.Mutex
|
||||
|
||||
clusters map[string]*ClusterSecrets
|
||||
changed bool
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
type ClusterSecrets struct {
|
||||
CAs map[string]*CA
|
||||
Tokens map[string]string
|
||||
}
|
||||
|
||||
type CA struct {
|
||||
Key []byte
|
||||
Cert []byte
|
||||
|
||||
Signed map[string]*KeyCert
|
||||
CAs map[string]*CA
|
||||
Tokens map[string]string
|
||||
Passwords map[string]string
|
||||
SSHKeyPairs map[string][]SSHKeyPair
|
||||
}
|
||||
|
||||
type KeyCert struct {
|
||||
@ -57,21 +36,18 @@ func secretDataPath() string {
|
||||
return filepath.Join(*dataDir, "secret-data.json")
|
||||
}
|
||||
|
||||
func loadSecretData(config *config.Config) (err error) {
|
||||
func loadSecretData(config *config.Config) (sd *SecretData, err error) {
|
||||
log.Info("Loading secret data")
|
||||
|
||||
sd := &SecretData{
|
||||
sd = &SecretData{
|
||||
clusters: make(map[string]*ClusterSecrets),
|
||||
changed: false,
|
||||
config: config,
|
||||
}
|
||||
|
||||
ba, err := ioutil.ReadFile(secretDataPath())
|
||||
ba, err := os.ReadFile(secretDataPath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
sd.changed = true
|
||||
err = nil
|
||||
secretData = sd
|
||||
return
|
||||
}
|
||||
return
|
||||
@ -81,207 +57,51 @@ func loadSecretData(config *config.Config) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
secretData = sd
|
||||
return
|
||||
}
|
||||
|
||||
func (sd *SecretData) Changed() bool {
|
||||
return sd.changed
|
||||
}
|
||||
|
||||
func (sd *SecretData) Save() error {
|
||||
sd.l.Lock()
|
||||
defer sd.l.Unlock()
|
||||
|
||||
log.Info("Saving secret data")
|
||||
ba, err := json.Marshal(sd.clusters)
|
||||
func checkCertUsable(certPEM []byte) error {
|
||||
cert, err := certinfo.ParseCertificatePEM(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(secretDataPath(), ba, 0600)
|
||||
|
||||
certDuration := cert.NotAfter.Sub(cert.NotBefore)
|
||||
delayBeforeRegen := certDuration / 3 // TODO allow configuration
|
||||
|
||||
if cert.NotAfter.Sub(time.Now()) < delayBeforeRegen {
|
||||
return errors.New("too old")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sd *SecretData) cluster(name string) (cs *ClusterSecrets) {
|
||||
cs, ok := sd.clusters[name]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
func dlsSigningKeys() (ed25519.PrivateKey, ed25519.PublicKey) {
|
||||
var signerDER []byte
|
||||
|
||||
sd.l.Lock()
|
||||
defer sd.l.Unlock()
|
||||
|
||||
log.Info("secret-data: new cluster: ", name)
|
||||
|
||||
cs = &ClusterSecrets{
|
||||
CAs: make(map[string]*CA),
|
||||
Tokens: make(map[string]string),
|
||||
}
|
||||
sd.clusters[name] = cs
|
||||
sd.changed = true
|
||||
return
|
||||
}
|
||||
|
||||
func (sd *SecretData) Token(cluster, name string) (token string, err error) {
|
||||
cs := sd.cluster(cluster)
|
||||
|
||||
token = cs.Tokens[name]
|
||||
if token != "" {
|
||||
return
|
||||
}
|
||||
|
||||
sd.l.Lock()
|
||||
defer sd.l.Unlock()
|
||||
|
||||
log.Info("secret-data: new token in cluster ", cluster, ": ", name)
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err = rand.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)
|
||||
|
||||
cs.Tokens[name] = token
|
||||
sd.changed = true
|
||||
return
|
||||
}
|
||||
|
||||
func (sd *SecretData) CA(cluster, name string) (ca *CA, err error) {
|
||||
cs := sd.cluster(cluster)
|
||||
|
||||
ca, ok := cs.CAs[name]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
sd.l.Lock()
|
||||
defer sd.l.Unlock()
|
||||
|
||||
log.Info("secret-data: new CA in cluster ", cluster, ": ", name)
|
||||
|
||||
req := &csr.CertificateRequest{
|
||||
CN: "Direktil Local Server",
|
||||
KeyRequest: &csr.BasicKeyRequest{
|
||||
A: "ecdsa",
|
||||
S: 521, // 256, 384, 521
|
||||
},
|
||||
Names: []csr.Name{
|
||||
{
|
||||
C: "NC",
|
||||
O: "novit.nc",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cert, _, key, err := initca.New(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ca = &CA{
|
||||
Key: key,
|
||||
Cert: cert,
|
||||
Signed: make(map[string]*KeyCert),
|
||||
}
|
||||
|
||||
cs.CAs[name] = ca
|
||||
sd.changed = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sd *SecretData) KeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest) (kc *KeyCert, err error) {
|
||||
for idx, host := range req.Hosts {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
// valid IP (v4 or v6)
|
||||
continue
|
||||
if err := readSecret("signer", &signerDER); os.IsNotExist(err) {
|
||||
_, key, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if host == "*" {
|
||||
continue
|
||||
signerDER, err = derhelpers.MarshalEd25519PrivateKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
|
||||
continue
|
||||
}
|
||||
if errs := validation.IsWildcardDNS1123Subdomain(host); len(errs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
path := field.NewPath(cluster, name, "hosts").Index(idx)
|
||||
return nil, fmt.Errorf("%v: %q is not an IP or FQDN", path, host)
|
||||
writeSecret("signer", signerDER)
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if req.CA != nil {
|
||||
err = errors.New("no CA section allowed here")
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := sd.CA(cluster, caName)
|
||||
pkeyGeneric, err := derhelpers.ParseEd25519PrivateKey(signerDER)
|
||||
if err != nil {
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rh := hash(req)
|
||||
kc, ok := ca.Signed[name]
|
||||
if ok && rh == kc.ReqHash {
|
||||
return
|
||||
} else if ok {
|
||||
log.Infof("secret-data: cluster %s: CA %s: CSR changed for %s: hash=%q previous=%q",
|
||||
cluster, caName, name, rh, kc.ReqHash)
|
||||
} else {
|
||||
log.Infof("secret-data: cluster %s: CA %s: new CSR for %s", cluster, caName, name)
|
||||
}
|
||||
pkey := pkeyGeneric.(ed25519.PrivateKey)
|
||||
pubkey := pkey.Public().(ed25519.PublicKey)
|
||||
|
||||
sd.l.Lock()
|
||||
defer sd.l.Unlock()
|
||||
|
||||
sgr, err := ca.Signer(sd.config.Signing)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
generator := &csr.Generator{Validator: func(_ *csr.CertificateRequest) error { return nil }}
|
||||
|
||||
csr, key, err := generator.ProcessRequest(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
signReq := signer.SignRequest{
|
||||
Request: string(csr),
|
||||
Profile: profile,
|
||||
Label: label,
|
||||
}
|
||||
|
||||
cert, err := sgr.Sign(signReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kc = &KeyCert{
|
||||
Key: key,
|
||||
Cert: cert,
|
||||
ReqHash: rh,
|
||||
}
|
||||
|
||||
ca.Signed[name] = kc
|
||||
sd.changed = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ca *CA) Signer(policy *config.Signing) (result *local.Signer, err error) {
|
||||
caCert, err := helpers.ParseCertificatePEM(ca.Cert)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
caKey, err := helpers.ParsePrivateKeyPEM(ca.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return local.NewSigner(caKey, caCert, signer.DefaultSigAlgo(caKey), policy)
|
||||
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)
|
||||
}
|
||||
207
cmd/dkl-local-server/ssh-secrets.go
Normal file
207
cmd/dkl-local-server/ssh-secrets.go
Normal file
@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var sshHostKeys = KVSecrets[[]SSHKeyPair]{"hosts/ssh-host-keys"}
|
||||
|
||||
type SSHKeyPair struct {
|
||||
Type string
|
||||
Public string
|
||||
Private string
|
||||
}
|
||||
|
||||
func getSSHKeyPairs(host string) (pairs []SSHKeyPair, err error) {
|
||||
pairs, _, err = sshHostKeys.Get(host)
|
||||
|
||||
didGenerate := false
|
||||
|
||||
genLoop:
|
||||
for _, keyType := range []string{
|
||||
"rsa",
|
||||
"ecdsa",
|
||||
"ed25519",
|
||||
} {
|
||||
for _, pair := range pairs {
|
||||
if pair.Type == keyType {
|
||||
continue genLoop
|
||||
}
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
outFile, err := os.CreateTemp("/tmp", "dls-key.")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
outPath := outFile.Name()
|
||||
|
||||
removeTemp := func() {
|
||||
os.Remove(outPath)
|
||||
os.Remove(outPath + ".pub")
|
||||
}
|
||||
|
||||
removeTemp()
|
||||
defer removeTemp()
|
||||
|
||||
var out, privKey, pubKey []byte
|
||||
|
||||
cmd := exec.Command("ssh-keygen",
|
||||
"-N", "",
|
||||
"-C", "root@"+host,
|
||||
"-f", outPath,
|
||||
"-t", keyType)
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssh-keygen failed: %v: %s", err, string(out))
|
||||
return
|
||||
}
|
||||
|
||||
privKey, err = os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKey, err = os.ReadFile(outPath + ".pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pairs = append(pairs, SSHKeyPair{
|
||||
Type: keyType,
|
||||
Public: string(pubKey),
|
||||
Private: string(privKey),
|
||||
})
|
||||
didGenerate = true
|
||||
|
||||
return
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if didGenerate {
|
||||
err = sshHostKeys.Put(host, pairs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var sshCAKeys = KVSecrets[string]{"ssh-ca-keys"}
|
||||
|
||||
func sshCAKey(cluster string) (caKeyPem string, err error) {
|
||||
storeKey := "clusters/" + cluster
|
||||
caKeyPem, _, err = sshCAKeys.Get(storeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if caKeyPem == "" {
|
||||
_, pk, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pemBlock, err := ssh.MarshalPrivateKey(crypto.PrivateKey(pk), "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pubkey, identity, _, _, err := ssh.ParseAuthorizedKey(bytes.TrimSpace(userPubKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ak := ssh.MarshalAuthorizedKey(pubkey)
|
||||
|
||||
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(userPubKeyFile.Name())
|
||||
|
||||
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(ak))
|
||||
userPubKeyFile.Close()
|
||||
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
|
||||
}
|
||||
10
cmd/dkl-local-server/ssh-secrets_test.go
Normal file
10
cmd/dkl-local-server/ssh-secrets_test.go
Normal file
@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSSHKeyGet(t *testing.T) {
|
||||
// TODO needs fake secret store
|
||||
// if _, err := getSSHKeyPairs("host"); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
}
|
||||
164
cmd/dkl-local-server/state.go
Normal file
164
cmd/dkl-local-server/state.go
Normal file
@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"m.cluseau.fr/go/watchable"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
type PublicState struct {
|
||||
ServerVersion string
|
||||
UIHash string
|
||||
Store struct {
|
||||
New bool
|
||||
Open bool
|
||||
}
|
||||
}
|
||||
|
||||
var wPublicState = watchable.New[PublicState]()
|
||||
|
||||
type State struct {
|
||||
HasConfig bool
|
||||
|
||||
Store struct {
|
||||
DownloadToken string
|
||||
KeyNames []string
|
||||
}
|
||||
|
||||
Clusters []ClusterState
|
||||
Hosts []HostState
|
||||
Config *localconfig.Config
|
||||
|
||||
Downloads map[string]DownloadSpec
|
||||
|
||||
HostTemplates []string
|
||||
}
|
||||
|
||||
type ClusterState struct {
|
||||
Name string
|
||||
Addons bool
|
||||
Passwords []string
|
||||
Tokens []string
|
||||
CAs []CAState
|
||||
}
|
||||
|
||||
type HostState struct {
|
||||
Name string
|
||||
Cluster string
|
||||
IPs []string
|
||||
|
||||
Template string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type CAState struct {
|
||||
Name string
|
||||
Signed []string
|
||||
}
|
||||
|
||||
var wState = watchable.New[State]()
|
||||
|
||||
func init() {
|
||||
wState.Set(State{Downloads: map[string]DownloadSpec{}})
|
||||
}
|
||||
|
||||
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; v.Store.KeyNames = keyNames })
|
||||
return
|
||||
}
|
||||
|
||||
if secStore.IsNew() || !secStore.Unlocked() {
|
||||
wState.Change(func(v *State) { v.HasConfig = false; v.Config = nil; v.Store.KeyNames = keyNames })
|
||||
return
|
||||
}
|
||||
|
||||
// remove heavy data
|
||||
clusters := make([]ClusterState, 0, len(cfg.Clusters))
|
||||
for _, cluster := range cfg.Clusters {
|
||||
c := ClusterState{
|
||||
Name: cluster.Name,
|
||||
Addons: len(cluster.Addons) != 0,
|
||||
}
|
||||
|
||||
c.Passwords, err = clusterPasswords.Keys(c.Name + "/")
|
||||
if err != nil {
|
||||
log.Print("failed to read cluster passwords: ", err)
|
||||
}
|
||||
c.Tokens, err = clusterTokens.Keys(c.Name + "/")
|
||||
if err != nil {
|
||||
log.Print("failed to read cluster tokens: ", err)
|
||||
}
|
||||
|
||||
caNames, err := clusterCAs.Keys(c.Name + "/")
|
||||
if err != nil {
|
||||
log.Print("failed to read cluster CAs: ", err)
|
||||
}
|
||||
for _, caName := range caNames {
|
||||
ca := CAState{Name: caName}
|
||||
|
||||
signedNames, err := clusterCASignedKeys.Keys(c.Name + "/" + caName + "/")
|
||||
if err != nil {
|
||||
log.Print("failed to read cluster CA signed keys: ", err)
|
||||
}
|
||||
for _, signedName := range signedNames {
|
||||
ca.Signed = append(ca.Signed, signedName)
|
||||
}
|
||||
|
||||
c.CAs = append(c.CAs, ca)
|
||||
}
|
||||
|
||||
clusters = append(clusters, c)
|
||||
}
|
||||
|
||||
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.Store.KeyNames = keyNames
|
||||
v.Clusters = clusters
|
||||
v.Hosts = hosts
|
||||
v.HostTemplates = hostTemplates
|
||||
})
|
||||
}
|
||||
206
cmd/dkl-local-server/tls-ca.go
Normal file
206
cmd/dkl-local-server/tls-ca.go
Normal file
@ -0,0 +1,206 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/initca"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
"github.com/cloudflare/cfssl/signer/local"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
type CA struct {
|
||||
Key []byte
|
||||
Cert []byte
|
||||
|
||||
Signed map[string]*KeyCert
|
||||
}
|
||||
|
||||
func (ca *CA) Init() (err error) {
|
||||
req := ca.newReq()
|
||||
|
||||
cert, _, key, err := initca.New(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("initca: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
ca.Key = key
|
||||
ca.Cert = cert
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ca *CA) RenewCert() (err error) {
|
||||
var signer crypto.Signer
|
||||
signer, err = helpers.ParsePrivateKeyPEM(ca.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newCert, _, err := initca.NewFromSigner(ca.newReq(), signer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ca.Cert = newCert
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (_ CA) newReq() *csr.CertificateRequest {
|
||||
return &csr.CertificateRequest{
|
||||
CN: "Direktil Local Server",
|
||||
KeyRequest: &csr.KeyRequest{
|
||||
A: "ecdsa",
|
||||
S: 521, // 256, 384, 521
|
||||
},
|
||||
Names: []csr.Name{
|
||||
{
|
||||
O: "novit.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 := ca.ParseCert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
caKey, err := ca.ParseKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return local.NewSigner(caKey, caCert, signer.DefaultSigAlgo(caKey), policy)
|
||||
}
|
||||
|
||||
func getUsableKeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest, cfg *config.Config) (kc KeyCert, err error) {
|
||||
log := log.New(log.Default().Writer(), cluster+": CA "+caName+": ", log.Flags()|log.Lmsgprefix)
|
||||
|
||||
ca, err := getUsableClusterCA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, host := range req.Hosts {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
// valid IP (v4 or v6)
|
||||
continue
|
||||
}
|
||||
|
||||
if host == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
|
||||
continue
|
||||
}
|
||||
if errs := validation.IsWildcardDNS1123Subdomain(host); len(errs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%q is not an IP or FQDN", host)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CA != nil {
|
||||
err = errors.New("no CA section allowed here")
|
||||
return
|
||||
}
|
||||
|
||||
rh := hash(req)
|
||||
|
||||
key := cluster + "/" + caName + "/" + name
|
||||
|
||||
kc, found, err := clusterCASignedKeys.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if found {
|
||||
if rh == kc.ReqHash {
|
||||
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
|
||||
}
|
||||
|
||||
log.Print("regenerating certificate: ", err)
|
||||
|
||||
} else {
|
||||
log.Printf("CSR changed for %s: hash=%q previous=%q", name, rh, kc.ReqHash)
|
||||
}
|
||||
} else {
|
||||
log.Print("new CSR for ", name)
|
||||
}
|
||||
|
||||
sgr, err := ca.Signer(cfg.Signing)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
generator := &csr.Generator{Validator: func(_ *csr.CertificateRequest) error { return nil }}
|
||||
|
||||
csr, tlsKey, err := generator.ProcessRequest(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
signReq := signer.SignRequest{
|
||||
Request: string(csr),
|
||||
Profile: profile,
|
||||
Label: label,
|
||||
}
|
||||
|
||||
cert, err := sgr.Sign(signReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kc = KeyCert{
|
||||
Key: tlsKey,
|
||||
Cert: cert,
|
||||
ReqHash: rh,
|
||||
}
|
||||
|
||||
err = clusterCASignedKeys.Put(key, kc)
|
||||
|
||||
return
|
||||
}
|
||||
24
cmd/dkl-local-server/token.go
Normal file
24
cmd/dkl-local-server/token.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"m.cluseau.fr/go/httperr"
|
||||
)
|
||||
|
||||
func newToken(sizeInBytes int) (token string, err error) {
|
||||
randBytes := make([]byte, sizeInBytes)
|
||||
|
||||
_, err = rand.Read(randBytes)
|
||||
if err != nil {
|
||||
log.Print("rand read error: ", err)
|
||||
err = httperr.New(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
45
cmd/dkl-local-server/ui.go
Normal file
45
cmd/dkl-local-server/ui.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
dlshtml "novit.tech/direktil/local-server/html"
|
||||
)
|
||||
|
||||
func computeUIHash() {
|
||||
xxh := xxhash.New()
|
||||
|
||||
err := fs.WalkDir(dlshtml.FS, "ui", func(path string, entry fs.DirEntry, walkErr error) (err error) {
|
||||
if walkErr != nil {
|
||||
err = walkErr
|
||||
return
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := dlshtml.FS.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
io.Copy(xxh, f)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("failed to hash UI: ", err)
|
||||
}
|
||||
|
||||
h := strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(xxh.Sum(nil)))[:5]
|
||||
log.Printf("UI hash: %s", h)
|
||||
wPublicState.Change(func(v *PublicState) { v.UIHash = h })
|
||||
}
|
||||
@ -1,22 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/miolini/datacounter"
|
||||
)
|
||||
|
||||
var (
|
||||
upstreamURL = flag.String("upstream", "https://direktil.novit.nc/dist", "Upstream server for dist elements")
|
||||
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
|
||||
@ -39,42 +62,82 @@ func (ctx *renderContext) distFetch(path ...string) (outPath string, err error)
|
||||
return
|
||||
}
|
||||
|
||||
tempOutPath := filepath.Join(filepath.Dir(outPath), "._part_"+filepath.Base(outPath))
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("wrong status: %s", resp.Status)
|
||||
return
|
||||
}
|
||||
|
||||
length, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
|
||||
|
||||
fOut, err := os.Create(filepath.Join(filepath.Dir(outPath), "._part_"+filepath.Base(outPath)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hash := sha1.New()
|
||||
|
||||
body := datacounter.NewReaderCounter(resp.Body)
|
||||
out := io.MultiWriter(fOut, hash)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
defer resp.Body.Close()
|
||||
defer close(done)
|
||||
_, err = io.Copy(out, body)
|
||||
fOut.Close()
|
||||
|
||||
out, err := os.Create(tempOutPath)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
os.Remove(fOut.Name())
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
done <- err
|
||||
close(done)
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
wait:
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Print("still fetching ", subPath, "...")
|
||||
status := ""
|
||||
if length != 0 {
|
||||
count := body.Count()
|
||||
elapsedDuration := time.Since(start)
|
||||
|
||||
progress := float64(count) / float64(length)
|
||||
|
||||
elapsed := float64(elapsedDuration)
|
||||
remaining := time.Duration(elapsed/progress - elapsed)
|
||||
|
||||
status = fmt.Sprintf(" (%.2f%%, ETA %v, %s/s)",
|
||||
progress*100,
|
||||
remaining.Truncate(time.Second),
|
||||
humanize.Bytes(uint64(float64(count)/elapsedDuration.Seconds())))
|
||||
}
|
||||
log.Printf("still fetching %s%s...", subPath, status)
|
||||
goto wait
|
||||
|
||||
case err = <-done:
|
||||
if err != nil {
|
||||
log.Print("fetch of ", subPath, " failed: ", err)
|
||||
os.Remove(tempOutPath)
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("fetch of ", subPath, " finished")
|
||||
}
|
||||
|
||||
err = os.Rename(tempOutPath, outPath)
|
||||
hexSum := hex.EncodeToString(hash.Sum(nil))
|
||||
log.Printf("fetch of %s finished (SHA1 checksum: %s)", subPath, hexSum)
|
||||
|
||||
if remoteSum := resp.Header.Get("X-Content-SHA1"); remoteSum != "" {
|
||||
log.Printf("fetch of %s: remote SHA1 checksum: %s", subPath, remoteSum)
|
||||
if remoteSum != hexSum {
|
||||
err = fmt.Errorf("wrong SHA1 checksum: server=%s local=%s", remoteSum, hexSum)
|
||||
log.Print("fetch of ", subPath, ": ", err)
|
||||
os.Remove(fOut.Name())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Rename(fOut.Name(), outPath)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
44
cmd/dkl-local-server/wireguard.go
Normal file
44
cmd/dkl-local-server/wireguard.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
var wgKeys = KVSecrets[string]{"wireguard"}
|
||||
|
||||
func wgKey(path string) (key string, err error) {
|
||||
return wgKeys.GetOrCreate(path, func() (key string, err error) {
|
||||
k, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key = k.String()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func wgPSKey(path string) (key string, err error) {
|
||||
return wgKeys.GetOrCreate(path, func() (key string, err error) {
|
||||
k, err := wgtypes.GenerateKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key = k.String()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func wgPubKey(path string) (pubkey string, err error) {
|
||||
key, err := wgKey(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k, err := wgtypes.ParseKey(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubkey = k.PublicKey().String()
|
||||
return
|
||||
}
|
||||
@ -7,24 +7,20 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
resp.WriteErrorString(401, "401: Not Authorized")
|
||||
wsError(resp, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
@ -33,8 +29,12 @@ func getToken(req *restful.Request) string {
|
||||
|
||||
token := req.HeaderParameter("Authorization")
|
||||
|
||||
if token == "" {
|
||||
return req.QueryParameter("token")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token, bearerPrefix) {
|
||||
return ""
|
||||
return token
|
||||
}
|
||||
|
||||
return token[len(bearerPrefix):]
|
||||
|
||||
96
cmd/dkl-local-server/ws-cluster-cas.go
Normal file
96
cmd/dkl-local-server/ws-cluster-cas.go
Normal file
@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
var clusterCAs = newClusterSecretKV[CA]("CAs")
|
||||
|
||||
func wsClusterCAs(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
clusterCAs.WsList(resp, clusterName+"/")
|
||||
}
|
||||
|
||||
func wsClusterCA(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
name := req.PathParameter("ca-name")
|
||||
|
||||
clusterCAs.WsGet(resp, clusterName+"/"+name)
|
||||
}
|
||||
|
||||
func getUsableClusterCA(cluster, name string) (ca CA, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cluster %s CA %s: %w", cluster, name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
key := cluster + "/" + name
|
||||
|
||||
ca, found, err := clusterCAs.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Info("new CA in cluster ", cluster, ": ", name)
|
||||
|
||||
err = ca.Init()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = clusterCAs.Put(key, ca)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
checkErr := checkCertUsable(ca.Cert)
|
||||
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)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var clusterCASignedKeys = newClusterSecretKV[KeyCert]("CA-signed-keys")
|
||||
|
||||
func wsClusterCASignedKeys(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
caName := req.PathParameter("ca-name")
|
||||
clusterCASignedKeys.WsList(resp, clusterName+"/"+caName+"/")
|
||||
}
|
||||
|
||||
func wsClusterCASignedKey(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
caName := req.PathParameter("ca-name")
|
||||
name := req.PathParameter("signed-name")
|
||||
|
||||
clusterCASignedKeys.WsGet(resp, clusterName+"/"+caName+"/"+name)
|
||||
}
|
||||
32
cmd/dkl-local-server/ws-cluster-passwords.go
Normal file
32
cmd/dkl-local-server/ws-cluster-passwords.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
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) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
clusterPasswords.WsList(resp, clusterName+"/")
|
||||
}
|
||||
|
||||
func wsClusterPassword(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
name := req.PathParameter("password-name")
|
||||
|
||||
clusterPasswords.WsGet(resp, clusterName+"/"+name)
|
||||
}
|
||||
|
||||
func wsClusterSetPassword(req *restful.Request, resp *restful.Response) {
|
||||
cluster := wsReadCluster(req, resp)
|
||||
if cluster == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := req.PathParameter("password-name")
|
||||
|
||||
clusterPasswords.WsPut(req, resp, cluster.Name+"/"+name)
|
||||
}
|
||||
43
cmd/dkl-local-server/ws-cluster-tokens.go
Normal file
43
cmd/dkl-local-server/ws-cluster-tokens.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
var clusterTokens = newClusterSecretKV[string]("tokens")
|
||||
|
||||
func getOrCreateClusterToken(cluster, name string) (token string, err error) {
|
||||
key := cluster + "/" + name
|
||||
|
||||
token, found, err := clusterTokens.Get(key)
|
||||
|
||||
if err != nil || found {
|
||||
return
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err = rand.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)
|
||||
|
||||
err = clusterTokens.Put(key, token)
|
||||
return
|
||||
}
|
||||
|
||||
func wsClusterTokens(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
clusterTokens.WsList(resp, clusterName+"/")
|
||||
}
|
||||
|
||||
func wsClusterToken(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
name := req.PathParameter("token-name")
|
||||
|
||||
clusterTokens.WsGet(resp, clusterName+"/"+name)
|
||||
}
|
||||
@ -1,12 +1,31 @@
|
||||
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.nc/direktil/pkg/localconfig"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/mime"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
var clusterSecretKVs = []string{}
|
||||
|
||||
func newClusterSecretKV[T any](name string) KVSecrets[T] {
|
||||
clusterSecretKVs = append(clusterSecretKVs, name)
|
||||
return KVSecrets[T]{"clusters/" + name}
|
||||
}
|
||||
|
||||
func wsListClusters(req *restful.Request, resp *restful.Response) {
|
||||
cfg := wsReadConfig(resp)
|
||||
if cfg == nil {
|
||||
@ -31,7 +50,7 @@ func wsReadCluster(req *restful.Request, resp *restful.Response) (cluster *local
|
||||
|
||||
cluster = cfg.Cluster(clusterName)
|
||||
if cluster == nil {
|
||||
wsNotFound(req, resp)
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
@ -55,9 +74,258 @@ func wsClusterAddons(req *restful.Request, resp *restful.Response) {
|
||||
|
||||
if len(cluster.Addons) == 0 {
|
||||
log.Printf("cluster %q has no addons defined", cluster.Name)
|
||||
wsNotFound(req, resp)
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write([]byte(cluster.Addons))
|
||||
cfg := wsReadConfig(resp)
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sslCfg, err := sslConfigFromLocalConfig(cfg)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
wsRender(resp, sslCfg, cluster.Addons, cluster)
|
||||
}
|
||||
|
||||
func wsClusterCACert(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
caName := req.PathParameter("ca-name")
|
||||
|
||||
ca, found, err := clusterCAs.Get(clusterName + "/" + caName)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Type", mime.CERT)
|
||||
resp.Write(ca.Cert)
|
||||
}
|
||||
|
||||
func wsClusterSignedCert(req *restful.Request, resp *restful.Response) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
caName := req.PathParameter("ca-name")
|
||||
name := req.QueryParameter("name")
|
||||
|
||||
kc, found, err := clusterCASignedKeys.Get(clusterName + "/" + caName + "/" + name)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.AddHeader("Content-Type", mime.CERT)
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,42 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"gopkg.in/yaml.v2"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
body := req.Request.Body
|
||||
|
||||
err := writeNewConfig(body)
|
||||
body.Close()
|
||||
cfg := &localconfig.Config{}
|
||||
if err := req.ReadEntity(cfg); err != nil {
|
||||
wsError(resp, httperr.BadRequest(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
cfgBytes, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = writeNewConfig(cfgBytes)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(true)
|
||||
}
|
||||
|
||||
func writeNewConfig(reader io.Reader) (err error) {
|
||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
||||
func writeNewConfig(cfgBytes []byte) (err error) {
|
||||
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Remove(out.Name())
|
||||
|
||||
_, err = io.Copy(out, reader)
|
||||
_, err = io.Copy(out, bytes.NewReader(cfgBytes))
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
archivesPath := filepath.Join(*dataDir, "archives")
|
||||
cfgPath := configFilePath()
|
||||
in, err := os.Open(cfgPath)
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// nothing to backup
|
||||
} else {
|
||||
return // real error
|
||||
}
|
||||
} else {
|
||||
err = backupCurrentConfig(in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Rename(out.Name(), cfgPath)
|
||||
|
||||
updateState()
|
||||
return
|
||||
}
|
||||
|
||||
func backupCurrentConfig(in io.ReadCloser) (err error) {
|
||||
archivesPath := filepath.Join(*dataDir, "archives")
|
||||
|
||||
err = os.MkdirAll(archivesPath, 0700)
|
||||
if err != nil {
|
||||
@ -52,11 +88,6 @@ func writeNewConfig(reader io.Reader) (err error) {
|
||||
|
||||
defer bck.Close()
|
||||
|
||||
in, err := os.Open(cfgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gz, err := gzip.NewWriterLevel(bck, 2)
|
||||
if err != nil {
|
||||
return
|
||||
@ -66,10 +97,5 @@ func writeNewConfig(reader io.Reader) (err error) {
|
||||
gz.Close()
|
||||
in.Close()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(out.Name(), cfgPath)
|
||||
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)
|
||||
}
|
||||
199
cmd/dkl-local-server/ws-downloads.go
Normal file
199
cmd/dkl-local-server/ws-downloads.go
Normal file
@ -0,0 +1,199 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"m.cluseau.fr/go/cow"
|
||||
)
|
||||
|
||||
type DownloadSpec struct {
|
||||
Kind string
|
||||
Name string
|
||||
Assets []string
|
||||
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
func wsAuthorizeDownload(req *restful.Request, resp *restful.Response) {
|
||||
var spec DownloadSpec
|
||||
|
||||
if err := req.ReadEntity(&spec); err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
if spec.Kind == "" || spec.Name == "" || len(spec.Assets) == 0 {
|
||||
resp.WriteErrorString(http.StatusBadRequest, "missing data")
|
||||
return
|
||||
}
|
||||
|
||||
randBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randBytes)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
token := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
|
||||
|
||||
spec.createdAt = time.Now()
|
||||
|
||||
wState.Change(func(v *State) {
|
||||
cow.MapSet(&v.Downloads, token, spec)
|
||||
})
|
||||
|
||||
log.Printf("download token created for %s %q, assets %q", spec.Kind, spec.Name, spec.Assets)
|
||||
|
||||
resp.WriteAsJson(token)
|
||||
}
|
||||
|
||||
func wsDownloadAsset(req *restful.Request, resp *restful.Response) {
|
||||
token := req.PathParameter("token")
|
||||
asset := req.PathParameter("asset")
|
||||
|
||||
if token == "" || asset == "" {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
var spec DownloadSpec
|
||||
found := false
|
||||
wState.Change(func(v *State) {
|
||||
var ok bool
|
||||
spec, ok = v.Downloads[token]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
newAssets := make([]string, 0, len(spec.Assets))
|
||||
for _, a := range spec.Assets {
|
||||
if a == asset {
|
||||
found = true
|
||||
} else {
|
||||
newAssets = append(newAssets, a)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
cow.Map(&v.Downloads)
|
||||
|
||||
if len(newAssets) == 0 {
|
||||
delete(v.Downloads, token)
|
||||
} else {
|
||||
spec.Assets = newAssets
|
||||
v.Downloads[token] = spec
|
||||
}
|
||||
})
|
||||
|
||||
if !found {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
setHeader := func(ext string) {
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(kind+"_"+name+"_"+asset+ext))
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "cluster":
|
||||
cluster := cfg.ClusterByName(name)
|
||||
if cluster == nil {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
switch asset {
|
||||
case "addons":
|
||||
setHeader(".yaml")
|
||||
resp.Write([]byte(cluster.Addons))
|
||||
|
||||
default:
|
||||
wsNotFound(resp)
|
||||
}
|
||||
|
||||
case "host":
|
||||
host := hostOrTemplate(cfg, name)
|
||||
if host == nil {
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
switch asset {
|
||||
case "config", "bootstrap-config":
|
||||
setHeader(".yaml")
|
||||
default:
|
||||
setHeader("")
|
||||
}
|
||||
|
||||
renderHost(resp.ResponseWriter, req.Request, asset, host, cfg)
|
||||
|
||||
default:
|
||||
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)
|
||||
}
|
||||
@ -8,26 +8,31 @@ import (
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
|
||||
"novit.nc/direktil/local-server/pkg/mime"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
|
||||
"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) 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).
|
||||
Doc("Get the " + ws.hostDoc + "'s details"),
|
||||
rws.GET("").To(ws.get).
|
||||
Produces(mime.JSON).
|
||||
Doc("Get the "+ws.hostDoc+"'s details").
|
||||
Returns(200, "OK", localconfig.Host{}),
|
||||
|
||||
// raw configuration
|
||||
b("config").
|
||||
@ -42,22 +47,43 @@ 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").
|
||||
Produces(mime.TAR).
|
||||
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"),
|
||||
b("boot-efi.tar").
|
||||
Produces(mime.TAR).
|
||||
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"),
|
||||
|
||||
// read-only ISO support
|
||||
b("boot.iso").
|
||||
Produces(mime.ISO).
|
||||
Param(cmdlineParam).
|
||||
Doc("Get the " + ws.hostDoc + "'s boot CD-ROM image"),
|
||||
|
||||
// netboot support
|
||||
@ -65,42 +91,57 @@ 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)"),
|
||||
|
||||
// - 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)"),
|
||||
// - bootstrap
|
||||
b("bootstrap.tar").
|
||||
Produces(mime.TAR).
|
||||
Doc("Get the " + ws.hostDoc + "'s bootstrap seed archive"),
|
||||
} {
|
||||
alterRB(rb)
|
||||
rws.Route(rb)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *wsHost) host(req *restful.Request, resp *restful.Response) (host *localconfig.Host, cfg *localconfig.Config) {
|
||||
hostname := ws.getHost(req)
|
||||
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)
|
||||
return
|
||||
}
|
||||
if hostname == "" {
|
||||
wsNotFound(req, resp)
|
||||
wsNotFound(resp)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readConfig()
|
||||
cfg, err = readConfig()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
host = cfg.Host(hostname)
|
||||
host = hostOrTemplate(cfg, hostname)
|
||||
if host == nil {
|
||||
log.Print("no host named ", hostname)
|
||||
wsNotFound(req, resp)
|
||||
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
|
||||
@ -109,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
|
||||
@ -131,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)
|
||||
|
||||
@ -140,24 +180,40 @@ 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)
|
||||
|
||||
case "boot.tar":
|
||||
err = renderCtx(w, r, ctx, what, buildBootTar)
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
195
cmd/dkl-local-server/ws-public.go
Normal file
195
cmd/dkl-local-server/ws-public.go
Normal file
@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"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) {
|
||||
np := NamedPassphrase{}
|
||||
err := req.ReadEntity(&np)
|
||||
if err != nil {
|
||||
resp.WriteError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
||||
token := req.QueryParameter("token")
|
||||
if token != wState.Get().Store.DownloadToken {
|
||||
wsError(resp, ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
arch := tar.NewWriter(buf)
|
||||
|
||||
root := os.DirFS(secStoreRoot())
|
||||
|
||||
err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, readErr error) (err error) {
|
||||
if readErr != nil {
|
||||
err = readErr
|
||||
return
|
||||
}
|
||||
|
||||
if path == "." {
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := d.Info()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdr, err := tar.FileInfoHeader(fi, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdr.Name = path
|
||||
hdr.Uid = 0
|
||||
hdr.Gid = 0
|
||||
|
||||
err = arch.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := root.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
io.Copy(arch, f)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = arch.Close()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
80
cmd/dkl-local-server/ws-store.go
Normal file
80
cmd/dkl-local-server/ws-store.go
Normal file
@ -0,0 +1,80 @@
|
||||
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) {
|
||||
np := NamedPassphrase{}
|
||||
|
||||
err := req.ReadEntity(&np)
|
||||
if err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1,90 +1,213 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/emicklei/go-restful"
|
||||
"novit.nc/direktil/local-server/pkg/mime"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
"gopkg.in/yaml.v2"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
|
||||
"novit.tech/direktil/local-server/pkg/mime"
|
||||
)
|
||||
|
||||
func buildWS() *restful.WebService {
|
||||
ws := &restful.WebService{}
|
||||
func registerWS(rest *restful.Container) {
|
||||
// public-level APIs
|
||||
{
|
||||
ws := &restful.WebService{}
|
||||
ws.
|
||||
Path("/public").
|
||||
Produces(mime.JSON).
|
||||
Consumes(mime.JSON).
|
||||
Route(ws.POST("/unlock-store").To(wsUnlockStore).
|
||||
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.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")).
|
||||
Route(ws.GET("/download-set").To(wsDownloadSet)).
|
||||
Route(ws.GET("/download-set/{kind}/{name}/{asset}").To(wsDownloadSetAsset))
|
||||
|
||||
// configs API
|
||||
ws.Route(ws.POST("/configs").Filter(adminAuth).To(wsUploadConfig).
|
||||
rest.Add(ws)
|
||||
}
|
||||
|
||||
// Admin-level APIs
|
||||
ws := (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Filter(adminAuth).
|
||||
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(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).
|
||||
Consumes(mime.YAML, mime.JSON).Param(ws.BodyParameter("config", "The new full configuration")).
|
||||
Produces(mime.JSON).Writes(true).
|
||||
Doc("Upload a new current configuration, archiving the previous one"))
|
||||
|
||||
// clusters API
|
||||
ws.Route(ws.GET("/clusters").Filter(adminAuth).To(wsListClusters).
|
||||
// - clusters API
|
||||
ws.Route(ws.GET("/clusters").To(wsListClusters).
|
||||
Doc("List clusters"))
|
||||
|
||||
ws.Route(ws.GET("/clusters/{cluster-name}").Filter(adminAuth).To(wsCluster).
|
||||
Doc("Get cluster details"))
|
||||
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"))
|
||||
|
||||
ws.Route(ws.GET("/clusters/{cluster-name}/addons").Filter(adminAuth).To(wsClusterAddons).
|
||||
Produces(mime.YAML).
|
||||
Doc("Get cluster addons").
|
||||
Returns(http.StatusOK, "OK", nil).
|
||||
Returns(http.StatusNotFound, "The cluster does not exists or does not have addons defined", nil))
|
||||
const (
|
||||
GET = http.MethodGet
|
||||
PUT = http.MethodPut
|
||||
POST = http.MethodPost
|
||||
)
|
||||
|
||||
// hosts API
|
||||
ws.Route(ws.GET("/hosts").Filter(hostsAuth).To(wsListHosts).
|
||||
cluster := func(method, subPath string) *restful.RouteBuilder {
|
||||
return ws.Method(method).Path("/clusters/{cluster-name}" + subPath).
|
||||
Param(ws.PathParameter("cluster-name", "name of the cluster"))
|
||||
}
|
||||
|
||||
for _, builder := range []*restful.RouteBuilder{
|
||||
cluster(GET, "").To(wsCluster).
|
||||
Doc("Get cluster details"),
|
||||
|
||||
cluster(GET, "/addons").To(wsClusterAddons).
|
||||
Produces(mime.YAML).
|
||||
Doc("Get cluster addons").
|
||||
Returns(http.StatusOK, "OK", nil).
|
||||
Returns(http.StatusNotFound, "The cluster does not exists or does not have addons defined", nil),
|
||||
|
||||
cluster(GET, "/tokens").To(wsClusterTokens).
|
||||
Doc("List cluster's tokens"),
|
||||
cluster(GET, "/tokens/{token-name}").To(wsClusterToken).
|
||||
Doc("Get cluster's token"),
|
||||
|
||||
cluster(GET, "/passwords").To(wsClusterPasswords).
|
||||
Doc("List cluster's passwords"),
|
||||
cluster(GET, "/passwords/{password-name}").To(wsClusterPassword).
|
||||
Doc("Get cluster's password"),
|
||||
cluster(PUT, "/passwords/{password-name}").To(wsClusterSetPassword).
|
||||
Doc("Set cluster's password"),
|
||||
|
||||
cluster(GET, "/CAs").To(wsClusterCAs).
|
||||
Doc("Get cluster CAs"),
|
||||
cluster(GET, "/CAs/{ca-name}/certificate").To(wsClusterCACert).
|
||||
Produces(mime.CACERT).
|
||||
Doc("Get cluster CA's certificate"),
|
||||
cluster(GET, "/CAs/{ca-name}/signed").To(wsClusterSignedCert).
|
||||
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)
|
||||
}
|
||||
|
||||
ws.Route(ws.GET("/hosts").To(wsListHosts).
|
||||
Doc("List hosts"))
|
||||
|
||||
(&wsHost{
|
||||
prefix: "/me",
|
||||
hostDoc: "detected host",
|
||||
getHost: detectHost,
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Notes("In this case, the host is detected from the remote IP")
|
||||
})
|
||||
rest.Add(ws)
|
||||
|
||||
// Hosts API
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Filter(adminAuth).
|
||||
Path("/hosts/{host-name}").
|
||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||
|
||||
(&wsHost{
|
||||
prefix: "/hosts/{host-name}",
|
||||
hostDoc: "given host",
|
||||
getHost: func(req *restful.Request) string {
|
||||
return req.PathParameter("host-name")
|
||||
getHost: func(req *restful.Request) (string, error) {
|
||||
return req.PathParameter("host-name"), nil
|
||||
},
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Filter(adminAuth)
|
||||
rb.Param(ws.PathParameter("host-name", "host's name"))
|
||||
})
|
||||
|
||||
return ws
|
||||
rest.Add(ws)
|
||||
|
||||
// Hosts by token API
|
||||
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",
|
||||
getHost: func(req *restful.Request) (host string, err error) {
|
||||
reqToken := req.PathParameter("host-token")
|
||||
|
||||
data, err := hostDownloadTokens.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for h, token := range data {
|
||||
if token == reqToken {
|
||||
host = h
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Notes("In this case, the host is detected from the token")
|
||||
})
|
||||
|
||||
rest.Add(ws)
|
||||
}
|
||||
|
||||
func detectHost(req *restful.Request) string {
|
||||
r := req.Request
|
||||
remoteAddr := r.RemoteAddr
|
||||
|
||||
if *trustXFF {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
remoteAddr = strings.Split(xff, ",")[0]
|
||||
}
|
||||
func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
if !secStore.Unlocked() {
|
||||
wsError(resp, ErrStoreLocked)
|
||||
return
|
||||
}
|
||||
|
||||
hostIP, _, err := net.SplitHostPort(remoteAddr)
|
||||
|
||||
if err != nil {
|
||||
hostIP = remoteAddr
|
||||
}
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := cfg.HostByIP(hostIP)
|
||||
|
||||
if host == nil {
|
||||
log.Print("no host found for IP ", hostIP)
|
||||
return ""
|
||||
}
|
||||
|
||||
return host.Name
|
||||
chain.ProcessFilter(req, resp)
|
||||
}
|
||||
|
||||
func wsReadConfig(resp *restful.Response) *localconfig.Config {
|
||||
@ -98,13 +221,59 @@ func wsReadConfig(resp *restful.Response) *localconfig.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func wsNotFound(req *restful.Request, resp *restful.Response) {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
func wsNotFound(resp *restful.Response) {
|
||||
wsError(resp, ErrNotFound)
|
||||
}
|
||||
|
||||
func wsBadRequest(resp *restful.Response, err string) {
|
||||
httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter)
|
||||
}
|
||||
|
||||
func wsError(resp *restful.Response, err error) {
|
||||
log.Print("request failed: ", err)
|
||||
resp.WriteErrorString(
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
log.Output(2, fmt.Sprint("request failed: ", err))
|
||||
|
||||
switch err := err.(type) {
|
||||
case httperr.Error:
|
||||
err.WriteJSON(resp.ResponseWriter)
|
||||
|
||||
default:
|
||||
httperr.Internal(err).WriteJSON(resp.ResponseWriter)
|
||||
}
|
||||
}
|
||||
|
||||
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) {
|
||||
tmpl, err := template.New("wsRender").Funcs(templateFuncs(sslCfg)).Parse(tmplStr)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tmpl.Execute(resp, value)
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
restful.RegisterEntityAccessor(mime.YAML, yamlEntityAccessor{})
|
||||
}
|
||||
|
||||
type yamlEntityAccessor struct{}
|
||||
|
||||
var _ restful.EntityReaderWriter = yamlEntityAccessor{}
|
||||
|
||||
func (_ yamlEntityAccessor) Read(req *restful.Request, v any) error {
|
||||
decoder := yaml.NewDecoder(req.Request.Body)
|
||||
return decoder.Decode(v)
|
||||
}
|
||||
func (_ yamlEntityAccessor) Write(resp *restful.Response, status int, v any) error {
|
||||
if v == nil {
|
||||
resp.WriteHeader(status)
|
||||
// do not write a nil representation
|
||||
return nil
|
||||
}
|
||||
resp.Header().Set("Content-Type", mime.YAML)
|
||||
resp.WriteHeader(status)
|
||||
return yaml.NewEncoder(resp.ResponseWriter).Encode(v)
|
||||
}
|
||||
|
||||
1
gen-api-js.sh
Executable file
1
gen-api-js.sh
Executable file
@ -0,0 +1 @@
|
||||
docker run --rm --net=host --user $(id -u) -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i http://[::1]:7606/swagger.json -l javascript -o /local/js/api/
|
||||
109
go.mod
109
go.mod
@ -1,34 +1,83 @@
|
||||
module novit.nc/direktil/local-server
|
||||
module novit.tech/direktil/local-server
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e
|
||||
github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e
|
||||
github.com/coreos/etcd v3.3.11+incompatible // indirect
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible
|
||||
github.com/emicklei/go-restful-openapi v1.0.0
|
||||
github.com/go-openapi/jsonpointer v0.18.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.18.0 // indirect
|
||||
github.com/go-openapi/spec v0.18.0 // indirect
|
||||
github.com/go-openapi/swag v0.18.0 // indirect
|
||||
github.com/gobuffalo/buffalo-plugins v1.12.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.0.21 // indirect
|
||||
github.com/json-iterator/go v1.1.5 // indirect
|
||||
github.com/markbates/going v1.0.3 // indirect
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204031235-fc4ac9154422
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
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.16.2
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20191019002626-fd9128c24a34
|
||||
github.com/miolini/datacounter v1.0.3
|
||||
github.com/oklog/ulid v1.3.1
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible
|
||||
github.com/spf13/afero v1.2.1 // indirect
|
||||
github.com/src-d/go-git v4.7.0+incompatible // indirect
|
||||
github.com/ugorji/go/codec v0.0.0-20190128213124-ee1426cffec0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 // indirect
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
|
||||
golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c // indirect
|
||||
golang.org/x/tools v0.0.0-20190202235157-7414d4c1f71c // indirect
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.0
|
||||
gopkg.in/src-d/go-git.v4 v4.10.0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/apimachinery v0.0.0-20190201131811-df262fa1a1ba
|
||||
novit.nc/direktil/pkg v0.0.0-20181210211743-9dc80cd34b09
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible
|
||||
github.com/sergeymakinen/go-crypt v1.0.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/apimachinery v0.33.2
|
||||
m.cluseau.fr/go v0.0.0-20230809064045-12c5a121c766
|
||||
novit.tech/direktil/pkg v0.0.0-20260125193049-56f78e083a84
|
||||
)
|
||||
|
||||
replace github.com/zmap/zlint/v3 => github.com/zmap/zlint/v3 v3.3.1
|
||||
|
||||
require (
|
||||
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.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/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.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.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.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.40.3-0.20250617082559-9b2e24a9e482 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // 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/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.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
)
|
||||
|
||||
922
go.sum
922
go.sum
@ -1,690 +1,350 @@
|
||||
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/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
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 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 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
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=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:mep9E//vzfjmtYUVBAvRMIW5BKTL88nidNZc8RF2lhA=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180705210102-ff56ab5eb62a h1:wbCZ8e4WG5hcfVKSyaqobgvEe/uZeVvkbUBX1zgr9kw=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180705210102-ff56ab5eb62a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/cfssl v0.0.0-20181102015659-ea4033a214e7 h1:ROpiky+uT1fstFCMZCka5Cr9GmtpTakLMmvwFsVOtJA=
|
||||
github.com/cloudflare/cfssl v0.0.0-20181102015659-ea4033a214e7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e h1:Qux+lbuMaRzkQyTdzgtz8MgzPtzmaPQy6DXmxpdxT3U=
|
||||
github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
|
||||
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
|
||||
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/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.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/etcd v3.3.11+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/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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible h1:AyDqLHbJ1quqbWr/OWDw+PlIP8ZFoTmYrGYaxzrLbNg=
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful-openapi v1.0.0 h1:ZFk3RuCl8ZmG1yUAF/mSbXRi5cuyA/k5+EpHayuuTXM=
|
||||
github.com/emicklei/go-restful-openapi v1.0.0/go.mod h1:Q+bHVYfUWv1fvC4FNTsz2AVvFSsXAC7RCiWjF1Sva1A=
|
||||
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
|
||||
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
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=
|
||||
github.com/emicklei/go-restful-openapi v1.4.1 h1:SocVTIQWnXyit4dotTrwmncBAjtRaBmfcHjo3XGcCm4=
|
||||
github.com/emicklei/go-restful-openapi v1.4.1/go.mod h1:kWQ8rQMVQ6G6lePwjDveJ00KjAUr/jq6z1X8DrDP3Gc=
|
||||
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/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.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.18.0 h1:KVRzjXpMzgdM4GEMDmDTnGcY5yBwGWreJwmmk4k35yU=
|
||||
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.18.0 h1:oP2OUNdG1l2r5kYhrfVMXO54gWmzcfAwP/GFuHpNTkE=
|
||||
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/spec v0.18.0 h1:aIjeyG5mo5/FrvDkpKKEGZPmF9MPHahS72mzfVqeQXQ=
|
||||
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.18.0 h1:1DU8Km1MRGv9Pj7BNLmkA+umwTStwDHttXvx3NhJA70=
|
||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY=
|
||||
github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4=
|
||||
github.com/gobuffalo/buffalo v0.13.1/go.mod h1:K9c22KLfDz7obgxvHv1amvJtCQEZNiox9+q6FDJ1Zcs=
|
||||
github.com/gobuffalo/buffalo v0.13.2/go.mod h1:vA8I4Dwcfkx7RAzIRHVDZxfS3QJR7muiOjX4r8P2/GE=
|
||||
github.com/gobuffalo/buffalo v0.13.4/go.mod h1:y2jbKkO0k49OrNIOAkbWQiPBqxAFpHn5OKnkc7BDh+I=
|
||||
github.com/gobuffalo/buffalo v0.13.5/go.mod h1:hPcP12TkFSZmT3gUVHZ24KRhTX3deSgu6QSgn0nbWf4=
|
||||
github.com/gobuffalo/buffalo v0.13.6/go.mod h1:/Pm0MPLusPhWDayjRD+/vKYnelScIiv0sX9YYek0wpg=
|
||||
github.com/gobuffalo/buffalo v0.13.7/go.mod h1:3gQwZhI8DSbqmDqlFh7kfwuv/wd40rqdVxXtFWlCQHw=
|
||||
github.com/gobuffalo/buffalo v0.13.9/go.mod h1:vIItiQkTHq46D1p+bw8mFc5w3BwrtJhMvYjSIYK3yjE=
|
||||
github.com/gobuffalo/buffalo v0.13.10/go.mod h1:t/G4NHciI1PxWBUrPlW+6HGfgStmk7cozRIRt5O0674=
|
||||
github.com/gobuffalo/buffalo v0.13.12/go.mod h1:Y9e0p0cdo/eI+lHm7EFzlkc9YzjwGo5QeDj+FbsyqVA=
|
||||
github.com/gobuffalo/buffalo v0.13.13/go.mod h1:WAL36xBN8OkU71lNjuYv6llmgl0o8twjlY+j7oGUmYw=
|
||||
github.com/gobuffalo/buffalo-plugins v1.0.2/go.mod h1:pOp/uF7X3IShFHyobahTkTLZaeUXwb0GrUTb9ngJWTs=
|
||||
github.com/gobuffalo/buffalo-plugins v1.0.4/go.mod h1:pWS1vjtQ6uD17MVFWf7i3zfThrEKWlI5+PYLw/NaDB4=
|
||||
github.com/gobuffalo/buffalo-plugins v1.4.3/go.mod h1:uCzTY0woez4nDMdQjkcOYKanngeUVRO2HZi7ezmAjWY=
|
||||
github.com/gobuffalo/buffalo-plugins v1.5.1/go.mod h1:jbmwSZK5+PiAP9cC09VQOrGMZFCa/P0UMlIS3O12r5w=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.1/go.mod h1:/XZt7UuuDnx5P4v3cStK0+XoYiNOA2f0wDIsm1oLJQA=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.4/go.mod h1:/+N1aophkA2jZ1ifB2O3Y9yGwu6gKOVMtUmJnbg+OZI=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.5/go.mod h1:0HVkbgrVs/MnPZ/FOseDMVanCTm2RNcdM0PuXcL1NNI=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.6/go.mod h1:hSWAEkJyL9RENJlmanMivgnNkrQ9RC4xJARz8dQryi0=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.7/go.mod h1:ZGZRkzz2PiKWHs0z7QsPBOTo2EpcGRArMEym6ghKYgk=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.9/go.mod h1:yYlYTrPdMCz+6/+UaXg5Jm4gN3xhsvsQ2ygVatZV5vw=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.10/go.mod h1:HxzPZjAEzh9H0gnHelObxxrut9O+1dxydf7U93SYsc8=
|
||||
github.com/gobuffalo/buffalo-plugins v1.6.11/go.mod h1:eAA6xJIL8OuynJZ8amXjRmHND6YiusVAaJdHDN1Lu8Q=
|
||||
github.com/gobuffalo/buffalo-plugins v1.7.2/go.mod h1:vEbx30cLFeeZ48gBA/rkhbqC2M/2JpsKs5CoESWhkPw=
|
||||
github.com/gobuffalo/buffalo-plugins v1.8.1/go.mod h1:vu71J3fD4b7KKywJQ1tyaJGtahG837Cj6kgbxX0e4UI=
|
||||
github.com/gobuffalo/buffalo-plugins v1.8.2/go.mod h1:9te6/VjEQ7pKp7lXlDIMqzxgGpjlKoAcAANdCgoR960=
|
||||
github.com/gobuffalo/buffalo-plugins v1.8.3/go.mod h1:IAWq6vjZJVXebIq2qGTLOdlXzmpyTZ5iJG5b59fza5U=
|
||||
github.com/gobuffalo/buffalo-plugins v1.9.3/go.mod h1:BNRunDThMZKjqx6R+n14Rk3sRSOWgbMuzCKXLqbd7m0=
|
||||
github.com/gobuffalo/buffalo-plugins v1.9.4/go.mod h1:grCV6DGsQlVzQwk6XdgcL3ZPgLm9BVxlBmXPMF8oBHI=
|
||||
github.com/gobuffalo/buffalo-plugins v1.10.0/go.mod h1:4osg8d9s60txLuGwXnqH+RCjPHj9K466cDFRl3PErHI=
|
||||
github.com/gobuffalo/buffalo-plugins v1.11.0/go.mod h1:rtIvAYRjYibgmWhnjKmo7OadtnxuMG5ZQLr25ozAzjg=
|
||||
github.com/gobuffalo/buffalo-plugins v1.12.0/go.mod h1:kw4Mj2vQXqe4X5TI36PEQgswbL30heGQwJEeDKd1v+4=
|
||||
github.com/gobuffalo/buffalo-plugins v1.12.1/go.mod h1:Y9nH2VwHVkeKhmdM380ulNXmhhD5On81nRVeD+WlDTQ=
|
||||
github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.2/go.mod h1:czNLXcYbg5/fjr+uht0NyjZaQ0V2W23H1jzyORgCzQ4=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.5/go.mod h1:H01JIg42XwOHS4gRMhSeDZqBovNVlfBUsVXckU617s4=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.8/go.mod h1:1uaxOFzzVud/zR5f1OEBr21tMVLQS3OZpQ1A5cr0svE=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.13/go.mod h1:47GQoBjCMcl5Pw40iCWHQYJvd0HsT9kdaOPWgnzHzk4=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.14/go.mod h1:sAMh6+s7wytCn5cHqZIuItJbAqzvs6M7FemLexl+pwc=
|
||||
github.com/gobuffalo/buffalo-pop v1.1.15/go.mod h1:vnvvxhbEFAaEbac9E2ZPjsBeL7WHkma2UyKNVA4y9Wo=
|
||||
github.com/gobuffalo/buffalo-pop v1.2.1/go.mod h1:SHqojN0bVzaAzCbQDdWtsib202FDIxqwmCO8VDdweF4=
|
||||
github.com/gobuffalo/buffalo-pop v1.3.0/go.mod h1:P0PhA225dRGyv0WkgYjYKqgoxPdDPDFZDvHj60AGF5w=
|
||||
github.com/gobuffalo/buffalo-pop v1.4.0/go.mod h1:DYpTPbPFvMZYirNkWQkbwmsBOD5V7c/74V+LfCY6LXQ=
|
||||
github.com/gobuffalo/buffalo-pop v1.6.0/go.mod h1:vrEVNOBKe042HjSNMj72J4FgER/VG6lt4xW6WMpTdlY=
|
||||
github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc=
|
||||
github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||
github.com/gobuffalo/envy v1.6.6/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||
github.com/gobuffalo/envy v1.6.7 h1:XMZGuFqTupAXhZTriQ+qO38QvNOSU/0rl3hEPCFci/4=
|
||||
github.com/gobuffalo/envy v1.6.7/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||
github.com/gobuffalo/envy v1.6.8/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||
github.com/gobuffalo/envy v1.6.9/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
|
||||
github.com/gobuffalo/envy v1.6.10/go.mod h1:X0CFllQjTV5ogsnUrg+Oks2yTI+PU2dGYBJOEI2D1Uo=
|
||||
github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7FB6EAcg=
|
||||
github.com/gobuffalo/envy v1.6.12 h1:zkhss8DXz/pty2HAyA8BnvWMTYxo4gjd4+WCnYovoxY=
|
||||
github.com/gobuffalo/envy v1.6.12/go.mod h1:qJNrJhKkZpEW0glh5xP2syQHH5kgdmgsKss2Kk8PTP0=
|
||||
github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw=
|
||||
github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ=
|
||||
github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs=
|
||||
github.com/gobuffalo/events v1.1.1/go.mod h1:Ia9OgHMco9pEhJaPrPQJ4u4+IZlkxYVco2VbJ2XgnAE=
|
||||
github.com/gobuffalo/events v1.1.3/go.mod h1:9yPGWYv11GENtzrIRApwQRMYSbUgCsZ1w6R503fCfrk=
|
||||
github.com/gobuffalo/events v1.1.4/go.mod h1:09/YRRgZHEOts5Isov+g9X2xajxdvOAcUuAHIX/O//A=
|
||||
github.com/gobuffalo/events v1.1.5/go.mod h1:3YUSzgHfYctSjEjLCWbkXP6djH2M+MLaVRzb4ymbAK0=
|
||||
github.com/gobuffalo/events v1.1.6/go.mod h1:H/3ZB9BA+WorMb/0F79UvU6u0Cyo2hU97WA51bG2ONY=
|
||||
github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eLzh+S8CxY=
|
||||
github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8=
|
||||
github.com/gobuffalo/events v1.1.9/go.mod h1:/0nf8lMtP5TkgNbzYxR6Bl4GzBy5s5TebgNTdRfRbPM=
|
||||
github.com/gobuffalo/events v1.2.0/go.mod h1:pxvpvsKXKZNPtHuIxUV3K+g+KP5o4forzaeFj++bh68=
|
||||
github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc=
|
||||
github.com/gobuffalo/fizz v1.0.15/go.mod h1:EI3mEpjImuji6Bwu++N2uXhljQwOhwtimZQJ89zwyF4=
|
||||
github.com/gobuffalo/fizz v1.0.16/go.mod h1:EI3mEpjImuji6Bwu++N2uXhljQwOhwtimZQJ89zwyF4=
|
||||
github.com/gobuffalo/fizz v1.1.2/go.mod h1:THqzNTlNxNaF5hq3ddp16SnEcl2m83bTeTzJEoD+kqc=
|
||||
github.com/gobuffalo/fizz v1.1.3/go.mod h1:THqzNTlNxNaF5hq3ddp16SnEcl2m83bTeTzJEoD+kqc=
|
||||
github.com/gobuffalo/fizz v1.3.0/go.mod h1:THqzNTlNxNaF5hq3ddp16SnEcl2m83bTeTzJEoD+kqc=
|
||||
github.com/gobuffalo/fizz v1.5.0/go.mod h1:Uu3ch14M4S7LDU7LAP1GQ+KNCRmZYd05Gqasc96XLa0=
|
||||
github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181007231023-ae7ed6bfe683/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181018182602-fd24a256709f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181019110701-3d6f0b585514/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181024204909-8f6be1a8c6c2/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181104133451-1f6e9779237a/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181108195648-8fe1b44cfe32/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA=
|
||||
github.com/gobuffalo/flect v0.0.0-20181109221320-179d36177b5b/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI=
|
||||
github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdfh+WOvDSIASqJOSxTOWSxCCUF++k/Y53v9rI=
|
||||
github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
|
||||
github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk=
|
||||
github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k=
|
||||
github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g=
|
||||
github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181007153042-b8de7d566757/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
|
||||
github.com/gobuffalo/genny v0.0.0-20181012161047-33e5f43d83a6/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
|
||||
github.com/gobuffalo/genny v0.0.0-20181017160347-90a774534246/go.mod h1:+oG5Ljrw04czAHbPXREwaFojJbpUvcIy4DiOnbEJFTA=
|
||||
github.com/gobuffalo/genny v0.0.0-20181019144442-df0a36fdd146/go.mod h1:IyRrGrQb/sbHu/0z9i5mbpZroIsdxjCYfj+zFiFiWZQ=
|
||||
github.com/gobuffalo/genny v0.0.0-20181024195656-51392254bf53/go.mod h1:o9GEH5gn5sCKLVB5rHFC4tq40rQ3VRUzmx6WwmaqISE=
|
||||
github.com/gobuffalo/genny v0.0.0-20181025145300-af3f81d526b8/go.mod h1:uZ1fFYvdcP8mu0B/Ynarf6dsGvp7QFIpk/QACUuFUVI=
|
||||
github.com/gobuffalo/genny v0.0.0-20181027191429-94d6cfb5c7fc/go.mod h1:x7SkrQQBx204Y+O9EwRXeszLJDTaWN0GnEasxgLrQTA=
|
||||
github.com/gobuffalo/genny v0.0.0-20181027195209-3887b7171c4f/go.mod h1:JbKx8HSWICu5zyqWOa0dVV1pbbXOHusrSzQUprW6g+w=
|
||||
github.com/gobuffalo/genny v0.0.0-20181030163439-ed103521b8ec/go.mod h1:3Xm9z7/2oRxlB7PSPLxvadZ60/0UIek1YWmcC7QSaVs=
|
||||
github.com/gobuffalo/genny v0.0.0-20181106193839-7dcb0924caf1/go.mod h1:x61yHxvbDCgQ/7cOAbJCacZQuHgB0KMSzoYcw5debjU=
|
||||
github.com/gobuffalo/genny v0.0.0-20181107223128-f18346459dbe/go.mod h1:utQD3aKKEsdb03oR+Vi/6ztQb1j7pO10N3OBoowRcSU=
|
||||
github.com/gobuffalo/genny v0.0.0-20181109163038-9539921b620f/go.mod h1:118bnhJR2oviiji++mZj0IH/IaFBCzwkWHaI4OQq5hQ=
|
||||
github.com/gobuffalo/genny v0.0.0-20181110202416-7b7d8756a9e2/go.mod h1:118bnhJR2oviiji++mZj0IH/IaFBCzwkWHaI4OQq5hQ=
|
||||
github.com/gobuffalo/genny v0.0.0-20181111200257-599b33630ab4/go.mod h1:w+iD/cdtIpPDFax6LlUFuCdXFD0DLRUXsfp3IeT/Doc=
|
||||
github.com/gobuffalo/genny v0.0.0-20181114215459-0a4decd77f5d/go.mod h1:kN2KZ8VgXF9VIIOj/GM0Eo7YK+un4Q3tTreKOf0q1ng=
|
||||
github.com/gobuffalo/genny v0.0.0-20181119162812-e8ff4adce8bb/go.mod h1:BA9htSe4bZwBDJLe8CUkoqkypq3hn3+CkoHqVOW718E=
|
||||
github.com/gobuffalo/genny v0.0.0-20181127225641-2d959acc795b/go.mod h1:l54xLXNkteX/PdZ+HlgPk1qtcrgeOr3XUBBPDbH+7CQ=
|
||||
github.com/gobuffalo/genny v0.0.0-20181128191930-77e34f71ba2a/go.mod h1:FW/D9p7cEEOqxYA71/hnrkOWm62JZ5ZNxcNIVJEaWBU=
|
||||
github.com/gobuffalo/genny v0.0.0-20181203165245-fda8bcce96b1/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181203201232-849d2c9534ea/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181206121324-d6fb8a0dbe36/go.mod h1:wpNSANu9UErftfiaAlz1pDZclrYzLtO5lALifODyjuM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGGQf2T3vOhCrGHheYN54Y/REj0ayd0Suf4C/8=
|
||||
github.com/gobuffalo/genny v0.0.0-20181207183327-0a63325ed5db/go.mod h1:TWply4MiCmzG1s2mKgxIdzqZjBSX4ibOMC8JaP+/Vm4=
|
||||
github.com/gobuffalo/genny v0.0.0-20181207193730-587570ab2cf7/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM=
|
||||
github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM=
|
||||
github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY=
|
||||
github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo=
|
||||
github.com/gobuffalo/genny v0.0.0-20190124191459-3310289fa4b4/go.mod h1:yIRqxhZV2sAzb+B3iPUMLauTRrYP8tJUlZ1zV9teKik=
|
||||
github.com/gobuffalo/genny v0.0.0-20190131150032-1045e97d19fb/go.mod h1:yIRqxhZV2sAzb+B3iPUMLauTRrYP8tJUlZ1zV9teKik=
|
||||
github.com/gobuffalo/genny v0.0.0-20190131190646-008a76242145/go.mod h1:NJvPZJxb9M4z790P6N2SMZKSUYpASpEvLuUWnHGKzb4=
|
||||
github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I=
|
||||
github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY=
|
||||
github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI=
|
||||
github.com/gobuffalo/httptest v1.0.2/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
|
||||
github.com/gobuffalo/httptest v1.0.3/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
|
||||
github.com/gobuffalo/httptest v1.0.4/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
|
||||
github.com/gobuffalo/httptest v1.0.5/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
|
||||
github.com/gobuffalo/httptest v1.0.6/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E=
|
||||
github.com/gobuffalo/licenser v0.0.0-20180924033006-eae28e638a42/go.mod h1:Ubo90Np8gpsSZqNScZZkVXXAo5DGhTb+WYFIjlnog8w=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181025145548-437d89de4f75/go.mod h1:x3lEpYxkRG/XtGCUNkio+6RZ/dlOvLzTI9M1auIwFcw=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181027200154-58051a75da95/go.mod h1:BzhaaxGd1tq1+OLKObzgdCV9kqVhbTulxOpYbvMQWS0=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181109171355-91a2a7aac9a7/go.mod h1:m+Ygox92pi9bdg+gVaycvqE8RVSjZp7mWw75+K5NPHk=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181116224424-1b7fd3f9cbb4/go.mod h1:icHYfF2FVDi6CpI8BK9Sy1ChkSijz/0GNN7Qzzdk6JE=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181128165715-cc7305f8abed/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181128170751-82cc989582b9/go.mod h1:oU9F9UCE+AzI/MueCKZamsezGOOHfSirltllOVeRTAE=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181203160806-fe900bbede07/go.mod h1:ph6VDNvOzt1CdfaWC+9XwcBnlSTBz2j49PBwum6RFaU=
|
||||
github.com/gobuffalo/licenser v0.0.0-20181211173111-f8a311c51159/go.mod h1:ve/Ue99DRuvnTaLq2zKa6F4KtHiYf7W046tDjuGYPfM=
|
||||
github.com/gobuffalo/logger v0.0.0-20181022175615-46cfb361fc27/go.mod h1:8sQkgyhWipz1mIctHF4jTxmJh1Vxhp7mP8IqbljgJZo=
|
||||
github.com/gobuffalo/logger v0.0.0-20181027144941-73d08d2bb969/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
||||
github.com/gobuffalo/logger v0.0.0-20181027193913-9cf4dd0efe46/go.mod h1:7uGg2duHKpWnN4+YmyKBdLXfhopkAdVM6H3nKbyFbz8=
|
||||
github.com/gobuffalo/logger v0.0.0-20181109185836-3feeab578c17/go.mod h1:oNErH0xLe+utO+OW8ptXMSA5DkiSEDW1u3zGIt8F9Ew=
|
||||
github.com/gobuffalo/logger v0.0.0-20181117211126-8e9b89b7c264/go.mod h1:5etB91IE0uBlw9k756fVKZJdS+7M7ejVhmpXXiSFj0I=
|
||||
github.com/gobuffalo/logger v0.0.0-20181127160119-5b956e21995c/go.mod h1:+HxKANrR9VGw9yN3aOAppJKvhO05ctDi63w4mDnKv2U=
|
||||
github.com/gobuffalo/makr v1.1.5/go.mod h1:Y+o0btAH1kYAMDJW/TX3+oAXEu0bmSLLoC9mIFxtzOw=
|
||||
github.com/gobuffalo/mapi v1.0.0/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/meta v0.0.0-20181018155829-df62557efcd3/go.mod h1:XTTOhwMNryif3x9LkTTBO/Llrveezd71u3quLd0u7CM=
|
||||
github.com/gobuffalo/meta v0.0.0-20181018192820-8c6cef77dab3/go.mod h1:E94EPzx9NERGCY69UWlcj6Hipf2uK/vnfrF4QD0plVE=
|
||||
github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg=
|
||||
github.com/gobuffalo/meta v0.0.0-20181109154556-f76929ccd5fa/go.mod h1:1rYI5QsanV6cLpT1BlTAkrFi9rtCZrGkvSK8PglwfS8=
|
||||
github.com/gobuffalo/meta v0.0.0-20181114191255-b130ebedd2f7/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE=
|
||||
github.com/gobuffalo/meta v0.0.0-20181116202903-8850e47774f5/go.mod h1:K6cRZ29ozr4Btvsqkjvg5nDFTLOgTqf03KA70Ks0ypE=
|
||||
github.com/gobuffalo/meta v0.0.0-20181127070345-0d7e59dd540b/go.mod h1:RLO7tMvE0IAKAM8wny1aN12pvEKn7EtkBLkUZR00Qf8=
|
||||
github.com/gobuffalo/meta v0.0.0-20190120163247-50bbb1fa260d/go.mod h1:KKsH44nIK2gA8p0PJmRT9GvWJUdphkDUA8AJEvFWiqM=
|
||||
github.com/gobuffalo/meta v0.0.0-20190121163014-ecaa953cbfb3/go.mod h1:KLfkGnS+Tucc+iTkUcAUBtxpwOJGfhw2pHRLddPxMQY=
|
||||
github.com/gobuffalo/meta v0.0.0-20190126124307-c8fb6f4eb5a9/go.mod h1:zoh6GLgkk9+iI/62dST4amAuVAczZrBXoAk/t64n7Ew=
|
||||
github.com/gobuffalo/mw-basicauth v1.0.3/go.mod h1:dg7+ilMZOKnQFHDefUzUHufNyTswVUviCBgF244C1+0=
|
||||
github.com/gobuffalo/mw-basicauth v1.0.6/go.mod h1:RFyeGeDLZlVgp/eBflqu2eavFqyv0j0fVVP87WPYFwY=
|
||||
github.com/gobuffalo/mw-basicauth v1.0.7/go.mod h1:xJ9/OSiOWl+kZkjaSun62srODr3Cx8OB4AKr+G4FlS4=
|
||||
github.com/gobuffalo/mw-contenttype v0.0.0-20180802152300-74f5a47f4d56/go.mod h1:7EvcmzBbeCvFtQm5GqF9ys6QnCxz2UM1x0moiWLq1No=
|
||||
github.com/gobuffalo/mw-contenttype v0.0.0-20190129203934-2554e742333b/go.mod h1:7x87+mDrr9Peh7AqhOtESyJLanMd2zQNz2Hts+vtBoE=
|
||||
github.com/gobuffalo/mw-csrf v0.0.0-20180802151833-446ff26e108b/go.mod h1:sbGtb8DmDZuDUQoxjr8hG1ZbLtZboD9xsn6p77ppcHo=
|
||||
github.com/gobuffalo/mw-csrf v0.0.0-20190129204204-25460a055517/go.mod h1:o5u+nnN0Oa7LBeDYH9QP36qeMPnXV9qbVnbZ4D+Kb0Q=
|
||||
github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130/go.mod h1:JvNHRj7bYNAMUr/5XMkZaDcw3jZhUZpsmzhd//FFWmQ=
|
||||
github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6/go.mod h1:91AQfukc52A6hdfIfkxzyr+kpVYDodgAeT5cjX1UIj4=
|
||||
github.com/gobuffalo/mw-i18n v0.0.0-20181027200759-09e0c99be4d3/go.mod h1:1PpGPgqP8VsfUppgBA9FrTOXjI6X9gjqhh/8dmg48lg=
|
||||
github.com/gobuffalo/mw-i18n v0.0.0-20190129204410-552713a3ebb4/go.mod h1:rBg2eHxsyxVjtYra6fGy4GSF5C8NysOvz+Znnzk42EM=
|
||||
github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e/go.mod h1:6OJr6VwSzgJMqWMj7TYmRUqzNe2LXu/W1rRW4MAz/ME=
|
||||
github.com/gobuffalo/mw-paramlogger v0.0.0-20190129202837-395da1998525/go.mod h1:gEo/ABCsKqvpp/KCxN2AIzDEe0OJUXbJ9293FYrXw+w=
|
||||
github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189/go.mod h1:UqBF00IfKvd39ni5+yI5MLMjAf4gX7cDKN/26zDOD6c=
|
||||
github.com/gobuffalo/mw-tokenauth v0.0.0-20181227153927-0ac66122b3fa/go.mod h1:UqBF00IfKvd39ni5+yI5MLMjAf4gX7cDKN/26zDOD6c=
|
||||
github.com/gobuffalo/mw-tokenauth v0.0.0-20190129201951-95847f29c5c8/go.mod h1:n2oa93LHGD94hGI+PoJO+6cf60DNrXrAIv9L/Ke3GXc=
|
||||
github.com/gobuffalo/packd v0.0.0-20181027182251-01ad393492c8/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
|
||||
github.com/gobuffalo/packd v0.0.0-20181027190505-aafc0d02c411/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
|
||||
github.com/gobuffalo/packd v0.0.0-20181027194105-7ae579e6d213/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc=
|
||||
github.com/gobuffalo/packd v0.0.0-20181028162033-6d52e0eabf41/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181029140631-cf76bd87a5a6/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSqW9V1wT9pNOVzrpo2NWsxja53slX0=
|
||||
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181103221656-16c4ed88b296/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181104210303-d376b15f8e96/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181111195323-b2e760a5f0ff/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181114190715-f25c5d2471d7/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packd v0.0.0-20181124090624-311c6248e5fb/go.mod h1:Foenia9ZvITEvG05ab6XpiD5EfBHPL8A6hush8SJ0o8=
|
||||
github.com/gobuffalo/packd v0.0.0-20181207120301-c49825f8f6f4/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA=
|
||||
github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687 h1:uZ+G4JprR0UEq0aHZs+6eP7TEZuFfrIkmQWejIBV/QQ=
|
||||
github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA=
|
||||
github.com/gobuffalo/packr v1.13.7/go.mod h1:KkinLIn/n6+3tVXMwg6KkNvWwVsrRAz4ph+jgpk3Z24=
|
||||
github.com/gobuffalo/packr v1.15.0/go.mod h1:t5gXzEhIviQwVlNx/+3SfS07GS+cZ2hn76WLzPp6MGI=
|
||||
github.com/gobuffalo/packr v1.15.1/go.mod h1:IeqicJ7jm8182yrVmNbM6PR4g79SjN9tZLH8KduZZwE=
|
||||
github.com/gobuffalo/packr v1.16.0/go.mod h1:Yx/lcR/7mDLXhuJSzsz2MauD/HUwSc+EK6oigMRGGsM=
|
||||
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
|
||||
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
|
||||
github.com/gobuffalo/packr v1.20.0/go.mod h1:JDytk1t2gP+my1ig7iI4NcVaXr886+N0ecUga6884zw=
|
||||
github.com/gobuffalo/packr v1.21.0/go.mod h1:H00jGfj1qFKxscFJSw8wcL4hpQtPe1PfU2wa6sg/SR0=
|
||||
github.com/gobuffalo/packr v1.21.5/go.mod h1:zCvDxrZzFmq5Xd7Jw4vaGe/OYwzuXnma31D2EbTHMWk=
|
||||
github.com/gobuffalo/packr v1.21.7/go.mod h1:73tmYjwi4Cvb1eNiAwpmrzZ0gxVA4KBqVSZ2FNeJodM=
|
||||
github.com/gobuffalo/packr v1.21.8/go.mod h1:aRZXyERYmMgohDp5wDWnbgn5KiWuCKG19WnWZcAqeII=
|
||||
github.com/gobuffalo/packr v1.21.9 h1:zBaEhCmJpYy/UdHGAGIC3vO5Uh7RW091le41+Ydcg4E=
|
||||
github.com/gobuffalo/packr v1.21.9/go.mod h1:GC76q6nMzRtR+AEN/VV4w0z2/4q7SOaEmXh3Ooa8sOE=
|
||||
github.com/gobuffalo/packr v1.22.0 h1:/YVd/GRGsu0QuoCJtlcWSVllobs4q3Xvx3nqxTvPyN0=
|
||||
github.com/gobuffalo/packr v1.22.0/go.mod h1:Qr3Wtxr3+HuQEwWqlLnNW4t1oTvK+7Gc/Rnoi/lDFvA=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.5/go.mod h1:e6gmOfhf3KmT4zl2X/NDRSfBXk2oV4TXZ+NNOM0xwt8=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.7/go.mod h1:BzhceHWfF3DMAkbPUONHYWs63uacCZxygFY1b4H9N2A=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.8/go.mod h1:y60QCdzwuMwO2R49fdQhsjCPv7tLQFR0ayzxxla9zes=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.9/go.mod h1:fQqADRfZpEsgkc7c/K7aMew3n4aF1Kji7+lIZeR98Fc=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.10/go.mod h1:4CWWn4I5T3v4c1OsJ55HbHlUEKNWMITG5iIkdr4Px4w=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.11/go.mod h1:JoieH/3h3U4UmatmV93QmqyPUdf4wVM9HELaHEu+3fk=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZBjB/tDV9Cz/lSaR8=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs=
|
||||
github.com/gobuffalo/packr/v2 v2.0.0/go.mod h1:7McfLpSxaPUoSQm7gYpTZRQSK63mX8EKzzYSEFKvfkM=
|
||||
github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.22+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.23+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.30+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.31+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.32+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plush v3.7.33+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI=
|
||||
github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4=
|
||||
github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs=
|
||||
github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0=
|
||||
github.com/gobuffalo/plushgen v0.0.0-20190104222512-177cd2b872b3/go.mod h1:tYxCozi8X62bpZyKXYHw1ncx2ZtT2nFvG42kuLwYjoc=
|
||||
github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.8.5+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.8.7+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.8.8+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.0+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.5+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/pop v4.9.6+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
|
||||
github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
|
||||
github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
|
||||
github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
|
||||
github.com/gobuffalo/release v1.0.51/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
|
||||
github.com/gobuffalo/release v1.0.52/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug=
|
||||
github.com/gobuffalo/release v1.0.53/go.mod h1:FdF257nd8rqhNaqtDWFGhxdJ/Ig4J7VcS3KL7n/a+aA=
|
||||
github.com/gobuffalo/release v1.0.54/go.mod h1:Pe5/RxRa/BE8whDpGfRqSI7D1a0evGK1T4JDm339tJc=
|
||||
github.com/gobuffalo/release v1.0.61/go.mod h1:mfIO38ujUNVDlBziIYqXquYfBF+8FDHUjKZgYC1Hj24=
|
||||
github.com/gobuffalo/release v1.0.63/go.mod h1:/7hQAikt0l8Iu/tAX7slC1qiOhD6Nb+3KMmn/htiUfc=
|
||||
github.com/gobuffalo/release v1.0.72/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU=
|
||||
github.com/gobuffalo/release v1.0.74/go.mod h1:NP5NXgg/IX3M5XmHmWR99D687/3Dt9qZtTK/Lbwc1hU=
|
||||
github.com/gobuffalo/release v1.1.1/go.mod h1:Sluak1Xd6kcp6snkluR1jeXAogdJZpFFRzTYRs/2uwg=
|
||||
github.com/gobuffalo/release v1.1.3/go.mod h1:CuXc5/m+4zuq8idoDt1l4va0AXAn/OSs08uHOfMVr8E=
|
||||
github.com/gobuffalo/release v1.1.6/go.mod h1:18naWa3kBsqO0cItXZNJuefCKOENpbbUIqRL1g+p6z0=
|
||||
github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQBUL5tHPmvbA=
|
||||
github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f h1:S5EeH1reN93KR0L6TQvkRpu9YggCYXrUqFh1iEgvdC0=
|
||||
github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||
github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||
github.com/gobuffalo/tags v2.0.15+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY=
|
||||
github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||
github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||
github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE=
|
||||
github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM=
|
||||
github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
|
||||
github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
|
||||
github.com/gobuffalo/x v0.0.0-20181025165825-f204f550da9d/go.mod h1:Qh2Pb/Ak1Ko2mzHlGPigrnxkhO4WTTCI1jJM58sbgtE=
|
||||
github.com/gobuffalo/x v0.0.0-20181025192250-1ef645d63fe8/go.mod h1:AIlnMGlYXOCsoCntLPFLYtrJNS/pc2HD4IdSXH62TpU=
|
||||
github.com/gobuffalo/x v0.0.0-20181109195216-5b3131238124/go.mod h1:GpdLUY6/Ztf/3FfxfwsLkDqAGZ0brhlh7LzIibHyZp0=
|
||||
github.com/gobuffalo/x v0.0.0-20181110221217-14085ca3e1a9/go.mod h1:ig5vdn4+5IPtxgESlZWo1SSDyHKKef8EjVVKhY9kkIQ=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/go v0.0.0-20181209142756-11ce6eabd607 h1:JXKFLjiDtu/VfNLWGhu5G/MGmn/sqEuw6+wDyD9pnvY=
|
||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/certificate-transparency-go v1.0.20 h1:azETE79toaBOyp+StoEBy8atzQujL0PyBPEmsEeDCXI=
|
||||
github.com/google/certificate-transparency-go v1.0.20/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.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.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.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.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.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=
|
||||
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
|
||||
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
|
||||
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/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.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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
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 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
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/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||
github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||
github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
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.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.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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.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-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
|
||||
github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
|
||||
github.com/markbates/going v1.0.2/go.mod h1:UWCk3zm0UKefHZ7l8BNqi26UyiEMniznk8naLdTcy6c=
|
||||
github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
|
||||
github.com/markbates/grift v1.0.4/go.mod h1:wbmtW74veyx+cgfwFhlnnMWqhoz55rnHR47oMXzsyVs=
|
||||
github.com/markbates/grift v1.0.5/go.mod h1:EHmVIjOQoj/OOBDzlZ8RW0ZkvOtQ4xRHjrPvmfoiFaU=
|
||||
github.com/markbates/hmax v1.0.0/go.mod h1:cOkR9dktiESxIMu+65oc/r/bdY4bE8zZw3OLhLx0X2c=
|
||||
github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88=
|
||||
github.com/markbates/inflect v1.0.1/go.mod h1:uv3UVNBe5qBIfCm8O8Q+DW+S1EopeyINj+Ikhc7rnCk=
|
||||
github.com/markbates/inflect v1.0.3/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs=
|
||||
github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs=
|
||||
github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/refresh v1.4.10/go.mod h1:NDPHvotuZmTmesXxr95C9bjlw1/0frJwtME2dzcVKhc=
|
||||
github.com/markbates/refresh v1.4.11/go.mod h1:awpJuyo4zgexB/JaHfmBX0sRdvOjo2dXwIayWIz9i3g=
|
||||
github.com/markbates/refresh v1.5.0/go.mod h1:ZYMLkxV+x7wXQ2Xd7bXAPyF0EXiEWAMfiy/4URYb1+M=
|
||||
github.com/markbates/refresh v1.6.0/go.mod h1:p8jWGABFUaFf/cSw0pxbo0MQVujiz5NTQ0bmCHLC4ac=
|
||||
github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
|
||||
github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204022431-4c0c5c22c37e h1:wJFQrIkhbw1j3d+nOLUhGAciBnK6nD7Uhk4aaiTKSSE=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204022431-4c0c5c22c37e/go.mod h1:8/2Amku7IP+NNr3JUnduD8QjOK9mdpf+pj2fV8D4Hmw=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204022916-8c056deefc42 h1:1AyLGyOEvYWBNqo8Axma+yOkm0dCliJfso8NmDpdLzM=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204022916-8c056deefc42/go.mod h1:OllM1HVcFzek2effrrgC8BLKfG73mSKfrh41kEuhvMk=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204024002-84343d2c0eda h1:ykEyzlC7E/TOOIUdj8cmpDF2yb6bhzPK7S0Ee2Z+vSI=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204024002-84343d2c0eda/go.mod h1:OllM1HVcFzek2effrrgC8BLKfG73mSKfrh41kEuhvMk=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204025501-38c0b18f1edd h1:knS7/g8ffqSDiRaPlgb/5AnPPO26Z8emO9CC9OOlntg=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204025501-38c0b18f1edd/go.mod h1:OllM1HVcFzek2effrrgC8BLKfG73mSKfrh41kEuhvMk=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204030159-200d6ab398a9 h1:pPFPrmHOzd64dzF8kONrA41LvwzOf94RSLUku3cWsto=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204030159-200d6ab398a9/go.mod h1:OllM1HVcFzek2effrrgC8BLKfG73mSKfrh41kEuhvMk=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204031235-fc4ac9154422 h1:LduQ+iywUCm8L3S4BGs775iJSsB83pLfxF4eOT8kw34=
|
||||
github.com/mcluseau/go-swagger-ui v0.0.0-20190204031235-fc4ac9154422/go.mod h1:OllM1HVcFzek2effrrgC8BLKfG73mSKfrh41kEuhvMk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mailru/easyjson v0.0.0-20180323154445-8b799c424f57/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
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=
|
||||
github.com/miolini/datacounter v1.0.3/go.mod h1:C45dc2hBumHjDpEU64IqPwR6TDyPVpzOqqRTN7zmBUA=
|
||||
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.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||
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/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
|
||||
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.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
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.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
|
||||
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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
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.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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go v0.0.0-20181202030253-914043390fc6/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go v0.0.0-20181215222900-0143a8f55f04/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go v0.0.0-20190121191506-3fef8c783dec/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
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/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=
|
||||
github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
|
||||
github.com/spf13/viper v1.3.0/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/src-d/go-git v4.7.0+incompatible h1:IYSSnbAHeKmsfbQFi9ozbid+KNh0bKjlorMfQehQbcE=
|
||||
github.com/src-d/go-git v4.7.0+incompatible/go.mod h1:1bQciz+hn0jzPQNsYj0hDFZHLJBdV7gXE2mWhC7EkFk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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.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/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v0.0.0-20190128213124-ee1426cffec0/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
||||
github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||
github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||
github.com/unrolled/secure v0.0.0-20181022170031-4b6b7cf51606/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||
github.com/unrolled/secure v0.0.0-20190103195806-76e6d4e9b90c/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
|
||||
github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro=
|
||||
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8 h1:h7zdf0RiEvWbYBKIx4b+q41xoUVnMmvsGZnIVE5syG8=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190122013713-64072686203f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190129210102-ccddf3741a0c h1:MWY7h75sb9ioBR+s5Zgq1JYXxhbZvrSP2okwLi3ItmI=
|
||||
golang.org/x/crypto v0.0.0-20190129210102-ccddf3741a0c/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180921163948-d47a0f339242/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181019084534-8f1d3d21f81b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181022134430-8a28ead16f52/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181024145615-5cd93ef61a7c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181025063200-d989b31c8746/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026064943-731415f00dce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181106135930-3a76605856fd/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
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.8.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-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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181210030007-2a47403f2ae5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181213150753-586ba8c9bb14/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c h1:YeMXU0KQqExdpG959DFhAhfpY8myIsnfqj8lhNFRzzE=
|
||||
golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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-20181003024731-2f84ea8ef872/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181006002542-f60d9635b16a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181013182035-5e66757b835f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181019005945-6adeb8aab2de/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030151751-bb28844c46df/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181102223251-96e9e165b75e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181109152631-138c20b93253/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181109202920-92d8274bd7b8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181111003725-6d71ab8aade0/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181119130350-139d099f6620/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181127195227-b4e97c0ed882/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181201035826-d0ca3933b724/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181203210056-e5f3ab76ea4b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181205224935-3576414c54a4/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181206194817-bcd4e47d0288/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181207183836-8bc39b988060/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181207222222-4c874b978acb/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181210202511-1d424dbce8dd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181213190329-bbccd8cae4a9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190102213336-ca9055ed7d04/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190122202912-9c309ee22fab/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190124004107-78ee07aa9465/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190130015043-a06a922acc1b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190131142011-8dbcc66f33bb/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190202235157-7414d4c1f71c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
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-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=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.10.0 h1:NWjTJTQnk8UpIGlssuefyDZ6JruEjo5s88vm88uASbw=
|
||||
gopkg.in/src-d/go-git.v4 v4.10.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
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 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/apimachinery v0.0.0-20180808233214-a7b7e4de06b2 h1:w/5Dvht2Wf4vRaXFDb4cWInymI8NkaMJtrs8UsJyZLI=
|
||||
k8s.io/apimachinery v0.0.0-20180808233214-a7b7e4de06b2/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/apimachinery v0.0.0-20181207080347-f1a02064268b h1:NJFXh9cP1kqYx/N6RWK070lDco+UEChRMHlR62deTKI=
|
||||
k8s.io/apimachinery v0.0.0-20181207080347-f1a02064268b/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee h1:3MH/wGFP+9PjyLIMnPN2GYatdJosd+5TnSO2BzQqqo4=
|
||||
k8s.io/apimachinery v0.0.0-20190119020841-d41becfba9ee/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/apimachinery v0.0.0-20190126155707-0e6dcdd1b5ce h1:CzCioXa348Dzt9UuYrvxRrLGpL1ZV50QpyBHt01SYj4=
|
||||
k8s.io/apimachinery v0.0.0-20190126155707-0e6dcdd1b5ce/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/apimachinery v0.0.0-20190201131811-df262fa1a1ba h1:HEhywVhwcfpe9vpG7nc3wxA/YG6pb1W9zkvmFxs+320=
|
||||
k8s.io/apimachinery v0.0.0-20190201131811-df262fa1a1ba/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
novit.nc/direktil/pkg v0.0.0-20180707011528-e82b59c0324d h1:DMk2j7F+mXAQdZD5ouIxWVosY55RmpRevgCWSikRRzU=
|
||||
novit.nc/direktil/pkg v0.0.0-20180707011528-e82b59c0324d/go.mod h1:rbcL+fuxazzipTdJV8t9MW39YsdaK3pSvvhTdI9SXsc=
|
||||
novit.nc/direktil/pkg v0.0.0-20181210105214-68c87509f2eb h1:TP4vRMGiZDsL82xUmbV1cI2+mxZ2BV/3Zvl56sc/vvk=
|
||||
novit.nc/direktil/pkg v0.0.0-20181210105214-68c87509f2eb/go.mod h1:z5JgQ2ybqxBC1ZE5xC9FgH4rE9whqa7Gft+iP9J9jzo=
|
||||
novit.nc/direktil/pkg v0.0.0-20181210211743-9dc80cd34b09 h1:Y5GRTymITxgwaV5JVqKaxZ8U9qbLo+9jdhsGHxf/K2E=
|
||||
novit.nc/direktil/pkg v0.0.0-20181210211743-9dc80cd34b09/go.mod h1:z5JgQ2ybqxBC1ZE5xC9FgH4rE9whqa7Gft+iP9J9jzo=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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-20260125193049-56f78e083a84 h1:eqLPaRpVth1WgdvprKKtc4CVF13dkxuKbo7bLzlYG6s=
|
||||
novit.tech/direktil/pkg v0.0.0-20260125193049-56f78e083a84/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10=
|
||||
|
||||
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
|
||||
6
html/html.go
Normal file
6
html/html.go
Normal file
@ -0,0 +1,6 @@
|
||||
package dlshtml
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed ui
|
||||
var FS embed.FS
|
||||
131
html/ui/Cluster-703dcdca97841304.js
Normal file
131
html/ui/Cluster-703dcdca97841304.js
Normal file
@ -0,0 +1,131 @@
|
||||
const Cluster = {
|
||||
components: { Downloads, GetCopy },
|
||||
props: [ 'cluster', 'token', 'state' ],
|
||||
data() {
|
||||
return {
|
||||
signReqValidity: "1d",
|
||||
sshSignReq: {
|
||||
PubKey: "",
|
||||
Principal: "root",
|
||||
},
|
||||
sshUserCert: null,
|
||||
kubeSignReq: {
|
||||
CSR: "",
|
||||
User: "",
|
||||
Group: "system:masters",
|
||||
},
|
||||
kubeUserCert: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sshCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
||||
} else {
|
||||
resp.json().then((resp) => alert('failed to sign: '+resp.message))
|
||||
}
|
||||
})
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
kubeCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
|
||||
} else {
|
||||
resp.json().then((resp) => alert('failed to sign: '+resp.message))
|
||||
}
|
||||
})
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
readFile(e, onload) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) { return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => { onload(reader.result) };
|
||||
reader.onerror = () => { alert("error reading file"); };
|
||||
reader.readAsText(file);
|
||||
},
|
||||
loadPubKey(e) {
|
||||
this.readFile(e, (v) => {
|
||||
this.sshSignReq.PubKey = v;
|
||||
});
|
||||
},
|
||||
loadCSR(e) {
|
||||
this.readFile(e, (v) => {
|
||||
this.kubeSignReq.CSR = v;
|
||||
});
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<h3>Access</h3>
|
||||
|
||||
<p>Allow cluster access from a public key</p>
|
||||
|
||||
<h4>Grant SSH access</h4>
|
||||
|
||||
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
|
||||
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
|
||||
<p>Public key (OpenSSH format):<br/>
|
||||
<span class="text-and-file"><textarea v-model="sshSignReq.PubKey" style="height:3lh"></textarea>
|
||||
<input type="file" accept=".pub" @change="loadPubKey" /></span>
|
||||
</p>
|
||||
|
||||
<p><button @click="sshCASign">Sign SSH access</button>
|
||||
<template v-if="sshUserCert">
|
||||
=> <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<h4>Grant Kubernetes API access</h4>
|
||||
|
||||
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
|
||||
<p>User: <input type="text" v-model="kubeSignReq.User"/> (by default, from the CSR)</p>
|
||||
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
|
||||
<p>Certificate signing request (PEM format):<br/>
|
||||
<span class="text-and-file"><textarea v-model="kubeSignReq.CSR" style="height:7lh;"></textarea>
|
||||
<input type="file" accept=".csr" @change="loadCSR" /></span>
|
||||
</p>
|
||||
|
||||
<p><button @click="kubeCASign">Sign Kubernetes API access</button>
|
||||
<template v-if="kubeUserCert">
|
||||
=> <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<h3>Tokens</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
|
||||
</section>
|
||||
|
||||
<h3>Passwords</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Passwords" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/passwords/'+n" />
|
||||
</section>
|
||||
|
||||
<h3>Downloads</h3>
|
||||
<Downloads :token="token" :state="state" kind="cluster" :name="cluster.Name" />
|
||||
|
||||
<h3>CAs</h3>
|
||||
<table><tr><th>Name</th><th>Certificate</th><th>Signed certificates</th></tr>
|
||||
<tr v-for="ca in cluster.CAs">
|
||||
<td>{{ ca.Name }}</td>
|
||||
<td><GetCopy :token="token" name="cert" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/certificate'" /></td>
|
||||
<td><template v-for="signed in ca.Signed">
|
||||
{{" "}}
|
||||
<GetCopy :token="token" :name="signed" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/signed?name='+signed" />
|
||||
</template></td>
|
||||
</tr></table>
|
||||
|
||||
`
|
||||
}
|
||||
70
html/ui/Downloads-c9374f19f52c46d.js
Normal file
70
html/ui/Downloads-c9374f19f52c46d.js
Normal file
@ -0,0 +1,70 @@
|
||||
const Downloads = {
|
||||
props: [ 'kind', 'name', 'token', 'state' ],
|
||||
data() {
|
||||
return { createDisabled: false, selectedAssets: {} }
|
||||
},
|
||||
computed: {
|
||||
availableAssets() {
|
||||
return {
|
||||
cluster: ['addons'],
|
||||
host: [
|
||||
"kernel",
|
||||
"initrd",
|
||||
"bootstrap.tar",
|
||||
"boot.img.lz4",
|
||||
"boot.img.gz",
|
||||
"boot.qcow2",
|
||||
"boot.vmdk",
|
||||
"boot.img",
|
||||
"boot.iso",
|
||||
"boot.tar",
|
||||
"boot-efi.tar",
|
||||
"config",
|
||||
"bootstrap-config",
|
||||
"ipxe",
|
||||
],
|
||||
}[this.kind]
|
||||
},
|
||||
downloads() {
|
||||
return Object.entries(this.state.Downloads)
|
||||
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
||||
.map(e => {
|
||||
const token= e[0];
|
||||
return {
|
||||
text: token.substring(0, 5) + '...',
|
||||
url: '/public/downloads/'+token+"/",
|
||||
}
|
||||
})
|
||||
},
|
||||
assets() {
|
||||
return this.availableAssets.filter(a => this.selectedAssets[a])
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
createToken() {
|
||||
event.preventDefault()
|
||||
this.createDisabled = true
|
||||
|
||||
fetch('/authorize-download', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({Kind: this.kind, Name: this.name, Assets: this.assets}),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => resp.json())
|
||||
.then((token) => { this.selectedAssets = {}; this.createDisabled = false })
|
||||
.catch((e) => { alert('failed to create link'); this.createDisabled = false })
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<h4>Available assets</h4>
|
||||
<p class="downloads">
|
||||
<template v-for="asset in availableAssets">
|
||||
<label :class="{selected: selectedAssets[asset]}"><input type="checkbox" v-model="selectedAssets[asset]" /> {{ 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>`
|
||||
}
|
||||
32
html/ui/GetCopy-7e3c9678f9647d40.js
Normal file
32
html/ui/GetCopy-7e3c9678f9647d40.js
Normal file
@ -0,0 +1,32 @@
|
||||
const GetCopy = {
|
||||
props: [ 'name', 'href', 'token' ],
|
||||
data() { return {showCopied: false} },
|
||||
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a> <a href="#" class="copy" @click="fetchAndCopy()">🗐</a></span>`,
|
||||
methods: {
|
||||
fetch() {
|
||||
event.preventDefault()
|
||||
return fetch(this.href, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token },
|
||||
})
|
||||
},
|
||||
handleFetchError(e) {
|
||||
console.log("failed to get value:", e)
|
||||
alert('failed to get value')
|
||||
},
|
||||
fetchAndSave() {
|
||||
this.fetch().then(resp => resp.blob()).then((value) => {
|
||||
window.open(URL.createObjectURL(value), "_blank")
|
||||
}).catch(this.handleFetchError)
|
||||
},
|
||||
fetchAndCopy() {
|
||||
this.fetch()
|
||||
.then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text())
|
||||
.then((value) => {
|
||||
window.navigator.clipboard.writeText(value)
|
||||
this.showCopied = true
|
||||
setTimeout(() => { this.showCopied = false }, 1000)
|
||||
}).catch(this.handleFetchError)
|
||||
},
|
||||
},
|
||||
}
|
||||
14
html/ui/Host-61916516a854adff.js
Normal file
14
html/ui/Host-61916516a854adff.js
Normal file
@ -0,0 +1,14 @@
|
||||
const Host = {
|
||||
components: { Downloads },
|
||||
props: [ 'host', 'token', 'state' ],
|
||||
template: `
|
||||
<p>Cluster: {{ host.Cluster }}<template v-if="host.Template"> ({{ host.Template }})</template></p>
|
||||
<p>IPs:
|
||||
<code v-for="ip in host.IPs">
|
||||
{{ ip }}{{" "}}
|
||||
</code>
|
||||
</p>
|
||||
<h3>Downloads</h3>
|
||||
<Downloads :token="token" :state="state" kind="host" :name="host.Name" />
|
||||
`
|
||||
}
|
||||
267
html/ui/app-7f4645e84eaae7ce.js
Normal file
267
html/ui/app-7f4645e84eaae7ce.js
Normal file
@ -0,0 +1,267 @@
|
||||
|
||||
import { createApp } from './vue.esm-browser.js';
|
||||
|
||||
createApp({
|
||||
components: { Cluster, Host },
|
||||
data() {
|
||||
return {
|
||||
forms: {
|
||||
store: {},
|
||||
storeUpload: {},
|
||||
delKey: {},
|
||||
hostFromTemplate: {},
|
||||
hostFromTemplateDel: "",
|
||||
},
|
||||
view: "",
|
||||
viewFilter: "",
|
||||
session: {},
|
||||
error: null,
|
||||
publicState: null,
|
||||
serverVersion: null,
|
||||
uiHash: null,
|
||||
watchingState: false,
|
||||
state: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.session = JSON.parse(sessionStorage.state || "{}")
|
||||
this.watchPublicState()
|
||||
},
|
||||
watch: {
|
||||
session: {
|
||||
deep: true,
|
||||
handler(v) {
|
||||
sessionStorage.state = JSON.stringify(v)
|
||||
|
||||
if (v.token && !this.watchingState) {
|
||||
this.watchState()
|
||||
this.watchingState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
publicState: {
|
||||
deep: true,
|
||||
handler(v) {
|
||||
if (v) {
|
||||
this.serverVersion = v.ServerVersion
|
||||
if (this.uiHash && v.UIHash != this.uiHash) {
|
||||
console.log("reloading")
|
||||
location.reload()
|
||||
} else {
|
||||
this.uiHash = v.UIHash
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
views() {
|
||||
var views = [{type: "actions", name: "admin", title: "Admin actions"}];
|
||||
|
||||
(this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`}));
|
||||
(this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`}));
|
||||
|
||||
return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter));
|
||||
},
|
||||
viewObj() {
|
||||
if (this.view) {
|
||||
if (this.view.type == "cluster") {
|
||||
return this.state.Clusters.find((c) => c.Name == this.view.name);
|
||||
}
|
||||
if (this.view.type == "host") {
|
||||
return this.state.Hosts.find((h) => h.Name == this.view.name);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
hostsFromTemplate() {
|
||||
return (this.state.Hosts||[]).filter((h) => h.Template);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
any(array) {
|
||||
return array && array.length != 0;
|
||||
},
|
||||
copyText(text) {
|
||||
event.preventDefault()
|
||||
window.navigator.clipboard.writeText(text)
|
||||
},
|
||||
setToken() {
|
||||
event.preventDefault()
|
||||
this.session.token = this.forms.setToken
|
||||
this.forms.setToken = null
|
||||
},
|
||||
uploadStore() {
|
||||
event.preventDefault()
|
||||
this.apiPost('/public/store.tar', this.$refs.storeUpload.files[0], (v) => {
|
||||
this.forms.store = {}
|
||||
}, "application/tar")
|
||||
},
|
||||
namedPassphrase(name, passphrase) {
|
||||
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||
},
|
||||
storeAddKey() {
|
||||
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
||||
this.forms.store = {}
|
||||
})
|
||||
},
|
||||
storeDelKey() {
|
||||
event.preventDefault()
|
||||
|
||||
let name = this.forms.delKey.name
|
||||
|
||||
if (!confirm("Remove key named "+JSON.stringify(name)+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiPost('/store/delete-key', name , (v) => {
|
||||
this.forms.delKey = {}
|
||||
})
|
||||
},
|
||||
unlockStore() {
|
||||
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
|
||||
this.forms.store = {}
|
||||
|
||||
if (v) {
|
||||
this.session.token = v
|
||||
if (!this.watchingState) {
|
||||
this.watchState()
|
||||
this.watchingState = true
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadConfig() {
|
||||
const file = this.$refs.configUpload.files[0];
|
||||
let contentType = "text/vnd.yaml";
|
||||
if (file.name.endsWith(".json")) {
|
||||
contentType = "application/json";
|
||||
}
|
||||
this.apiPost('/configs', file, (v) => {}, contentType, true)
|
||||
},
|
||||
hostFromTemplateAdd() {
|
||||
let v = this.forms.hostFromTemplate;
|
||||
this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} });
|
||||
},
|
||||
hostFromTemplateDel() {
|
||||
event.preventDefault()
|
||||
|
||||
let v = this.forms.hostFromTemplateDel;
|
||||
if (!confirm("delete host template instance "+v+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
|
||||
},
|
||||
apiPost(action, data, onload, contentType = 'application/json', raw = false) {
|
||||
event.preventDefault()
|
||||
|
||||
if (data === undefined) {
|
||||
throw("action " + action + ": no data")
|
||||
}
|
||||
|
||||
/* TODO
|
||||
fetch(action, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => onload)
|
||||
// */
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.responseType = 'json'
|
||||
// TODO spinner, pending action notification, or something
|
||||
xhr.onerror = () => {
|
||||
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
|
||||
}
|
||||
xhr.onload = (r) => {
|
||||
if (xhr.status != 200) {
|
||||
this.error = xhr.response
|
||||
return
|
||||
}
|
||||
// this.actionResults.splice(idx, 1, {...item, done: true, resp: xhr.responseText})
|
||||
this.error = null
|
||||
if (onload) {
|
||||
onload(xhr.response)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open("POST", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
xhr.setRequestHeader('Content-Type', contentType)
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
|
||||
if (!raw && contentType == "application/json") {
|
||||
xhr.send(JSON.stringify(data))
|
||||
} else {
|
||||
xhr.send(data)
|
||||
}
|
||||
},
|
||||
apiDelete(action, data, onload) {
|
||||
event.preventDefault()
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onload = (r) => {
|
||||
if (xhr.status != 200) {
|
||||
this.error = xhr.response
|
||||
return
|
||||
}
|
||||
this.error = null
|
||||
if (onload) {
|
||||
onload(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.open("DELETE", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
xhr.send()
|
||||
},
|
||||
download(url) {
|
||||
event.target.target = '_blank'
|
||||
event.target.href = this.downloadLink(url)
|
||||
},
|
||||
downloadLink(url) {
|
||||
// TODO once-shot download link
|
||||
return url + '?token=' + this.session.token
|
||||
},
|
||||
watchPublicState() {
|
||||
this.watchStream('publicState', '/public-state')
|
||||
},
|
||||
watchState() {
|
||||
this.watchStream('state', '/state', true)
|
||||
},
|
||||
watchStream(field, path, withToken) {
|
||||
let evtSrc = new EventSource(path + (withToken ? '?token='+this.session.token : ''));
|
||||
evtSrc.onmessage = (e) => {
|
||||
let update = JSON.parse(e.data)
|
||||
|
||||
console.log("watch "+path+":", update)
|
||||
|
||||
if (update.err) {
|
||||
console.log("watch error from server:", err)
|
||||
}
|
||||
if (update.set) {
|
||||
this[field] = update.set
|
||||
}
|
||||
if (update.p) { // patch
|
||||
new jsonpatch.JSONPatch(update.p, true).apply(this[field])
|
||||
}
|
||||
}
|
||||
evtSrc.onerror = (e) => {
|
||||
// console.log("event source " + path + " error:", e)
|
||||
if (evtSrc) evtSrc.close()
|
||||
|
||||
this[field] = null
|
||||
|
||||
window.setTimeout(() => { this.watchStream(field, path, withToken) }, 1000)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
}).mount('#app');
|
||||
42
html/ui/app.css
Normal file
42
html/ui/app.css
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
.view-links > span {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1pt solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.downloads, .download-links {
|
||||
& > * {
|
||||
display: inline-block;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1px solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.downloads, .view-links {
|
||||
& > .selected {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
.text-and-file {
|
||||
position:relative;
|
||||
|
||||
textarea {
|
||||
width: 64em;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
position:absolute;
|
||||
bottom:0;right:0;
|
||||
}
|
||||
}
|
||||
BIN
html/ui/favicon.ico
Normal file
BIN
html/ui/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
145
html/ui/index.html
Normal file
145
html/ui/index.html
Normal file
@ -0,0 +1,145 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Direktil Local Server</title>
|
||||
<base href="/ui/" />
|
||||
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
|
||||
|
||||
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
||||
<script src="/ui/jsonpatch.min-942279a1c4209cc2.js" integrity="sha384-GcPrkRS12jtrElEkbJcrZ8asvvb9s3mc+CUq9UW/8bL4L0bpkmh9M+oFnWN6qLq2"></script>
|
||||
<script src="/ui/app-7f4645e84eaae7ce.js" defer integrity="sha384-31uiQnXVSPLDq61o+chfyKRkSdkmzv023M7KafOo+0xRw5AM70BUFyYO6yo0kPiZ" type="module"></script>
|
||||
<script src="/ui/Downloads-c9374f19f52c46d.js" integrity="sha384-WQIkCSxlUkvu4jFIWwS3bESMaazGwwnBn9dyvB7nItz3L8BmusRMsJACzfJa7sC4"></script>
|
||||
<script src="/ui/GetCopy-7e3c9678f9647d40.js" integrity="sha384-LzxUXylxE/t25HyTch8y2qvKcehvP2nqCo37swIBGEKZZUzHVJVQrS5UJDWNskTA"></script>
|
||||
<script src="/ui/Cluster-703dcdca97841304.js" integrity="sha384-ifGpq/GB1nDfqczm5clTRtcfnc9UMkT+SptMyS3UG9Di3xoKORtOGGjmK5RnJx+v"></script>
|
||||
<script src="/ui/Host-61916516a854adff.js" integrity="sha384-/wh3KrC0sb4MT7ekO2U84rswxI42WSH/0jB4dbDaaGaGhX60xTEZHFsdQAf7UgTG"></script>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<header>
|
||||
<div id="logo">
|
||||
<img src="favicon.ico" />
|
||||
<span>Direktil Local Server</span>
|
||||
</div>
|
||||
<div class="utils">
|
||||
<span id="login-hdr" v-if="session.token">
|
||||
Logged in
|
||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||
</span>
|
||||
|
||||
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
|
||||
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="error" v-if="error">
|
||||
<button class="btn-close" @click="error=null">×</button>
|
||||
<div class="code" v-if="error.code">{{ error.code }}</div>
|
||||
<div class="message">{{ error.message }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!publicState">
|
||||
<p>Not connected.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="publicState.Store.New">
|
||||
<p>Store is new.</p>
|
||||
<p>Option 1: initialize a new store</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="text" v-model="forms.store.name" name="name" placeholder="Name" /><br/>
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" required placeholder="Passphrase" />
|
||||
<input type="password" v-model="forms.store.pass2" required placeholder="Passphrase confirmation" />
|
||||
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||
</form>
|
||||
<p>Option 2: upload a previously downloaded store</p>
|
||||
<form @submit="uploadStore">
|
||||
<input type="file" ref="storeUpload" />
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else-if="!publicState.Store.Open">
|
||||
<p>Store is not open.</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else-if="!state">
|
||||
<p v-if="!session.token">Not logged in.</p>
|
||||
<p v-else>Invalid token</p>
|
||||
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="log in"/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div style="float:right;"><input type="search" placeholder="Filter" v-model="viewFilter"/></div>
|
||||
<p class="view-links"><span v-for="v in views" @click="view = v" :class="{selected: view.type==v.type && view.name==v.name}">{{v.title}}</span></p>
|
||||
|
||||
<h2 v-if="view">{{view.title}}</h2>
|
||||
|
||||
<div v-if="view.type == 'cluster'" id="clusters">
|
||||
<Cluster :cluster="viewObj" :token="session.token" :state="state" />
|
||||
</div>
|
||||
|
||||
<div v-if="view.type == 'host'" id="hosts">
|
||||
<Host :host="viewObj" :token="session.token" :state="state" />
|
||||
</div>
|
||||
|
||||
<div v-if="view.type == 'actions' && view.name == 'admin'">
|
||||
<h3>Config</h3>
|
||||
<form @submit="uploadConfig">
|
||||
<input type="file" ref="configUpload" required />
|
||||
<input type="submit" value="upload config" />
|
||||
</form>
|
||||
|
||||
<h3>Store</h3>
|
||||
<p><a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">Download</a></p>
|
||||
<form @submit="storeAddKey" action="/store/add-key">
|
||||
<p>Add an unlock phrase:</p>
|
||||
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||
</form>
|
||||
<form @submit="storeDelKey" action="/store/delete-key">
|
||||
<p>Remove an unlock phrase:</p>
|
||||
<input type="text" v-model="forms.delKey.name" name="name" required placeholder="Name" />
|
||||
<input type="submit" value="remove unlock phrase" />
|
||||
|
||||
<p v-if="state.Store.KeyNames">Available names:
|
||||
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
||||
</form>
|
||||
|
||||
<template v-if="any(state.HostTemplates) || any(hostsFromTemplate)">
|
||||
<h3>Hosts from template</h3>
|
||||
<form @submit="hostFromTemplateAdd" action="" v-if="any(state.HostTemplates)">
|
||||
<p>Add a host from template instance:</p>
|
||||
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
||||
<select v-model="forms.hostFromTemplate.Template" required>
|
||||
<option v-for="name in state.HostTemplates" :value="name">{{name}}</option>
|
||||
</select>
|
||||
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
||||
<input type="submit" value="add instance" />
|
||||
</form>
|
||||
<form @submit="hostFromTemplateDel" action="" v-if="any(hostsFromTemplate)">
|
||||
<p>Remove a host from template instance:</p>
|
||||
<select v-model="forms.hostFromTemplateDel" required>
|
||||
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
||||
</select>
|
||||
<input type="submit" value="delete instance" :disabled="!forms.hostFromTemplateDel" />
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
html/ui/jsonpatch.min-942279a1c4209cc2.js
Normal file
36
html/ui/jsonpatch.min-942279a1c4209cc2.js
Normal file
File diff suppressed because one or more lines are too long
195
html/ui/style.css
Normal file
195
html/ui/style.css
Normal file
@ -0,0 +1,195 @@
|
||||
:root {
|
||||
--bg: #eee;
|
||||
--color: black;
|
||||
--bevel-dark: darkgray;
|
||||
--bevel-light: lightgray;
|
||||
--link: blue;
|
||||
--input-bg: #ddd;
|
||||
--input-text: white;
|
||||
--btn-bg: #eee;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: black;
|
||||
--color: orange;
|
||||
--bevel-dark: #402900;
|
||||
--bevel-light: #805300;
|
||||
--link: #31b0fa;
|
||||
--input-bg: #111;
|
||||
--input-text: #ddd;
|
||||
--btn-bg: #222;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: var(--link);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border-left: dotted 1pt;
|
||||
border-right: dotted 1pt;
|
||||
border-bottom: dotted 1pt;
|
||||
padding: 2pt 4pt;
|
||||
}
|
||||
tr:first-child > th {
|
||||
border-top: dotted 1pt;
|
||||
}
|
||||
th, tr:last-child > td {
|
||||
border-bottom: solid 1pt;
|
||||
}
|
||||
|
||||
.flat > * { margin-left: 1ex; }
|
||||
.flat > *:first-child { margin-left: 0; }
|
||||
|
||||
.green { color: green; }
|
||||
.red { color: red; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.red { color: #c00; }
|
||||
}
|
||||
|
||||
textarea, select, input {
|
||||
background: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
border: solid 1pt;
|
||||
border-color: var(--bevel-light);
|
||||
border-top-color: var(--bevel-dark);
|
||||
border-left-color: var(--bevel-dark);
|
||||
margin: 1pt;
|
||||
|
||||
&:focus {
|
||||
outline: solid 1pt var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
button, input[type=button], input[type=submit], ::file-selector-button {
|
||||
background: var(--btn-bg);
|
||||
color: var(--color);
|
||||
border: solid 2pt;
|
||||
border-color: var(--bevel-dark);
|
||||
border-top-color: var(--bevel-light);
|
||||
border-left-color: var(--bevel-light);
|
||||
|
||||
&:hover {
|
||||
background: var(--bevel-dark);
|
||||
}
|
||||
&:active {
|
||||
background: var(--bevel-dark);
|
||||
border-color: var(--bevel-light);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2pt solid;
|
||||
margin: 0 0 1em 0;
|
||||
padding: 1ex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#logo > img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
header .utils > * {
|
||||
margin-left: 1ex;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: rgba(255,0,0,0.2);
|
||||
border: 1pt solid red;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.error .btn-close,
|
||||
.error .code {
|
||||
background: #600;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
align-self: stretch;
|
||||
padding: 1ex 1em;
|
||||
}
|
||||
.error .code {
|
||||
order: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.error .message {
|
||||
order: 2;
|
||||
padding: 1ex 2em;
|
||||
}
|
||||
.error .btn-close {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.sheets {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.sheets > div {
|
||||
margin: 0 1ex;
|
||||
border: 1pt solid;
|
||||
border-radius: 6pt;
|
||||
}
|
||||
.sheets .title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
padding: 2pt 6pt;
|
||||
background: rgba(127,127,127,0.5);
|
||||
}
|
||||
.sheets .section {
|
||||
padding: 2pt 6pt 2pt 6pt;
|
||||
font-weight: bold;
|
||||
border-top: 1px dotted;
|
||||
}
|
||||
.sheets section {
|
||||
margin: 2pt 6pt 6pt 6pt;
|
||||
}
|
||||
.sheets > *:last-child > table:last-child > tr:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notif {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.notif > div:first-child {
|
||||
position: absolute;
|
||||
min-width: 100%; height: 100%;
|
||||
background: white;
|
||||
opacity: 75%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.links > * { margin-left: 1ex; }
|
||||
.links > *:first-child { margin-left: 0; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.notif > div:first-child {
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
|
||||
.copy { font-size: small; }
|
||||
16172
html/ui/vue.esm-browser.js
Normal file
16172
html/ui/vue.esm-browser.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,23 @@
|
||||
#! /bin/sh
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "USAGE: $0 <device> <base url>"
|
||||
echo "USAGE: $0 <device>"
|
||||
fi
|
||||
|
||||
dev=$1
|
||||
base_url=$2
|
||||
|
||||
: ${MP:=/mnt}
|
||||
|
||||
set -ex
|
||||
|
||||
mkdir -p $MP
|
||||
zcat boot.img.gz | dd of=$dev
|
||||
|
||||
apk add sgdisk
|
||||
|
||||
[[ $dev =~ nvme ]] &&
|
||||
devp=${dev}p ||
|
||||
devp=${dev}
|
||||
|
||||
if vgdisplay storage; then
|
||||
# the system is already installed, just upgrade
|
||||
mount -t vfat ${devp}1 $MP
|
||||
curl ${base_url}/boot.tar |tar xv -C $MP
|
||||
umount $MP
|
||||
sgdisk --move-second-header --new=3:0:0 $dev
|
||||
|
||||
else
|
||||
sgdisk --clear $dev
|
||||
pvcreate ${devp}3
|
||||
vgcreate storage ${devp}3
|
||||
|
||||
curl ${base_url}/boot.img.lz4 |lz4cat >$dev
|
||||
|
||||
sgdisk --move-second-header --new=3:0:0 $dev
|
||||
|
||||
pvcreate ${devp}3
|
||||
vgcreate storage ${devp}3
|
||||
fi
|
||||
|
||||
while umount $MP; do true; done
|
||||
|
||||
14
modd.conf
14
modd.conf
@ -1,6 +1,12 @@
|
||||
**/*.go Dockerfile {
|
||||
modd.conf {}
|
||||
|
||||
**/*.go go.mod go.sum {
|
||||
prep: go test ./...
|
||||
prep: go install ./cmd/...
|
||||
prep: docker build -t dls .
|
||||
#daemon +sigterm: /var/lib/direktil/test-run
|
||||
prep: mkdir -p dist
|
||||
prep: hack/build ./...
|
||||
#daemon +sigterm: bash test-run
|
||||
}
|
||||
|
||||
html/**/* {
|
||||
prep: hack/build ./cmd/dkl-local-server
|
||||
}
|
||||
|
||||
@ -1,22 +1,32 @@
|
||||
package clustersconfig
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
templateDetailsDir = flag.String("template-details-dir",
|
||||
filepath.Join(os.TempDir(), "dkl-dir2config"),
|
||||
"write details of template execute in this dir")
|
||||
|
||||
templateID = 0
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Hosts []*Host
|
||||
Groups []*Group
|
||||
Clusters []*Cluster
|
||||
Configs []*Template
|
||||
StaticPods []*Template `yaml:"static_pods"`
|
||||
StaticPods map[string][]*Template `yaml:"static_pods"`
|
||||
Addons map[string][]*Template
|
||||
SSLConfig string `yaml:"ssl_config"`
|
||||
CertRequests []*CertRequest `yaml:"cert_requests"`
|
||||
@ -31,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
|
||||
}
|
||||
@ -76,15 +86,6 @@ func (c *Config) HostByMAC(mac string) *Host {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Group(name string) *Group {
|
||||
for _, group := range c.Groups {
|
||||
if group.Name == name {
|
||||
return group
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Cluster(name string) *Cluster {
|
||||
for _, cluster := range c.Clusters {
|
||||
if cluster.Name == name {
|
||||
@ -103,15 +104,6 @@ func (c *Config) ConfigTemplate(name string) *Template {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) StaticPodsTemplate(name string) *Template {
|
||||
for _, s := range c.StaticPods {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) CSR(name string) *CertRequest {
|
||||
for _, s := range c.CertRequests {
|
||||
if s.Name == name {
|
||||
@ -127,65 +119,102 @@ func (c *Config) SaveTo(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, ba, 0600)
|
||||
return os.WriteFile(path, ba, 0600)
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
Name string
|
||||
Template string
|
||||
|
||||
parsedTemplate *template.Template
|
||||
}
|
||||
|
||||
func (t *Template) Execute(wr io.Writer, data interface{}, extraFuncs map[string]interface{}) error {
|
||||
if t.parsedTemplate == nil {
|
||||
var templateFuncs = map[string]interface{}{
|
||||
"indent": func(indent, s string) (indented string) {
|
||||
indented = indent + strings.Replace(s, "\n", "\n"+indent, -1)
|
||||
return
|
||||
},
|
||||
func (t *Template) Execute(contextName, elementName string, wr io.Writer, data interface{}, extraFuncs map[string]interface{}) error {
|
||||
var templateFuncs = map[string]interface{}{
|
||||
"indent": func(indent, s string) (indented string) {
|
||||
indented = indent + strings.Replace(s, "\n", "\n"+indent, -1)
|
||||
return
|
||||
},
|
||||
"yaml": func(v any) (s string, err error) {
|
||||
ba, err := yaml.Marshal(v)
|
||||
s = string(ba)
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
for name, f := range extraFuncs {
|
||||
templateFuncs[name] = f
|
||||
}
|
||||
|
||||
tmpl, err := template.New(t.Name).
|
||||
Funcs(templateFuncs).
|
||||
Parse(t.Template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *templateDetailsDir != "" {
|
||||
templateID++
|
||||
|
||||
base := filepath.Join(*templateDetailsDir, contextName, fmt.Sprintf("%s-%03d", elementName, templateID))
|
||||
os.MkdirAll(base, 0700)
|
||||
|
||||
base += string(filepath.Separator)
|
||||
log.Print("writing template details: ", base, "{in,data,out}")
|
||||
|
||||
if err := os.WriteFile(base+"in", []byte(t.Template), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, f := range extraFuncs {
|
||||
templateFuncs[name] = f
|
||||
}
|
||||
|
||||
tmpl, err := template.New(t.Name).
|
||||
Funcs(templateFuncs).
|
||||
Parse(t.Template)
|
||||
yamlBytes, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.parsedTemplate = tmpl
|
||||
|
||||
if err := os.WriteFile(base+"data", yamlBytes, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(base + "out")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
wr = io.MultiWriter(wr, out)
|
||||
}
|
||||
|
||||
return t.parsedTemplate.Execute(wr, data)
|
||||
return tmpl.Execute(wr, data)
|
||||
}
|
||||
|
||||
// Host represents a host served by this server.
|
||||
type Host struct {
|
||||
WithRev
|
||||
Name string
|
||||
MAC string
|
||||
|
||||
Template bool `json:",omitempty"`
|
||||
|
||||
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
|
||||
Vars Vars
|
||||
}
|
||||
|
||||
// Group represents a group of hosts and provides their configuration.
|
||||
type Group struct {
|
||||
WithRev
|
||||
Name string
|
||||
Master bool
|
||||
IPXE string
|
||||
Kernel string
|
||||
Initrd string
|
||||
Config 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
|
||||
|
||||
StaticPods string `yaml:"static_pods"`
|
||||
Versions map[string]string
|
||||
Vars Vars
|
||||
|
||||
Vars Vars
|
||||
}
|
||||
|
||||
// Vars store user-defined key-values
|
||||
@ -194,13 +223,18 @@ type Vars map[string]interface{}
|
||||
// Cluster represents a cluster of hosts, allowing for cluster-wide variables.
|
||||
type Cluster struct {
|
||||
WithRev
|
||||
Name string
|
||||
|
||||
Name string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
|
||||
Domain string
|
||||
Addons string
|
||||
Addons []string
|
||||
Subnets struct {
|
||||
Services string
|
||||
Pods string
|
||||
}
|
||||
|
||||
Vars Vars
|
||||
}
|
||||
|
||||
@ -215,7 +249,7 @@ func (c *Cluster) DNSSvcIP() net.IP {
|
||||
func (c *Cluster) NthSvcIP(n byte) net.IP {
|
||||
_, cidr, err := net.ParseCIDR(c.Subnets.Services)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Invalid services CIDR: %v", err))
|
||||
panic(fmt.Errorf("invalid services CIDR: %v", err))
|
||||
}
|
||||
|
||||
ip := cidr.IP
|
||||
|
||||
@ -70,6 +70,7 @@ func (d *Defaults) Load(dir, suffix string, value Rev, data []byte) (err error)
|
||||
}
|
||||
|
||||
func (d *Defaults) Open(rev, filePath string) (rd io.Reader, err error) {
|
||||
log.Printf("openning defaults at %s:%s", rev, filePath)
|
||||
tree, err := d.treeAt(rev)
|
||||
if err != nil {
|
||||
return
|
||||
@ -94,12 +95,17 @@ func (d *Defaults) ReadAll(rev, filePath string) (ba []byte, err error) {
|
||||
}
|
||||
|
||||
func (d *Defaults) List(rev, dir string) (names []string, err error) {
|
||||
log.Printf("listing defaults at %s:%s", rev, dir)
|
||||
tree, err := d.treeAt(rev)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dirPrefix := dir + "/"
|
||||
err = tree.Files().ForEach(func(f *object.File) (err error) {
|
||||
if !strings.HasPrefix(f.Name, dirPrefix) {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(f.Name, ".yaml") {
|
||||
return
|
||||
}
|
||||
@ -126,6 +132,11 @@ func (d *Defaults) treeAt(rev string) (tree *object.Tree, err error) {
|
||||
obj, err = o.Object()
|
||||
|
||||
case *object.Commit: // commit -> tree
|
||||
msg := o.Message
|
||||
if len(msg) > 30 {
|
||||
msg = msg[:27] + "..."
|
||||
}
|
||||
log.Printf("open defaults at commit %s: %s", o.Hash.String()[:7], msg)
|
||||
return o.Tree()
|
||||
|
||||
default:
|
||||
|
||||
@ -2,8 +2,6 @@ package clustersconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -15,37 +13,38 @@ import (
|
||||
// Debug enables debug logs from this package.
|
||||
var Debug = false
|
||||
|
||||
func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
||||
if Debug {
|
||||
log.Printf("loading config from dir %s (defaults from %s)", dirPath, defaultsPath)
|
||||
}
|
||||
func FromDir(
|
||||
read func(path string) ([]byte, error),
|
||||
assemble func(path string) ([]byte, error),
|
||||
listBase func(path string) ([]string, error),
|
||||
listMerged func(path string) ([]string, error),
|
||||
) (*Config, error) {
|
||||
|
||||
defaults, err := NewDefaults(defaultsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load defaults: %v", err)
|
||||
}
|
||||
|
||||
store := &dirStore{dirPath}
|
||||
load := func(dir, name string, out Rev) error {
|
||||
ba, err := store.Get(path.Join(dir, name))
|
||||
load := func(dir, name string, out any) (err error) {
|
||||
ba, err := assemble(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load %s/%s from dir: %v", dir, name, err)
|
||||
return
|
||||
}
|
||||
if err = defaults.Load(dir, ".yaml", out, ba); err != nil {
|
||||
return fmt.Errorf("failed to enrich %s/%s from defaults: %v", dir, name, err)
|
||||
err = yaml.UnmarshalStrict(ba, out)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &Config{Addons: make(map[string][]*Template)}
|
||||
config := &Config{
|
||||
Addons: make(map[string][]*Template),
|
||||
StaticPods: make(map[string][]*Template),
|
||||
}
|
||||
|
||||
// load clusters
|
||||
names, err := store.List("clusters")
|
||||
names, err := listBase("clusters")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list clusters: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
name, _ = strings.CutSuffix(name, ".yaml")
|
||||
cluster := &Cluster{Name: name}
|
||||
if err := load("clusters", name, cluster); err != nil {
|
||||
return nil, err
|
||||
@ -54,97 +53,14 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
||||
config.Clusters = append(config.Clusters, cluster)
|
||||
}
|
||||
|
||||
// load groups
|
||||
names, err = store.List("groups")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list groups: %v", err)
|
||||
}
|
||||
|
||||
read := func(rev, filePath string) (data []byte, fromDefaults bool, err error) {
|
||||
data, err = store.Get(filePath)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("faild to read %s: %v", filePath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
return // ok
|
||||
}
|
||||
|
||||
if len(rev) == 0 {
|
||||
err = fmt.Errorf("entry not found: %s", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
data, err = defaults.ReadAll(rev, filePath+".yaml")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to read %s:%s: %v", rev, filePath, err)
|
||||
return
|
||||
}
|
||||
|
||||
fromDefaults = true
|
||||
return
|
||||
}
|
||||
|
||||
template := func(rev, dir, name string, templates *[]*Template) (ref string, err error) {
|
||||
ref = name
|
||||
if len(name) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ba, fromDefaults, err := read(rev, path.Join(dir, name))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fromDefaults {
|
||||
ref = rev + ":" + name
|
||||
}
|
||||
|
||||
if !hasTemplate(ref, *templates) {
|
||||
if Debug {
|
||||
log.Printf("new template in %s: %s", dir, ref)
|
||||
}
|
||||
|
||||
*templates = append(*templates, &Template{
|
||||
Name: ref,
|
||||
Template: string(ba),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
group := &Group{Name: name}
|
||||
if err := load("groups", name, group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.Config, err = template(group.Rev(), "configs", group.Config, &config.Configs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config for group %q: %v", name, err)
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Printf("group %q: config=%q static_pods=%q", group.Name, group.Config, group.StaticPods)
|
||||
}
|
||||
|
||||
group.StaticPods, err = template(group.Rev(), "static-pods", group.StaticPods, &config.StaticPods)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load static pods for group %q: %v", name, err)
|
||||
}
|
||||
|
||||
config.Groups = append(config.Groups, group)
|
||||
}
|
||||
|
||||
// load hosts
|
||||
names, err = store.List("hosts")
|
||||
names, err = listBase("hosts")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list hosts: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
name, _ = strings.CutSuffix(name, ".yaml")
|
||||
o := &Host{Name: name}
|
||||
if err := load("hosts", name, o); err != nil {
|
||||
return nil, err
|
||||
@ -154,28 +70,20 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
||||
}
|
||||
|
||||
// load config templates
|
||||
loadTemplates := func(rev, dir string, templates *[]*Template) error {
|
||||
names, err := store.List(dir)
|
||||
loadTemplates := func(dir string, templates *[]*Template) error {
|
||||
names, err := listMerged(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s: %v", dir, err)
|
||||
}
|
||||
|
||||
if len(rev) != 0 {
|
||||
var defaultsNames []string
|
||||
defaultsNames, err = defaults.List(rev, dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list %s:%s: %v", rev, dir, err)
|
||||
}
|
||||
for _, fullName := range names {
|
||||
name, _ := strings.CutSuffix(fullName, ".yaml")
|
||||
|
||||
names = append(names, defaultsNames...)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
if hasTemplate(name, *templates) {
|
||||
continue
|
||||
}
|
||||
|
||||
ba, _, err := read(rev, path.Join(dir, name))
|
||||
ba, err := read(path.Join(dir, fullName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -189,33 +97,57 @@ func FromDir(dirPath, defaultsPath string) (*Config, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
loadTemplates("configs", &config.Configs)
|
||||
|
||||
// cluster addons
|
||||
for _, cluster := range config.Clusters {
|
||||
addonSet := cluster.Addons
|
||||
if len(addonSet) == 0 {
|
||||
addonSets := cluster.Addons
|
||||
if len(addonSets) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := config.Addons[addonSet]; ok {
|
||||
for _, addonSet := range addonSets {
|
||||
if _, ok := config.Addons[addonSet]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
templates := make([]*Template, 0)
|
||||
if err = loadTemplates(path.Join("addons", addonSet), &templates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Addons[addonSet] = templates
|
||||
}
|
||||
}
|
||||
|
||||
// cluster static pods
|
||||
for _, host := range config.Hosts {
|
||||
bpSet := host.StaticPods
|
||||
if bpSet == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := config.StaticPods[bpSet]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
templates := make([]*Template, 0)
|
||||
if err = loadTemplates(cluster.Rev(), path.Join("addons", addonSet), &templates); err != nil {
|
||||
if err = loadTemplates(path.Join("static-pods", bpSet), &templates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Addons[addonSet] = templates
|
||||
config.StaticPods[bpSet] = templates
|
||||
}
|
||||
|
||||
// load SSL configuration
|
||||
if ba, err := ioutil.ReadFile(filepath.Join(dirPath, "ssl-config.json")); err == nil {
|
||||
if ba, err := read("ssl-config.json"); err == nil {
|
||||
config.SSLConfig = string(ba)
|
||||
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ba, err := ioutil.ReadFile(filepath.Join(dirPath, "cert-requests.yaml")); err == nil {
|
||||
if ba, err := read("cert-requests.yaml"); err == nil {
|
||||
reqs := make([]*CertRequest, 0)
|
||||
if err = yaml.Unmarshal(ba, &reqs); err != nil {
|
||||
return nil, err
|
||||
@ -245,7 +177,7 @@ type dirStore struct {
|
||||
|
||||
// listDir
|
||||
func (b *dirStore) listDir(prefix string) (subDirs []string, err error) {
|
||||
entries, err := ioutil.ReadDir(filepath.Join(b.path, prefix))
|
||||
entries, err := os.ReadDir(filepath.Join(b.path, prefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -293,7 +225,7 @@ func (b *dirStore) List(prefix string) ([]string, error) {
|
||||
|
||||
// Load is part of the DataBackend interface
|
||||
func (b *dirStore) Get(key string) (ba []byte, err error) {
|
||||
ba, err = ioutil.ReadFile(filepath.Join(b.path, filepath.Join(path.Split(key))+".yaml"))
|
||||
ba, err = os.ReadFile(filepath.Join(b.path, filepath.Join(path.Split(key))+".yaml"))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
61
pkg/initrdconfig/config.go
Normal file
61
pkg/initrdconfig/config.go
Normal file
@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
AntiPhishingCode string `json:"anti_phishing_code"`
|
||||
|
||||
Keymap string
|
||||
Modules string
|
||||
|
||||
Auths []Auth
|
||||
|
||||
Networks []struct {
|
||||
Name string
|
||||
Interfaces []struct {
|
||||
Var string
|
||||
N int
|
||||
Regexps []string
|
||||
}
|
||||
Script string
|
||||
}
|
||||
|
||||
LVM []LvmVG
|
||||
Bootstrap Bootstrap
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Name string
|
||||
SSHKey string `yaml:"sshKey"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
type LvmVG struct {
|
||||
VG string
|
||||
PVs struct {
|
||||
N int
|
||||
Regexps []string
|
||||
}
|
||||
|
||||
Defaults struct {
|
||||
FS string
|
||||
Raid *RaidConfig
|
||||
}
|
||||
|
||||
LVs []struct {
|
||||
Name string
|
||||
Crypt string
|
||||
FS string
|
||||
Raid *RaidConfig
|
||||
Size string
|
||||
Extents string
|
||||
}
|
||||
}
|
||||
|
||||
type RaidConfig struct {
|
||||
Mirrors int
|
||||
Stripes int
|
||||
}
|
||||
|
||||
type Bootstrap struct {
|
||||
Dev string
|
||||
Seed string
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
package mime
|
||||
|
||||
const (
|
||||
YAML = "text/vnd.yaml"
|
||||
TAR = "application/tar"
|
||||
DISK = "application/x-diskimage"
|
||||
ISO = "application/x-iso9660-image"
|
||||
IPXE = "text/x-ipxe"
|
||||
OCTET = "application/octet-stream"
|
||||
JSON = "application/json"
|
||||
YAML = "text/vnd.yaml"
|
||||
TAR = "application/tar"
|
||||
DISK = "application/x-diskimage"
|
||||
ISO = "application/x-iso9660-image"
|
||||
IPXE = "text/x-ipxe"
|
||||
OCTET = "application/octet-stream"
|
||||
CERT = "application/x-x509-user-cert"
|
||||
CACERT = "application/x-x509-ca-cert"
|
||||
)
|
||||
|
||||
29
pkg/utf16/utf16.go
Normal file
29
pkg/utf16/utf16.go
Normal file
@ -0,0 +1,29 @@
|
||||
package utf16
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func FromUTF8(data []byte) (res []byte) {
|
||||
endian := binary.LittleEndian
|
||||
|
||||
res = make([]byte, (len(data)+1)*2)
|
||||
|
||||
res = res[:2]
|
||||
endian.PutUint16(res, 0xfeff)
|
||||
|
||||
for len(data) > 0 {
|
||||
r, size := utf8.DecodeRune(data)
|
||||
if r > 65535 {
|
||||
panic(fmt.Errorf("r=0x%x > 0xffff", r))
|
||||
}
|
||||
|
||||
slen := len(res)
|
||||
res = res[:slen+2]
|
||||
endian.PutUint16(res[slen:], uint16(r))
|
||||
data = data[size:]
|
||||
}
|
||||
return
|
||||
}
|
||||
30
secretstore/io.go
Normal file
30
secretstore/io.go
Normal file
@ -0,0 +1,30 @@
|
||||
package secretstore
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func readFull(in io.Reader, ba []byte) (err error) {
|
||||
_, err = io.ReadFull(in, ba)
|
||||
return
|
||||
}
|
||||
|
||||
func read[T any](in io.Reader) (v T, err error) {
|
||||
err = binary.Read(in, binary.BigEndian, &v)
|
||||
return
|
||||
}
|
||||
|
||||
var readSize = read[uint16]
|
||||
|
||||
func randRead(ba []byte) (err error) {
|
||||
err = readFull(rand.Reader, ba)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to read random bytes: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
9
secretstore/mem.go
Normal file
9
secretstore/mem.go
Normal file
@ -0,0 +1,9 @@
|
||||
package secretstore
|
||||
|
||||
func Memzero(ba []byte) { memzero(ba) }
|
||||
|
||||
func memzero(ba []byte) {
|
||||
for i := range ba {
|
||||
ba[i] = 0
|
||||
}
|
||||
}
|
||||
68
secretstore/reader.go
Normal file
68
secretstore/reader.go
Normal file
@ -0,0 +1,68 @@
|
||||
package secretstore
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (s *Store) NewReader(reader io.Reader) (r io.Reader, err error) {
|
||||
iv := [aes.BlockSize]byte{}
|
||||
|
||||
err = readFull(reader, iv[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = storeReader{reader, s.NewDecrypter(iv)}
|
||||
return
|
||||
}
|
||||
|
||||
type storeReader struct {
|
||||
reader io.Reader
|
||||
decrypter cipher.Stream
|
||||
}
|
||||
|
||||
func (r storeReader) Read(ba []byte) (n int, err error) {
|
||||
n, err = r.reader.Read(ba)
|
||||
|
||||
if n > 0 {
|
||||
r.decrypter.XORKeyStream(ba[:n], ba[:n])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) NewWriter(writer io.Writer) (r io.Writer, err error) {
|
||||
iv := [aes.BlockSize]byte{}
|
||||
|
||||
if err = randRead(iv[:]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = writer.Write(iv[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = storeWriter{writer, s.NewEncrypter(iv)}
|
||||
return
|
||||
}
|
||||
|
||||
type storeWriter struct {
|
||||
writer io.Writer
|
||||
encrypter cipher.Stream
|
||||
}
|
||||
|
||||
func (r storeWriter) Write(ba []byte) (n int, err error) {
|
||||
if len(ba) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
encBA := make([]byte, len(ba))
|
||||
r.encrypter.XORKeyStream(encBA, ba)
|
||||
|
||||
n, err = r.writer.Write(encBA)
|
||||
|
||||
return
|
||||
}
|
||||
278
secretstore/secret-store.go
Normal file
278
secretstore/secret-store.go
Normal file
@ -0,0 +1,278 @@
|
||||
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
|
||||
}
|
||||
|
||||
type KeyEntry struct {
|
||||
Name string
|
||||
Hash [64]byte
|
||||
EncKey [32]byte
|
||||
}
|
||||
|
||||
func New() (s *Store) {
|
||||
s = &Store{}
|
||||
syscall.Mlock(s.key[:])
|
||||
return
|
||||
}
|
||||
|
||||
func Open(path string) (s *Store, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
s = New()
|
||||
_, err = s.ReadFrom(bufio.NewReader(f))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) SaveTo(path string) (err error) {
|
||||
f, err := os.OpenFile(path, syscall.O_CREAT|syscall.O_TRUNC|syscall.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
out := bufio.NewWriter(f)
|
||||
|
||||
_, err = s.WriteTo(out)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = out.Flush()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
memzero(s.key[:])
|
||||
syscall.Munlock(s.key[:])
|
||||
s.unlocked = false
|
||||
}
|
||||
|
||||
func (s *Store) IsNew() bool {
|
||||
return len(s.Keys) == 0
|
||||
}
|
||||
|
||||
func (s *Store) Unlocked() bool {
|
||||
return s.unlocked
|
||||
}
|
||||
|
||||
func (s *Store) Init(name string, passphrase []byte) (err error) {
|
||||
err = randRead(s.key[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = randRead(s.Salt[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Output(2, fmt.Sprintf("failed after %d bytes", n))
|
||||
}
|
||||
}()
|
||||
|
||||
readFull := func(ba []byte) {
|
||||
var nr int
|
||||
nr, err = io.ReadFull(in, ba)
|
||||
n += int64(nr)
|
||||
}
|
||||
|
||||
// read the file's start (json header or start of salt)
|
||||
|
||||
readFull(s.Salt[:len(jsonFormatHdr)])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(s.Salt[:len(jsonFormatHdr)], jsonFormatHdr) {
|
||||
// old key file
|
||||
|
||||
// finish reading the salt
|
||||
readFull(s.Salt[len(jsonFormatHdr):])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
_, err = out.Write(jsonFormatHdr)
|
||||
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)
|
||||
defer memzero(key[:])
|
||||
|
||||
var idx = -1
|
||||
for i := range s.Keys {
|
||||
if hash == s.Keys[i].Hash {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
s.decryptTo(s.key[:], s.Keys[idx].EncKey[:], &key)
|
||||
|
||||
s.unlocked = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) AddKey(name string, passphrase []byte) {
|
||||
key, hash := s.keyPairFromPassword(passphrase)
|
||||
memzero(passphrase)
|
||||
|
||||
defer memzero(key[:])
|
||||
|
||||
k := KeyEntry{Name: name, Hash: hash}
|
||||
|
||||
encKey := s.encrypt(s.key[:], &key)
|
||||
copy(k.EncKey[:], encKey)
|
||||
|
||||
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)
|
||||
|
||||
copy(key[:], keySlice)
|
||||
memzero(keySlice)
|
||||
|
||||
hash = sha512.Sum512(key[:])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) NewEncrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
||||
if !s.unlocked {
|
||||
panic("not unlocked")
|
||||
}
|
||||
return newEncrypter(iv, &s.key)
|
||||
}
|
||||
|
||||
func (s *Store) NewDecrypter(iv [aes.BlockSize]byte) cipher.Stream {
|
||||
if !s.unlocked {
|
||||
panic("not unlocked")
|
||||
}
|
||||
return newDecrypter(iv, &s.key)
|
||||
}
|
||||
|
||||
func (s *Store) encrypt(src []byte, key *[32]byte) (dst []byte) {
|
||||
dst = make([]byte, len(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)
|
||||
}
|
||||
|
||||
func newEncrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
||||
c, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to init AES: %w", err))
|
||||
}
|
||||
|
||||
return cipher.NewCFBEncrypter(c, iv[:])
|
||||
}
|
||||
|
||||
func newDecrypter(iv [aes.BlockSize]byte, key *[32]byte) cipher.Stream {
|
||||
c, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to init AES: %w", err))
|
||||
}
|
||||
|
||||
return cipher.NewCFBDecrypter(c, iv[:])
|
||||
}
|
||||
24
ui/Trunk.toml
Normal file
24
ui/Trunk.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[build]
|
||||
public_url = "/ui"
|
||||
dist = "../html/ui"
|
||||
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/public-state"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/state"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/public"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/store"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/authorize-download"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/sign-download-set"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/configs"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/clusters"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/hosts-from-template"
|
||||
[[proxy]]
|
||||
backend = "http://localhost:7606/hosts"
|
||||
42
ui/app.css
Normal file
42
ui/app.css
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
.view-links > span {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1pt solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.downloads, .download-links {
|
||||
& > * {
|
||||
display: inline-block;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1px solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.downloads, .view-links {
|
||||
& > .selected {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
.text-and-file {
|
||||
position:relative;
|
||||
|
||||
textarea {
|
||||
width: 64em;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
position:absolute;
|
||||
bottom:0;right:0;
|
||||
}
|
||||
}
|
||||
BIN
ui/favicon.ico
Normal file
BIN
ui/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
145
ui/index.html
Normal file
145
ui/index.html
Normal file
@ -0,0 +1,145 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Direktil Local Server</title>
|
||||
<base data-trunk-public-url />
|
||||
<link data-trunk rel="copy-file" href="favicon.ico" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<link data-trunk rel="copy-file" href="js/vue.esm-browser.js" />
|
||||
<link data-trunk rel="copy-file" href="style.css" />
|
||||
<link data-trunk rel="copy-file" href="app.css" />
|
||||
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
||||
<script data-trunk src="js/jsonpatch.min.js"></script>
|
||||
<script data-trunk src="js/app.js" type="module" defer></script>
|
||||
<script data-trunk src="js/Downloads.js"></script>
|
||||
<script data-trunk src="js/GetCopy.js"></script>
|
||||
<script data-trunk src="js/Cluster.js"></script>
|
||||
<script data-trunk src="js/Host.js"></script>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<header>
|
||||
<div id="logo">
|
||||
<img src="favicon.ico" />
|
||||
<span>Direktil Local Server</span>
|
||||
</div>
|
||||
<div class="utils">
|
||||
<span id="login-hdr" v-if="session.token">
|
||||
Logged in
|
||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||
</span>
|
||||
|
||||
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
|
||||
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="error" v-if="error">
|
||||
<button class="btn-close" @click="error=null">×</button>
|
||||
<div class="code" v-if="error.code">{{ error.code }}</div>
|
||||
<div class="message">{{ error.message }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!publicState">
|
||||
<p>Not connected.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="publicState.Store.New">
|
||||
<p>Store is new.</p>
|
||||
<p>Option 1: initialize a new store</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="text" v-model="forms.store.name" name="name" placeholder="Name" /><br/>
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" required placeholder="Passphrase" />
|
||||
<input type="password" v-model="forms.store.pass2" required placeholder="Passphrase confirmation" />
|
||||
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||
</form>
|
||||
<p>Option 2: upload a previously downloaded store</p>
|
||||
<form @submit="uploadStore">
|
||||
<input type="file" ref="storeUpload" />
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else-if="!publicState.Store.Open">
|
||||
<p>Store is not open.</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else-if="!state">
|
||||
<p v-if="!session.token">Not logged in.</p>
|
||||
<p v-else>Invalid token</p>
|
||||
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="log in"/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div style="float:right;"><input type="search" placeholder="Filter" v-model="viewFilter"/></div>
|
||||
<p class="view-links"><span v-for="v in views" @click="view = v" :class="{selected: view.type==v.type && view.name==v.name}">{{v.title}}</span></p>
|
||||
|
||||
<h2 v-if="view">{{view.title}}</h2>
|
||||
|
||||
<div v-if="view.type == 'cluster'" id="clusters">
|
||||
<Cluster :cluster="viewObj" :token="session.token" :state="state" />
|
||||
</div>
|
||||
|
||||
<div v-if="view.type == 'host'" id="hosts">
|
||||
<Host :host="viewObj" :token="session.token" :state="state" />
|
||||
</div>
|
||||
|
||||
<div v-if="view.type == 'actions' && view.name == 'admin'">
|
||||
<h3>Config</h3>
|
||||
<form @submit="uploadConfig">
|
||||
<input type="file" ref="configUpload" required />
|
||||
<input type="submit" value="upload config" />
|
||||
</form>
|
||||
|
||||
<h3>Store</h3>
|
||||
<p><a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">Download</a></p>
|
||||
<form @submit="storeAddKey" action="/store/add-key">
|
||||
<p>Add an unlock phrase:</p>
|
||||
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
||||
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
||||
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
||||
<input type="submit" value="add unlock phrase" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
||||
</form>
|
||||
<form @submit="storeDelKey" action="/store/delete-key">
|
||||
<p>Remove an unlock phrase:</p>
|
||||
<input type="text" v-model="forms.delKey.name" name="name" required placeholder="Name" />
|
||||
<input type="submit" value="remove unlock phrase" />
|
||||
|
||||
<p v-if="state.Store.KeyNames">Available names:
|
||||
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
||||
</form>
|
||||
|
||||
<template v-if="any(state.HostTemplates) || any(hostsFromTemplate)">
|
||||
<h3>Hosts from template</h3>
|
||||
<form @submit="hostFromTemplateAdd" action="" v-if="any(state.HostTemplates)">
|
||||
<p>Add a host from template instance:</p>
|
||||
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
||||
<select v-model="forms.hostFromTemplate.Template" required>
|
||||
<option v-for="name in state.HostTemplates" :value="name">{{name}}</option>
|
||||
</select>
|
||||
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
||||
<input type="submit" value="add instance" />
|
||||
</form>
|
||||
<form @submit="hostFromTemplateDel" action="" v-if="any(hostsFromTemplate)">
|
||||
<p>Remove a host from template instance:</p>
|
||||
<select v-model="forms.hostFromTemplateDel" required>
|
||||
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
||||
</select>
|
||||
<input type="submit" value="delete instance" :disabled="!forms.hostFromTemplateDel" />
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
131
ui/js/Cluster.js
Normal file
131
ui/js/Cluster.js
Normal file
@ -0,0 +1,131 @@
|
||||
const Cluster = {
|
||||
components: { Downloads, GetCopy },
|
||||
props: [ 'cluster', 'token', 'state' ],
|
||||
data() {
|
||||
return {
|
||||
signReqValidity: "1d",
|
||||
sshSignReq: {
|
||||
PubKey: "",
|
||||
Principal: "root",
|
||||
},
|
||||
sshUserCert: null,
|
||||
kubeSignReq: {
|
||||
CSR: "",
|
||||
User: "",
|
||||
Group: "system:masters",
|
||||
},
|
||||
kubeUserCert: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sshCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
||||
} else {
|
||||
resp.json().then((resp) => alert('failed to sign: '+resp.message))
|
||||
}
|
||||
})
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
kubeCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
|
||||
} else {
|
||||
resp.json().then((resp) => alert('failed to sign: '+resp.message))
|
||||
}
|
||||
})
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
readFile(e, onload) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) { return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => { onload(reader.result) };
|
||||
reader.onerror = () => { alert("error reading file"); };
|
||||
reader.readAsText(file);
|
||||
},
|
||||
loadPubKey(e) {
|
||||
this.readFile(e, (v) => {
|
||||
this.sshSignReq.PubKey = v;
|
||||
});
|
||||
},
|
||||
loadCSR(e) {
|
||||
this.readFile(e, (v) => {
|
||||
this.kubeSignReq.CSR = v;
|
||||
});
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<h3>Access</h3>
|
||||
|
||||
<p>Allow cluster access from a public key</p>
|
||||
|
||||
<h4>Grant SSH access</h4>
|
||||
|
||||
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
|
||||
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
|
||||
<p>Public key (OpenSSH format):<br/>
|
||||
<span class="text-and-file"><textarea v-model="sshSignReq.PubKey" style="height:3lh"></textarea>
|
||||
<input type="file" accept=".pub" @change="loadPubKey" /></span>
|
||||
</p>
|
||||
|
||||
<p><button @click="sshCASign">Sign SSH access</button>
|
||||
<template v-if="sshUserCert">
|
||||
=> <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<h4>Grant Kubernetes API access</h4>
|
||||
|
||||
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
|
||||
<p>User: <input type="text" v-model="kubeSignReq.User"/> (by default, from the CSR)</p>
|
||||
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
|
||||
<p>Certificate signing request (PEM format):<br/>
|
||||
<span class="text-and-file"><textarea v-model="kubeSignReq.CSR" style="height:7lh;"></textarea>
|
||||
<input type="file" accept=".csr" @change="loadCSR" /></span>
|
||||
</p>
|
||||
|
||||
<p><button @click="kubeCASign">Sign Kubernetes API access</button>
|
||||
<template v-if="kubeUserCert">
|
||||
=> <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<h3>Tokens</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
|
||||
</section>
|
||||
|
||||
<h3>Passwords</h3>
|
||||
<section class="links">
|
||||
<GetCopy v-for="n in cluster.Passwords" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/passwords/'+n" />
|
||||
</section>
|
||||
|
||||
<h3>Downloads</h3>
|
||||
<Downloads :token="token" :state="state" kind="cluster" :name="cluster.Name" />
|
||||
|
||||
<h3>CAs</h3>
|
||||
<table><tr><th>Name</th><th>Certificate</th><th>Signed certificates</th></tr>
|
||||
<tr v-for="ca in cluster.CAs">
|
||||
<td>{{ ca.Name }}</td>
|
||||
<td><GetCopy :token="token" name="cert" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/certificate'" /></td>
|
||||
<td><template v-for="signed in ca.Signed">
|
||||
{{" "}}
|
||||
<GetCopy :token="token" :name="signed" :href="'/clusters/'+cluster.Name+'/CAs/'+ca.Name+'/signed?name='+signed" />
|
||||
</template></td>
|
||||
</tr></table>
|
||||
|
||||
`
|
||||
}
|
||||
70
ui/js/Downloads.js
Normal file
70
ui/js/Downloads.js
Normal file
@ -0,0 +1,70 @@
|
||||
const Downloads = {
|
||||
props: [ 'kind', 'name', 'token', 'state' ],
|
||||
data() {
|
||||
return { createDisabled: false, selectedAssets: {} }
|
||||
},
|
||||
computed: {
|
||||
availableAssets() {
|
||||
return {
|
||||
cluster: ['addons'],
|
||||
host: [
|
||||
"kernel",
|
||||
"initrd",
|
||||
"bootstrap.tar",
|
||||
"boot.img.lz4",
|
||||
"boot.img.gz",
|
||||
"boot.qcow2",
|
||||
"boot.vmdk",
|
||||
"boot.img",
|
||||
"boot.iso",
|
||||
"boot.tar",
|
||||
"boot-efi.tar",
|
||||
"config",
|
||||
"bootstrap-config",
|
||||
"ipxe",
|
||||
],
|
||||
}[this.kind]
|
||||
},
|
||||
downloads() {
|
||||
return Object.entries(this.state.Downloads)
|
||||
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
||||
.map(e => {
|
||||
const token= e[0];
|
||||
return {
|
||||
text: token.substring(0, 5) + '...',
|
||||
url: '/public/downloads/'+token+"/",
|
||||
}
|
||||
})
|
||||
},
|
||||
assets() {
|
||||
return this.availableAssets.filter(a => this.selectedAssets[a])
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
createToken() {
|
||||
event.preventDefault()
|
||||
this.createDisabled = true
|
||||
|
||||
fetch('/authorize-download', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({Kind: this.kind, Name: this.name, Assets: this.assets}),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => resp.json())
|
||||
.then((token) => { this.selectedAssets = {}; this.createDisabled = false })
|
||||
.catch((e) => { alert('failed to create link'); this.createDisabled = false })
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<h4>Available assets</h4>
|
||||
<p class="downloads">
|
||||
<template v-for="asset in availableAssets">
|
||||
<label :class="{selected: selectedAssets[asset]}"><input type="checkbox" v-model="selectedAssets[asset]" /> {{ 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>`
|
||||
}
|
||||
32
ui/js/GetCopy.js
Normal file
32
ui/js/GetCopy.js
Normal file
@ -0,0 +1,32 @@
|
||||
const GetCopy = {
|
||||
props: [ 'name', 'href', 'token' ],
|
||||
data() { return {showCopied: false} },
|
||||
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a> <a href="#" class="copy" @click="fetchAndCopy()">🗐</a></span>`,
|
||||
methods: {
|
||||
fetch() {
|
||||
event.preventDefault()
|
||||
return fetch(this.href, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token },
|
||||
})
|
||||
},
|
||||
handleFetchError(e) {
|
||||
console.log("failed to get value:", e)
|
||||
alert('failed to get value')
|
||||
},
|
||||
fetchAndSave() {
|
||||
this.fetch().then(resp => resp.blob()).then((value) => {
|
||||
window.open(URL.createObjectURL(value), "_blank")
|
||||
}).catch(this.handleFetchError)
|
||||
},
|
||||
fetchAndCopy() {
|
||||
this.fetch()
|
||||
.then((resp) => resp.headers.get("content-type") == "application/json" ? resp.json() : resp.text())
|
||||
.then((value) => {
|
||||
window.navigator.clipboard.writeText(value)
|
||||
this.showCopied = true
|
||||
setTimeout(() => { this.showCopied = false }, 1000)
|
||||
}).catch(this.handleFetchError)
|
||||
},
|
||||
},
|
||||
}
|
||||
14
ui/js/Host.js
Normal file
14
ui/js/Host.js
Normal file
@ -0,0 +1,14 @@
|
||||
const Host = {
|
||||
components: { Downloads },
|
||||
props: [ 'host', 'token', 'state' ],
|
||||
template: `
|
||||
<p>Cluster: {{ host.Cluster }}<template v-if="host.Template"> ({{ host.Template }})</template></p>
|
||||
<p>IPs:
|
||||
<code v-for="ip in host.IPs">
|
||||
{{ ip }}{{" "}}
|
||||
</code>
|
||||
</p>
|
||||
<h3>Downloads</h3>
|
||||
<Downloads :token="token" :state="state" kind="host" :name="host.Name" />
|
||||
`
|
||||
}
|
||||
267
ui/js/app.js
Normal file
267
ui/js/app.js
Normal file
@ -0,0 +1,267 @@
|
||||
|
||||
import { createApp } from './vue.esm-browser.js';
|
||||
|
||||
createApp({
|
||||
components: { Cluster, Host },
|
||||
data() {
|
||||
return {
|
||||
forms: {
|
||||
store: {},
|
||||
storeUpload: {},
|
||||
delKey: {},
|
||||
hostFromTemplate: {},
|
||||
hostFromTemplateDel: "",
|
||||
},
|
||||
view: "",
|
||||
viewFilter: "",
|
||||
session: {},
|
||||
error: null,
|
||||
publicState: null,
|
||||
serverVersion: null,
|
||||
uiHash: null,
|
||||
watchingState: false,
|
||||
state: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.session = JSON.parse(sessionStorage.state || "{}")
|
||||
this.watchPublicState()
|
||||
},
|
||||
watch: {
|
||||
session: {
|
||||
deep: true,
|
||||
handler(v) {
|
||||
sessionStorage.state = JSON.stringify(v)
|
||||
|
||||
if (v.token && !this.watchingState) {
|
||||
this.watchState()
|
||||
this.watchingState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
publicState: {
|
||||
deep: true,
|
||||
handler(v) {
|
||||
if (v) {
|
||||
this.serverVersion = v.ServerVersion
|
||||
if (this.uiHash && v.UIHash != this.uiHash) {
|
||||
console.log("reloading")
|
||||
location.reload()
|
||||
} else {
|
||||
this.uiHash = v.UIHash
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
views() {
|
||||
var views = [{type: "actions", name: "admin", title: "Admin actions"}];
|
||||
|
||||
(this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`}));
|
||||
(this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`}));
|
||||
|
||||
return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter));
|
||||
},
|
||||
viewObj() {
|
||||
if (this.view) {
|
||||
if (this.view.type == "cluster") {
|
||||
return this.state.Clusters.find((c) => c.Name == this.view.name);
|
||||
}
|
||||
if (this.view.type == "host") {
|
||||
return this.state.Hosts.find((h) => h.Name == this.view.name);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
hostsFromTemplate() {
|
||||
return (this.state.Hosts||[]).filter((h) => h.Template);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
any(array) {
|
||||
return array && array.length != 0;
|
||||
},
|
||||
copyText(text) {
|
||||
event.preventDefault()
|
||||
window.navigator.clipboard.writeText(text)
|
||||
},
|
||||
setToken() {
|
||||
event.preventDefault()
|
||||
this.session.token = this.forms.setToken
|
||||
this.forms.setToken = null
|
||||
},
|
||||
uploadStore() {
|
||||
event.preventDefault()
|
||||
this.apiPost('/public/store.tar', this.$refs.storeUpload.files[0], (v) => {
|
||||
this.forms.store = {}
|
||||
}, "application/tar")
|
||||
},
|
||||
namedPassphrase(name, passphrase) {
|
||||
return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)}
|
||||
},
|
||||
storeAddKey() {
|
||||
this.apiPost('/store/add-key', this.namedPassphrase(), (v) => {
|
||||
this.forms.store = {}
|
||||
})
|
||||
},
|
||||
storeDelKey() {
|
||||
event.preventDefault()
|
||||
|
||||
let name = this.forms.delKey.name
|
||||
|
||||
if (!confirm("Remove key named "+JSON.stringify(name)+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiPost('/store/delete-key', name , (v) => {
|
||||
this.forms.delKey = {}
|
||||
})
|
||||
},
|
||||
unlockStore() {
|
||||
this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => {
|
||||
this.forms.store = {}
|
||||
|
||||
if (v) {
|
||||
this.session.token = v
|
||||
if (!this.watchingState) {
|
||||
this.watchState()
|
||||
this.watchingState = true
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadConfig() {
|
||||
const file = this.$refs.configUpload.files[0];
|
||||
let contentType = "text/vnd.yaml";
|
||||
if (file.name.endsWith(".json")) {
|
||||
contentType = "application/json";
|
||||
}
|
||||
this.apiPost('/configs', file, (v) => {}, contentType, true)
|
||||
},
|
||||
hostFromTemplateAdd() {
|
||||
let v = this.forms.hostFromTemplate;
|
||||
this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} });
|
||||
},
|
||||
hostFromTemplateDel() {
|
||||
event.preventDefault()
|
||||
|
||||
let v = this.forms.hostFromTemplateDel;
|
||||
if (!confirm("delete host template instance "+v+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
|
||||
},
|
||||
apiPost(action, data, onload, contentType = 'application/json', raw = false) {
|
||||
event.preventDefault()
|
||||
|
||||
if (data === undefined) {
|
||||
throw("action " + action + ": no data")
|
||||
}
|
||||
|
||||
/* TODO
|
||||
fetch(action, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => onload)
|
||||
// */
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.responseType = 'json'
|
||||
// TODO spinner, pending action notification, or something
|
||||
xhr.onerror = () => {
|
||||
// this.actionResults.splice(idx, 1, {...item, done: true, failed: true })
|
||||
}
|
||||
xhr.onload = (r) => {
|
||||
if (xhr.status != 200) {
|
||||
this.error = xhr.response
|
||||
return
|
||||
}
|
||||
// this.actionResults.splice(idx, 1, {...item, done: true, resp: xhr.responseText})
|
||||
this.error = null
|
||||
if (onload) {
|
||||
onload(xhr.response)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open("POST", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
xhr.setRequestHeader('Content-Type', contentType)
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
|
||||
if (!raw && contentType == "application/json") {
|
||||
xhr.send(JSON.stringify(data))
|
||||
} else {
|
||||
xhr.send(data)
|
||||
}
|
||||
},
|
||||
apiDelete(action, data, onload) {
|
||||
event.preventDefault()
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onload = (r) => {
|
||||
if (xhr.status != 200) {
|
||||
this.error = xhr.response
|
||||
return
|
||||
}
|
||||
this.error = null
|
||||
if (onload) {
|
||||
onload(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.open("DELETE", action)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
if (this.session.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token)
|
||||
}
|
||||
xhr.send()
|
||||
},
|
||||
download(url) {
|
||||
event.target.target = '_blank'
|
||||
event.target.href = this.downloadLink(url)
|
||||
},
|
||||
downloadLink(url) {
|
||||
// TODO once-shot download link
|
||||
return url + '?token=' + this.session.token
|
||||
},
|
||||
watchPublicState() {
|
||||
this.watchStream('publicState', '/public-state')
|
||||
},
|
||||
watchState() {
|
||||
this.watchStream('state', '/state', true)
|
||||
},
|
||||
watchStream(field, path, withToken) {
|
||||
let evtSrc = new EventSource(path + (withToken ? '?token='+this.session.token : ''));
|
||||
evtSrc.onmessage = (e) => {
|
||||
let update = JSON.parse(e.data)
|
||||
|
||||
console.log("watch "+path+":", update)
|
||||
|
||||
if (update.err) {
|
||||
console.log("watch error from server:", err)
|
||||
}
|
||||
if (update.set) {
|
||||
this[field] = update.set
|
||||
}
|
||||
if (update.p) { // patch
|
||||
new jsonpatch.JSONPatch(update.p, true).apply(this[field])
|
||||
}
|
||||
}
|
||||
evtSrc.onerror = (e) => {
|
||||
// console.log("event source " + path + " error:", e)
|
||||
if (evtSrc) evtSrc.close()
|
||||
|
||||
this[field] = null
|
||||
|
||||
window.setTimeout(() => { this.watchStream(field, path, withToken) }, 1000)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
}).mount('#app');
|
||||
36
ui/js/jsonpatch.min.js
vendored
Normal file
36
ui/js/jsonpatch.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16172
ui/js/vue.esm-browser.js
Normal file
16172
ui/js/vue.esm-browser.js
Normal file
File diff suppressed because it is too large
Load Diff
195
ui/style.css
Normal file
195
ui/style.css
Normal file
@ -0,0 +1,195 @@
|
||||
:root {
|
||||
--bg: #eee;
|
||||
--color: black;
|
||||
--bevel-dark: darkgray;
|
||||
--bevel-light: lightgray;
|
||||
--link: blue;
|
||||
--input-bg: #ddd;
|
||||
--input-text: white;
|
||||
--btn-bg: #eee;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: black;
|
||||
--color: orange;
|
||||
--bevel-dark: #402900;
|
||||
--bevel-light: #805300;
|
||||
--link: #31b0fa;
|
||||
--input-bg: #111;
|
||||
--input-text: #ddd;
|
||||
--btn-bg: #222;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: var(--link);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border-left: dotted 1pt;
|
||||
border-right: dotted 1pt;
|
||||
border-bottom: dotted 1pt;
|
||||
padding: 2pt 4pt;
|
||||
}
|
||||
tr:first-child > th {
|
||||
border-top: dotted 1pt;
|
||||
}
|
||||
th, tr:last-child > td {
|
||||
border-bottom: solid 1pt;
|
||||
}
|
||||
|
||||
.flat > * { margin-left: 1ex; }
|
||||
.flat > *:first-child { margin-left: 0; }
|
||||
|
||||
.green { color: green; }
|
||||
.red { color: red; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.red { color: #c00; }
|
||||
}
|
||||
|
||||
textarea, select, input {
|
||||
background: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
border: solid 1pt;
|
||||
border-color: var(--bevel-light);
|
||||
border-top-color: var(--bevel-dark);
|
||||
border-left-color: var(--bevel-dark);
|
||||
margin: 1pt;
|
||||
|
||||
&:focus {
|
||||
outline: solid 1pt var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
button, input[type=button], input[type=submit], ::file-selector-button {
|
||||
background: var(--btn-bg);
|
||||
color: var(--color);
|
||||
border: solid 2pt;
|
||||
border-color: var(--bevel-dark);
|
||||
border-top-color: var(--bevel-light);
|
||||
border-left-color: var(--bevel-light);
|
||||
|
||||
&:hover {
|
||||
background: var(--bevel-dark);
|
||||
}
|
||||
&:active {
|
||||
background: var(--bevel-dark);
|
||||
border-color: var(--bevel-light);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2pt solid;
|
||||
margin: 0 0 1em 0;
|
||||
padding: 1ex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#logo > img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
header .utils > * {
|
||||
margin-left: 1ex;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: rgba(255,0,0,0.2);
|
||||
border: 1pt solid red;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.error .btn-close,
|
||||
.error .code {
|
||||
background: #600;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
align-self: stretch;
|
||||
padding: 1ex 1em;
|
||||
}
|
||||
.error .code {
|
||||
order: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.error .message {
|
||||
order: 2;
|
||||
padding: 1ex 2em;
|
||||
}
|
||||
.error .btn-close {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.sheets {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.sheets > div {
|
||||
margin: 0 1ex;
|
||||
border: 1pt solid;
|
||||
border-radius: 6pt;
|
||||
}
|
||||
.sheets .title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
padding: 2pt 6pt;
|
||||
background: rgba(127,127,127,0.5);
|
||||
}
|
||||
.sheets .section {
|
||||
padding: 2pt 6pt 2pt 6pt;
|
||||
font-weight: bold;
|
||||
border-top: 1px dotted;
|
||||
}
|
||||
.sheets section {
|
||||
margin: 2pt 6pt 6pt 6pt;
|
||||
}
|
||||
.sheets > *:last-child > table:last-child > tr:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notif {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.notif > div:first-child {
|
||||
position: absolute;
|
||||
min-width: 100%; height: 100%;
|
||||
background: white;
|
||||
opacity: 75%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.links > * { margin-left: 1ex; }
|
||||
.links > *:first-child { margin-left: 0; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.notif > div:first-child {
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
|
||||
.copy { font-size: small; }
|
||||
19
update-boot.sh
Executable file
19
update-boot.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#! /bin/bash
|
||||
|
||||
dls_url="$1"
|
||||
|
||||
set -ex
|
||||
|
||||
mount -o remount,rw /boot
|
||||
|
||||
if [ -e /boot/previous ]; then
|
||||
rm -fr /boot/previous
|
||||
fi
|
||||
|
||||
if [ -e /boot/current ]; then
|
||||
mv /boot/current /boot/previous
|
||||
fi
|
||||
|
||||
curl $dls_url/me/boot.tar |tar xv -C /boot
|
||||
sync
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user