Compare commits
143 Commits
test
...
08cbccc756
Author | SHA1 | Date | |
---|---|---|---|
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
|
||||
|
38
Dockerfile
38
Dockerfile
@ -1,23 +1,33 @@
|
||||
# ------------------------------------------------------------------------
|
||||
from golang:1.11.5 as build
|
||||
from golang:1.24.4-bookworm 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:bookworm
|
||||
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 \
|
||||
&& 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=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,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"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 +47,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 +68,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 +86,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 +127,83 @@ func main() {
|
||||
|
||||
defer out.Close()
|
||||
|
||||
out.Write([]byte("# dkl-dir2config " + Version + "\n"))
|
||||
|
||||
if err = yaml.NewEncoder(out).Encode(dst); err != nil {
|
||||
log.Fatal("failed to render output: ", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func renderAddons(cluster *clustersconfig.Cluster) string {
|
||||
if len(cluster.Addons) == 0 {
|
||||
return ""
|
||||
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)
|
||||
}
|
||||
|
||||
addons := src.Addons[cluster.Addons]
|
||||
if addons == nil {
|
||||
log.Fatalf("cluster %q: no addons with name %q", cluster.Name, cluster.Addons)
|
||||
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)
|
||||
}
|
||||
|
||||
clusterAsMap := asMap(cluster)
|
||||
clusterAsMap["kubernetes_svc_ip"] = cluster.KubernetesSvcIP().String()
|
||||
clusterAsMap["dns_svc_ip"] = cluster.DNSSvcIP().String()
|
||||
for _, include := range includes {
|
||||
switch {
|
||||
case include.Branch != "" || include.Tag != "":
|
||||
p := cfgPath(include.Path) // FIXME parse git path to allow remote repos
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
var rev plumbing.Revision
|
||||
|
||||
for _, addon := range addons {
|
||||
fmt.Fprintf(buf, "---\n# addon: %s\n", addon.Name)
|
||||
err := addon.Execute(buf, clusterAsMap, nil)
|
||||
switch {
|
||||
case include.Branch != "":
|
||||
log.Printf("opening include path %q as git, branch %q", p, include.Branch)
|
||||
rev = plumbing.Revision(plumbing.NewBranchReferenceName(include.Branch))
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("cluster %q: addons %q: failed to render %q: %v",
|
||||
cluster.Name, cluster.Addons, addon.Name, err)
|
||||
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)})
|
||||
}
|
||||
|
||||
fmt.Fprintln(buf)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
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,32 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"path"
|
||||
"reflect"
|
||||
"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 +40,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,7 +219,7 @@ 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
|
||||
}
|
||||
@ -145,43 +229,47 @@ func (ctx *renderContext) templateFuncs(ctxMap map[string]interface{}) map[strin
|
||||
key += "/" + ctx.Host.Name
|
||||
}
|
||||
|
||||
if funcName == "tls_dir" {
|
||||
switch funcName {
|
||||
case "tls_dir":
|
||||
// needs the dir name
|
||||
dir := "/etc/tls/" + name
|
||||
|
||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q %q }}", funcName,
|
||||
dir, cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||
|
||||
} else {
|
||||
default:
|
||||
s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
|
||||
cluster, req.CA, key, req.Profile, req.Label, buf.String())
|
||||
}
|
||||
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 +279,23 @@ 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 }}"
|
||||
},
|
||||
|
||||
"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 +307,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]interface{}{}
|
||||
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[interface{}]interface{})
|
||||
|
||||
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
|
||||
}
|
@ -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 := ctx.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
|
||||
}
|
||||
|
@ -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 := ctx.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 := ctx.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
|
||||
}
|
||||
|
209
cmd/dkl-local-server/bootv2.go
Normal file
209
cmd/dkl-local-server/bootv2.go
Normal file
@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cat := cpiocat.New(out)
|
||||
|
||||
// initrd
|
||||
initrdPath, err := ctx.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]
|
||||
modulesPath, err := ctx.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", "dsa", "ecdsa", "ed25519"} {
|
||||
keyPath := "/etc/ssh/ssh_host_" + format + "_key"
|
||||
cat.AppendBytes(cfg.FileContent(keyPath), keyPath, 0o600)
|
||||
}
|
||||
|
||||
// ssh user CA
|
||||
userCA, err := sshCAPubKey(ctx.Host.ClusterName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get SSH user CA: %w", err)
|
||||
}
|
||||
|
||||
cat.AppendBytes(userCA, "user_ca.pub", 0600)
|
||||
|
||||
return cat.Close()
|
||||
}
|
||||
|
||||
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 := ctx.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
|
||||
}
|
213
cmd/dkl-local-server/cluster-render-context.go
Normal file
213
cmd/dkl-local-server/cluster-render-context.go
Normal file
@ -0,0 +1,213 @@
|
||||
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,
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
ca, err := getUsableClusterCA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return asYaml([]config.FileDef{
|
||||
{
|
||||
Path: path.Join(dir, "ca.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(ca.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "tls.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(kc.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "tls.key"),
|
||||
Mode: 0600,
|
||||
Content: string(kc.Key),
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func asYaml(v interface{}) (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"}
|
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,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func renderKernel(w http.ResponseWriter, r *http.Request, ctx *renderContext) error {
|
||||
@ -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 := ctx.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/", http.FileServer(http.Dir(*dataDir+"/dist"))))
|
||||
|
||||
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,128 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,147 +134,10 @@ 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) {
|
||||
certReq := &csr.CertificateRequest{
|
||||
KeyRequest: csr.NewBasicKeyRequest(),
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(reqJson), certReq)
|
||||
if err != nil {
|
||||
log.Print("CSR unmarshal failed on: ", reqJson)
|
||||
return
|
||||
}
|
||||
|
||||
return secretData.KeyCert(cluster, caName, name, profile, label, certReq)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"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
|
||||
},
|
||||
|
||||
"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
|
||||
ca, err := secretData.CA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return asYaml([]config.FileDef{
|
||||
{
|
||||
Path: path.Join(dir, "ca.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(ca.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "tls.crt"),
|
||||
Mode: 0644,
|
||||
Content: string(kc.Cert),
|
||||
},
|
||||
{
|
||||
Path: path.Join(dir, "tls.key"),
|
||||
Mode: 0600,
|
||||
Content: string(kc.Key),
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *renderContext) distFilePath(path ...string) string {
|
||||
return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
|
||||
}
|
||||
@ -247,7 +152,7 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
|
||||
enc := yaml.NewEncoder(h)
|
||||
|
||||
for _, o := range []interface{}{cfg, ctx} {
|
||||
for _, o := range []any{cfg, ctx} {
|
||||
if err := enc.Encode(o); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -256,17 +161,87 @@ func (ctx *renderContext) Tag() (string, error) {
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func asMap(v interface{}) map[string]interface{} {
|
||||
ba, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
func (ctx *renderContext) TemplateFuncs() map[string]any {
|
||||
funcs := templateFuncs(ctx.SSLConfig)
|
||||
|
||||
for name, method := range map[string]any{
|
||||
"host_ip": func() (s string) {
|
||||
return ctx.Host.IPs[0]
|
||||
},
|
||||
"host_name": func() (s string) {
|
||||
return ctx.Host.Name
|
||||
},
|
||||
"machine_id": func() (s string) {
|
||||
ba := sha1.Sum([]byte(ctx.Host.ClusterName + "/" + ctx.Host.Name))
|
||||
return hex.EncodeToString(ba[:])
|
||||
},
|
||||
|
||||
"ssh_user_ca": func(path, cluster string) (s string, err error) {
|
||||
userCA, err := sshCAPubKey(cluster)
|
||||
return asYaml([]config.FileDef{{
|
||||
Path: path,
|
||||
Mode: 0644,
|
||||
Content: string(userCA),
|
||||
}})
|
||||
},
|
||||
"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
|
||||
if host == "" {
|
||||
host = ctx.Host.Name
|
||||
}
|
||||
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)
|
||||
},
|
||||
"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
|
||||
},
|
||||
} {
|
||||
funcs[name] = method
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
if err := yaml.Unmarshal(ba, result); err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
return result
|
||||
return funcs
|
||||
}
|
||||
|
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,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"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/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 +34,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 +55,21 @@ 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)
|
||||
}
|
||||
|
||||
func (sd *SecretData) cluster(name string) (cs *ClusterSecrets) {
|
||||
cs, ok := sd.clusters[name]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
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 host == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if req.CA != nil {
|
||||
err = errors.New("no CA section allowed here")
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := sd.CA(cluster, caName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
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)
|
||||
}
|
211
cmd/dkl-local-server/ssh-secrets.go
Normal file
211
cmd/dkl-local-server/ssh-secrets.go
Normal file
@ -0,0 +1,211 @@
|
||||
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",
|
||||
"dsa",
|
||||
"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
|
||||
}
|
||||
|
||||
_, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(userPubKeyFile.Name())
|
||||
|
||||
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey))
|
||||
userPubKeyFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serial := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)
|
||||
|
||||
if validity != "" {
|
||||
cmd.Args = append(cmd.Args, "-V", validity)
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
cmd.Args = append(cmd.Args, "-O", opt)
|
||||
}
|
||||
|
||||
cmd.Args = append(cmd.Args, userPubKeyFile.Name())
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd.Stdin = bytes.NewBuffer([]byte(caKey))
|
||||
cmd.Stderr = stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssh-keygen sign failed: %s", strings.TrimSpace(stderr.String()))
|
||||
return
|
||||
}
|
||||
|
||||
certFile := userPubKeyFile.Name() + "-cert.pub"
|
||||
cert, err = os.ReadFile(certFile)
|
||||
|
||||
os.Remove(certFile)
|
||||
|
||||
return
|
||||
}
|
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
|
||||
}
|
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,18 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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) {
|
||||
@ -39,42 +46,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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -18,11 +17,14 @@ func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(true)
|
||||
}
|
||||
|
||||
func writeNewConfig(reader io.Reader) (err error) {
|
||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
||||
out, err := os.CreateTemp(*dataDir, ".config-upload")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -35,8 +37,30 @@ func writeNewConfig(reader io.Reader) (err error) {
|
||||
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 +76,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 +85,5 @@ func writeNewConfig(reader io.Reader) (err error) {
|
||||
gz.Close()
|
||||
in.Close()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(out.Name(), cfgPath)
|
||||
return
|
||||
}
|
||||
|
151
cmd/dkl-local-server/ws-downloads.go
Normal file
151
cmd/dkl-local-server/ws-downloads.go
Normal file
@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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 wsDownload(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)
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
setHeader := func(ext string) {
|
||||
resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(spec.Kind+"_"+spec.Name+"_"+asset+ext))
|
||||
}
|
||||
|
||||
switch spec.Kind {
|
||||
case "cluster":
|
||||
cluster := cfg.ClusterByName(spec.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, spec.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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
117
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
117
cmd/dkl-local-server/ws-hosts-from-templates.go
Normal file
@ -0,0 +1,117 @@
|
||||
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 {
|
||||
log.Print("no host named ", name)
|
||||
return
|
||||
}
|
||||
|
||||
hft, found, err := hostsFromTemplate.Get(name)
|
||||
if err != nil {
|
||||
log.Print("failed to read store: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Print("no host from template named ", name)
|
||||
return
|
||||
}
|
||||
|
||||
ht := cfg.HostTemplate(hft.Template)
|
||||
if ht == nil {
|
||||
log.Print("no host template named ", name)
|
||||
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})
|
||||
}
|
44
cmd/dkl-local-server/ws-ssh-acls.go
Normal file
44
cmd/dkl-local-server/ws-ssh-acls.go
Normal file
@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type SSH_ACL struct {
|
||||
Keys []string
|
||||
Clusters []string
|
||||
Groups []string
|
||||
Hosts []string
|
||||
}
|
||||
|
||||
func loadSSH_ACLs() (acls []SSH_ACL, err error) {
|
||||
f, err := os.Open(filepath.Join(*dataDir, "ssh-acls.yaml"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
err = yaml.NewDecoder(f).Decode(&acls)
|
||||
return
|
||||
}
|
||||
|
||||
func wsSSH_ACL_List(req *restful.Request, resp *restful.Response) {
|
||||
// TODO
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func wsSSH_ACL_Get(req *restful.Request, resp *restful.Response) {
|
||||
// TODO
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func wsSSH_ACL_Set(req *restful.Request, resp *restful.Response) {
|
||||
// TODO
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
}
|
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,62 +1,231 @@
|
||||
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"
|
||||
"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}/{asset}").To(wsDownload).
|
||||
Param(ws.PathParameter("token", "the download token")).
|
||||
Param(ws.PathParameter("asset", "the requested asset")).
|
||||
Doc("Fetch an asset via a download token"))
|
||||
|
||||
// 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"))
|
||||
|
||||
// - configs API
|
||||
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
||||
Consumes(mime.YAML).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"))
|
||||
|
||||
ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List))
|
||||
ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get))
|
||||
ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set))
|
||||
|
||||
rest.Add(ws)
|
||||
|
||||
// Hosts API
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Filter(adminAuth).
|
||||
Path("/hosts/{host-name}").
|
||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||
|
||||
(&wsHost{
|
||||
hostDoc: "given host",
|
||||
getHost: func(req *restful.Request) (string, error) {
|
||||
return req.PathParameter("host-name"), nil
|
||||
},
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Param(ws.PathParameter("host-name", "host's name"))
|
||||
})
|
||||
|
||||
rest.Add(ws)
|
||||
|
||||
// Detected host API
|
||||
ws = (&restful.WebService{}).
|
||||
Filter(requireSecStore).
|
||||
Path("/me").
|
||||
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
|
||||
|
||||
(&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")
|
||||
})
|
||||
|
||||
// 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{
|
||||
prefix: "/hosts/{host-name}",
|
||||
hostDoc: "given host",
|
||||
getHost: func(req *restful.Request) string {
|
||||
return req.PathParameter("host-name")
|
||||
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.Filter(adminAuth)
|
||||
rb.Notes("In this case, the host is detected from the token")
|
||||
})
|
||||
|
||||
return ws
|
||||
rest.Add(ws)
|
||||
}
|
||||
|
||||
func detectHost(req *restful.Request) string {
|
||||
func requireSecStore(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
if !secStore.Unlocked() {
|
||||
wsError(resp, ErrStoreLocked)
|
||||
return
|
||||
}
|
||||
chain.ProcessFilter(req, resp)
|
||||
}
|
||||
|
||||
func detectHost(req *restful.Request) (hostName string, err error) {
|
||||
if !*allowDetectedHost {
|
||||
return
|
||||
}
|
||||
|
||||
r := req.Request
|
||||
remoteAddr := r.RemoteAddr
|
||||
|
||||
@ -74,17 +243,17 @@ func detectHost(req *restful.Request) string {
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
host := cfg.HostByIP(hostIP)
|
||||
|
||||
if host == nil {
|
||||
log.Print("no host found for IP ", hostIP)
|
||||
return ""
|
||||
return
|
||||
}
|
||||
|
||||
return host.Name
|
||||
return host.Name, nil
|
||||
}
|
||||
|
||||
func wsReadConfig(resp *restful.Response) *localconfig.Config {
|
||||
@ -98,13 +267,36 @@ 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
|
||||
}
|
||||
}
|
||||
|
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/
|
107
go.mod
107
go.mod
@ -1,34 +1,81 @@
|
||||
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/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
|
||||
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-20250706092353-d857af8032a1
|
||||
)
|
||||
|
||||
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/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
)
|
||||
|
918
go.sum
918
go.sum
@ -1,690 +1,346 @@
|
||||
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/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=
|
||||
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-20250706092353-d857af8032a1 h1:hKj9qhbTAoTxYIj6KaMLJp9I+bvZfkSM/QwK8Bd496o=
|
||||
novit.tech/direktil/pkg v0.0.0-20250706092353-d857af8032a1/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)}" $*
|
8
hack/docker-build
Executable file
8
hack/docker-build
Executable file
@ -0,0 +1,8 @@
|
||||
#! /bin/bash
|
||||
set -ex
|
||||
case "$1" in
|
||||
commit) tag=$(git describe --always --dirty) ;;
|
||||
"") tag=latest ;;
|
||||
*) tag=$1 ;;
|
||||
esac
|
||||
docker build -t novit.tech/direktil/local-server:$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
|
BIN
html/favicon.ico
Normal file
BIN
html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
6
html/html.go
Normal file
6
html/html.go
Normal file
@ -0,0 +1,6 @@
|
||||
package dlshtml
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed favicon.ico ui
|
||||
var FS embed.FS
|
37
html/ui/app.css
Normal file
37
html/ui/app.css
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
.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 {
|
||||
& > * {
|
||||
display: inline-block;
|
||||
margin-right: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
padding: 0.5ex;
|
||||
border: 1px solid;
|
||||
border-radius: 1ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
& > .selected {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
|
||||
.download-links a {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.downloads > .selected,
|
||||
.view-links > .selected {
|
||||
color: #31b0fa;
|
||||
}
|
||||
}
|
139
html/ui/index.html
Normal file
139
html/ui/index.html
Normal file
@ -0,0 +1,139 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Direktil Local Server</title>
|
||||
<style>
|
||||
@import url('./style.css');
|
||||
@import url('./app.css');
|
||||
</style>
|
||||
<script src="js/jsonpatch.min.js" crossorigin="anonymous"></script>
|
||||
<script src="js/app.js" type="module" defer></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="text" placeholder="Filter" v-model="viewFilter"/></div>
|
||||
<p class="view-links"><span v-for="v in views" @click="view = v" :class="{selected: view.type==v.type && view.name==v.name}">{{v.title}}</span></p>
|
||||
|
||||
<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="state.HostTemplates && state.HostTemplates.length">
|
||||
<h3>Hosts from template</h3>
|
||||
<form @submit="hostFromTemplateAdd" action="">
|
||||
<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="">
|
||||
<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>
|
101
html/ui/js/Cluster.js
Normal file
101
html/ui/js/Cluster.js
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
import Downloads from './Downloads.js';
|
||||
import GetCopy from './GetCopy.js';
|
||||
|
||||
export default {
|
||||
components: { Downloads, GetCopy },
|
||||
props: [ 'cluster', 'token', 'state' ],
|
||||
data() {
|
||||
return {
|
||||
signReqValidity: "1d",
|
||||
sshSignReq: {
|
||||
PubKey: "",
|
||||
Principal: "root",
|
||||
},
|
||||
sshUserCert: null,
|
||||
kubeSignReq: {
|
||||
CSR: "",
|
||||
User: "anonymous",
|
||||
Group: "",
|
||||
},
|
||||
kubeUserCert: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sshCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => resp.blob())
|
||||
.then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
kubeCASign() {
|
||||
event.preventDefault();
|
||||
fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
|
||||
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||
}).then((resp) => resp.blob())
|
||||
.then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
|
||||
.catch((e) => { alert('failed to sign: '+e); })
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<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>
|
||||
|
||||
<h3>Access</h3>
|
||||
|
||||
<p>Allow cluster access from a public key</p>
|
||||
<p>Certificate time validity: <input type="text" v-model="signReqValidity"/> <small>ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</p>
|
||||
|
||||
<h4>Grant SSH access</h4>
|
||||
|
||||
<p>Public key (OpenSSH format):<br/>
|
||||
<textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea>
|
||||
</p>
|
||||
<p>Principal: <input type="text" v-model="sshSignReq.Principal"/></p>
|
||||
|
||||
<p><button @click="sshCASign">Sign SSH access request</button></p>
|
||||
<p v-if="sshUserCert">
|
||||
<a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
|
||||
</p>
|
||||
|
||||
<h4>Grant Kubernetes API access</h4>
|
||||
|
||||
<p>Certificate signing request (PEM format):<br/>
|
||||
<textarea v-model="kubeSignReq.CSR" style="width:64em;height:7lh;"></textarea>
|
||||
</p>
|
||||
<p>User: <input type="text" v-model="kubeSignReq.User"/></p>
|
||||
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
|
||||
|
||||
<p><button @click="kubeCASign">Sign Kubernetes API access request</button></p>
|
||||
<p v-if="kubeUserCert">
|
||||
<a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
|
||||
</p>
|
||||
`
|
||||
}
|
72
html/ui/js/Downloads.js
Normal file
72
html/ui/js/Downloads.js
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
export default {
|
||||
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() {
|
||||
let ret = []
|
||||
Object.entries(this.state.Downloads)
|
||||
.filter(e => { let d=e[1]; return d.Kind == this.kind && d.Name == this.name })
|
||||
.forEach(e => {
|
||||
let token= e[0], d = e[1]
|
||||
d.Assets.forEach(asset => {
|
||||
ret.push({name: asset, url: '/public/downloads/'+token+'/'+asset})
|
||||
})
|
||||
})
|
||||
return ret
|
||||
},
|
||||
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 links</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" download>{{ d.name }}</a>{{" "}}</template></p>
|
||||
</template>`
|
||||
}
|
32
html/ui/js/GetCopy.js
Normal file
32
html/ui/js/GetCopy.js
Normal file
@ -0,0 +1,32 @@
|
||||
export default {
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
17
html/ui/js/Host.js
Normal file
17
html/ui/js/Host.js
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
import Downloads from './Downloads.js';
|
||||
|
||||
export default {
|
||||
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" />
|
||||
`
|
||||
}
|
263
html/ui/js/app.js
Normal file
263
html/ui/js/app.js
Normal file
@ -0,0 +1,263 @@
|
||||
|
||||
import { createApp } from './vue.esm-browser.js';
|
||||
|
||||
import Cluster from './Cluster.js';
|
||||
import Host from './Host.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.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: {
|
||||
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() {
|
||||
this.apiPost('/configs', this.$refs.configUpload.files[0], (v) => {}, "text/vnd.yaml")
|
||||
},
|
||||
hostFromTemplateAdd() {
|
||||
let v = this.forms.hostFromTemplate;
|
||||
this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} });
|
||||
},
|
||||
hostFromTemplateDel() {
|
||||
event.preventDefault()
|
||||
|
||||
let v = this.forms.hostFromTemplateDel;
|
||||
if (!confirm("delete host template instance "+v+"?")) {
|
||||
return
|
||||
}
|
||||
this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" });
|
||||
},
|
||||
apiPost(action, data, onload, contentType = 'application/json') {
|
||||
event.preventDefault()
|
||||
|
||||
if (data === undefined) {
|
||||
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 (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
html/ui/js/jsonpatch.min.js
vendored
Normal file
36
html/ui/js/jsonpatch.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16172
html/ui/js/vue.esm-browser.js
Normal file
16172
html/ui/js/vue.esm-browser.js
Normal file
File diff suppressed because it is too large
Load Diff
162
html/ui/style.css
Normal file
162
html/ui/style.css
Normal file
@ -0,0 +1,162 @@
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: blue;
|
||||
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) {
|
||||
body {
|
||||
background: black;
|
||||
color: orange;
|
||||
}
|
||||
button, input[type=submit] {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
a[href], a[href]:visited, button.link {
|
||||
border: none;
|
||||
color: #31b0fa;
|
||||
}
|
||||
|
||||
.red { color: #c00; }
|
||||
|
||||
textarea, input[type=text] {
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
border: dotted 1pt;
|
||||
border-top-color: #805300;
|
||||
border-left-color: #805300;
|
||||
border-bottom-color: orange;
|
||||
border-right-color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
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:
|
||||
|
@ -3,7 +3,6 @@ package clustersconfig
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -15,37 +14,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 +54,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 +71,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 +98,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
|
||||
|
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[:])
|
||||
}
|
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
|
||||
|
5
vendor/github.com/PuerkitoBio/purell/.gitignore
generated
vendored
5
vendor/github.com/PuerkitoBio/purell/.gitignore
generated
vendored
@ -1,5 +0,0 @@
|
||||
*.sublime-*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
tags
|
7
vendor/github.com/PuerkitoBio/purell/.travis.yml
generated
vendored
7
vendor/github.com/PuerkitoBio/purell/.travis.yml
generated
vendored
@ -1,7 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
12
vendor/github.com/PuerkitoBio/purell/LICENSE
generated
vendored
12
vendor/github.com/PuerkitoBio/purell/LICENSE
generated
vendored
@ -1,12 +0,0 @@
|
||||
Copyright (c) 2012, Martin Angers
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
187
vendor/github.com/PuerkitoBio/purell/README.md
generated
vendored
187
vendor/github.com/PuerkitoBio/purell/README.md
generated
vendored
@ -1,187 +0,0 @@
|
||||
# Purell
|
||||
|
||||
Purell is a tiny Go library to normalize URLs. It returns a pure URL. Pure-ell. Sanitizer and all. Yeah, I know...
|
||||
|
||||
Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
|
||||
|
||||
[](http://travis-ci.org/PuerkitoBio/purell)
|
||||
|
||||
## Install
|
||||
|
||||
`go get github.com/PuerkitoBio/purell`
|
||||
|
||||
## Changelog
|
||||
|
||||
* **2016-11-14 (v1.1.0)** : IDN: Conform to RFC 5895: Fold character width (thanks to @beeker1121).
|
||||
* **2016-07-27 (v1.0.0)** : Normalize IDN to ASCII (thanks to @zenovich).
|
||||
* **2015-02-08** : Add fix for relative paths issue ([PR #5][pr5]) and add fix for unnecessary encoding of reserved characters ([see issue #7][iss7]).
|
||||
* **v0.2.0** : Add benchmarks, Attempt IDN support.
|
||||
* **v0.1.0** : Initial release.
|
||||
|
||||
## Examples
|
||||
|
||||
From `example_test.go` (note that in your code, you would import "github.com/PuerkitoBio/purell", and would prefix references to its methods and constants with "purell."):
|
||||
|
||||
```go
|
||||
package purell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func ExampleNormalizeURLString() {
|
||||
if normalized, err := NormalizeURLString("hTTp://someWEBsite.com:80/Amazing%3f/url/",
|
||||
FlagLowercaseScheme|FlagLowercaseHost|FlagUppercaseEscapes); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
fmt.Print(normalized)
|
||||
}
|
||||
// Output: http://somewebsite.com:80/Amazing%3F/url/
|
||||
}
|
||||
|
||||
func ExampleMustNormalizeURLString() {
|
||||
normalized := MustNormalizeURLString("hTTpS://someWEBsite.com:443/Amazing%fa/url/",
|
||||
FlagsUnsafeGreedy)
|
||||
fmt.Print(normalized)
|
||||
|
||||
// Output: http://somewebsite.com/Amazing%FA/url
|
||||
}
|
||||
|
||||
func ExampleNormalizeURL() {
|
||||
if u, err := url.Parse("Http://SomeUrl.com:8080/a/b/.././c///g?c=3&a=1&b=9&c=0#target"); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
normalized := NormalizeURL(u, FlagsUsuallySafeGreedy|FlagRemoveDuplicateSlashes|FlagRemoveFragment)
|
||||
fmt.Print(normalized)
|
||||
}
|
||||
|
||||
// Output: http://someurl.com:8080/a/c/g?c=3&a=1&b=9&c=0
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
As seen in the examples above, purell offers three methods, `NormalizeURLString(string, NormalizationFlags) (string, error)`, `MustNormalizeURLString(string, NormalizationFlags) (string)` and `NormalizeURL(*url.URL, NormalizationFlags) (string)`. They all normalize the provided URL based on the specified flags. Here are the available flags:
|
||||
|
||||
```go
|
||||
const (
|
||||
// Safe normalizations
|
||||
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
|
||||
FlagLowercaseHost // http://HOST -> http://host
|
||||
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
|
||||
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
|
||||
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
|
||||
FlagRemoveDefaultPort // http://host:80 -> http://host
|
||||
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
|
||||
|
||||
// Usually safe normalizations
|
||||
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
|
||||
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
|
||||
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
|
||||
|
||||
// Unsafe normalizations
|
||||
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
|
||||
FlagRemoveFragment // http://host/path#fragment -> http://host/path
|
||||
FlagForceHTTP // https://host -> http://host
|
||||
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
|
||||
FlagRemoveWWW // http://www.host/ -> http://host/
|
||||
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
|
||||
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
|
||||
|
||||
// Normalizations not in the wikipedia article, required to cover tests cases
|
||||
// submitted by jehiah
|
||||
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
|
||||
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
|
||||
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
|
||||
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
|
||||
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
|
||||
|
||||
// Convenience set of safe normalizations
|
||||
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
|
||||
|
||||
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
|
||||
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
|
||||
|
||||
// Convenience set of usually safe normalizations (includes FlagsSafe)
|
||||
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
|
||||
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
|
||||
|
||||
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
|
||||
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
|
||||
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
|
||||
|
||||
// Convenience set of all available flags
|
||||
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||
)
|
||||
```
|
||||
|
||||
For convenience, the set of flags `FlagsSafe`, `FlagsUsuallySafe[Greedy|NonGreedy]`, `FlagsUnsafe[Greedy|NonGreedy]` and `FlagsAll[Greedy|NonGreedy]` are provided for the similarly grouped normalizations on [wikipedia's URL normalization page][wiki]. You can add (using the bitwise OR `|` operator) or remove (using the bitwise AND NOT `&^` operator) individual flags from the sets if required, to build your own custom set.
|
||||
|
||||
The [full godoc reference is available on gopkgdoc][godoc].
|
||||
|
||||
Some things to note:
|
||||
|
||||
* `FlagDecodeUnnecessaryEscapes`, `FlagEncodeNecessaryEscapes`, `FlagUppercaseEscapes` and `FlagRemoveEmptyQuerySeparator` are always implicitly set, because internally, the URL string is parsed as an URL object, which automatically decodes unnecessary escapes, uppercases and encodes necessary ones, and removes empty query separators (an unnecessary `?` at the end of the url). So this operation cannot **not** be done. For this reason, `FlagRemoveEmptyQuerySeparator` (as well as the other three) has been included in the `FlagsSafe` convenience set, instead of `FlagsUnsafe`, where Wikipedia puts it.
|
||||
|
||||
* The `FlagDecodeUnnecessaryEscapes` decodes the following escapes (*from -> to*):
|
||||
- %24 -> $
|
||||
- %26 -> &
|
||||
- %2B-%3B -> +,-./0123456789:;
|
||||
- %3D -> =
|
||||
- %40-%5A -> @ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
- %5F -> _
|
||||
- %61-%7A -> abcdefghijklmnopqrstuvwxyz
|
||||
- %7E -> ~
|
||||
|
||||
|
||||
* When the `NormalizeURL` function is used (passing an URL object), this source URL object is modified (that is, after the call, the URL object will be modified to reflect the normalization).
|
||||
|
||||
* The *replace IP with domain name* normalization (`http://208.77.188.166/ → http://www.example.com/`) is obviously not possible for a library without making some network requests. This is not implemented in purell.
|
||||
|
||||
* The *remove unused query string parameters* and *remove default query parameters* are also not implemented, since this is a very case-specific normalization, and it is quite trivial to do with an URL object.
|
||||
|
||||
### Safe vs Usually Safe vs Unsafe
|
||||
|
||||
Purell allows you to control the level of risk you take while normalizing an URL. You can aggressively normalize, play it totally safe, or anything in between.
|
||||
|
||||
Consider the following URL:
|
||||
|
||||
`HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
|
||||
|
||||
Normalizing with the `FlagsSafe` gives:
|
||||
|
||||
`https://www.root.com/toto/tE%1F///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
|
||||
|
||||
With the `FlagsUsuallySafeGreedy`:
|
||||
|
||||
`https://www.root.com/toto/tE%1F///a/c?z=3&w=2&a=4&w=1#invalid`
|
||||
|
||||
And with `FlagsUnsafeGreedy`:
|
||||
|
||||
`http://root.com/toto/tE%1F/a/c?a=4&w=1&w=2&z=3`
|
||||
|
||||
## TODOs
|
||||
|
||||
* Add a class/default instance to allow specifying custom directory index names? At the moment, removing directory index removes `(^|/)((?:default|index)\.\w{1,4})$`.
|
||||
|
||||
## Thanks / Contributions
|
||||
|
||||
@rogpeppe
|
||||
@jehiah
|
||||
@opennota
|
||||
@pchristopher1275
|
||||
@zenovich
|
||||
@beeker1121
|
||||
|
||||
## License
|
||||
|
||||
The [BSD 3-Clause license][bsd].
|
||||
|
||||
[bsd]: http://opensource.org/licenses/BSD-3-Clause
|
||||
[wiki]: http://en.wikipedia.org/wiki/URL_normalization
|
||||
[rfc]: http://tools.ietf.org/html/rfc3986#section-6
|
||||
[godoc]: http://go.pkgdoc.org/github.com/PuerkitoBio/purell
|
||||
[pr5]: https://github.com/PuerkitoBio/purell/pull/5
|
||||
[iss7]: https://github.com/PuerkitoBio/purell/issues/7
|
379
vendor/github.com/PuerkitoBio/purell/purell.go
generated
vendored
379
vendor/github.com/PuerkitoBio/purell/purell.go
generated
vendored
@ -1,379 +0,0 @@
|
||||
/*
|
||||
Package purell offers URL normalization as described on the wikipedia page:
|
||||
http://en.wikipedia.org/wiki/URL_normalization
|
||||
*/
|
||||
package purell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/urlesc"
|
||||
"golang.org/x/net/idna"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"golang.org/x/text/width"
|
||||
)
|
||||
|
||||
// A set of normalization flags determines how a URL will
|
||||
// be normalized.
|
||||
type NormalizationFlags uint
|
||||
|
||||
const (
|
||||
// Safe normalizations
|
||||
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
|
||||
FlagLowercaseHost // http://HOST -> http://host
|
||||
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
|
||||
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
|
||||
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
|
||||
FlagRemoveDefaultPort // http://host:80 -> http://host
|
||||
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
|
||||
|
||||
// Usually safe normalizations
|
||||
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
|
||||
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
|
||||
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
|
||||
|
||||
// Unsafe normalizations
|
||||
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
|
||||
FlagRemoveFragment // http://host/path#fragment -> http://host/path
|
||||
FlagForceHTTP // https://host -> http://host
|
||||
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
|
||||
FlagRemoveWWW // http://www.host/ -> http://host/
|
||||
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
|
||||
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
|
||||
|
||||
// Normalizations not in the wikipedia article, required to cover tests cases
|
||||
// submitted by jehiah
|
||||
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
|
||||
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
|
||||
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
|
||||
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
|
||||
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
|
||||
|
||||
// Convenience set of safe normalizations
|
||||
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
|
||||
|
||||
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
|
||||
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
|
||||
|
||||
// Convenience set of usually safe normalizations (includes FlagsSafe)
|
||||
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
|
||||
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
|
||||
|
||||
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
|
||||
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
|
||||
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
|
||||
|
||||
// Convenience set of all available flags
|
||||
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHttpPort = ":80"
|
||||
defaultHttpsPort = ":443"
|
||||
)
|
||||
|
||||
// Regular expressions used by the normalizations
|
||||
var rxPort = regexp.MustCompile(`(:\d+)/?$`)
|
||||
var rxDirIndex = regexp.MustCompile(`(^|/)((?:default|index)\.\w{1,4})$`)
|
||||
var rxDupSlashes = regexp.MustCompile(`/{2,}`)
|
||||
var rxDWORDHost = regexp.MustCompile(`^(\d+)((?:\.+)?(?:\:\d*)?)$`)
|
||||
var rxOctalHost = regexp.MustCompile(`^(0\d*)\.(0\d*)\.(0\d*)\.(0\d*)((?:\.+)?(?:\:\d*)?)$`)
|
||||
var rxHexHost = regexp.MustCompile(`^0x([0-9A-Fa-f]+)((?:\.+)?(?:\:\d*)?)$`)
|
||||
var rxHostDots = regexp.MustCompile(`^(.+?)(:\d+)?$`)
|
||||
var rxEmptyPort = regexp.MustCompile(`:+$`)
|
||||
|
||||
// Map of flags to implementation function.
|
||||
// FlagDecodeUnnecessaryEscapes has no action, since it is done automatically
|
||||
// by parsing the string as an URL. Same for FlagUppercaseEscapes and FlagRemoveEmptyQuerySeparator.
|
||||
|
||||
// Since maps have undefined traversing order, make a slice of ordered keys
|
||||
var flagsOrder = []NormalizationFlags{
|
||||
FlagLowercaseScheme,
|
||||
FlagLowercaseHost,
|
||||
FlagRemoveDefaultPort,
|
||||
FlagRemoveDirectoryIndex,
|
||||
FlagRemoveDotSegments,
|
||||
FlagRemoveFragment,
|
||||
FlagForceHTTP, // Must be after remove default port (because https=443/http=80)
|
||||
FlagRemoveDuplicateSlashes,
|
||||
FlagRemoveWWW,
|
||||
FlagAddWWW,
|
||||
FlagSortQuery,
|
||||
FlagDecodeDWORDHost,
|
||||
FlagDecodeOctalHost,
|
||||
FlagDecodeHexHost,
|
||||
FlagRemoveUnnecessaryHostDots,
|
||||
FlagRemoveEmptyPortSeparator,
|
||||
FlagRemoveTrailingSlash, // These two (add/remove trailing slash) must be last
|
||||
FlagAddTrailingSlash,
|
||||
}
|
||||
|
||||
// ... and then the map, where order is unimportant
|
||||
var flags = map[NormalizationFlags]func(*url.URL){
|
||||
FlagLowercaseScheme: lowercaseScheme,
|
||||
FlagLowercaseHost: lowercaseHost,
|
||||
FlagRemoveDefaultPort: removeDefaultPort,
|
||||
FlagRemoveDirectoryIndex: removeDirectoryIndex,
|
||||
FlagRemoveDotSegments: removeDotSegments,
|
||||
FlagRemoveFragment: removeFragment,
|
||||
FlagForceHTTP: forceHTTP,
|
||||
FlagRemoveDuplicateSlashes: removeDuplicateSlashes,
|
||||
FlagRemoveWWW: removeWWW,
|
||||
FlagAddWWW: addWWW,
|
||||
FlagSortQuery: sortQuery,
|
||||
FlagDecodeDWORDHost: decodeDWORDHost,
|
||||
FlagDecodeOctalHost: decodeOctalHost,
|
||||
FlagDecodeHexHost: decodeHexHost,
|
||||
FlagRemoveUnnecessaryHostDots: removeUnncessaryHostDots,
|
||||
FlagRemoveEmptyPortSeparator: removeEmptyPortSeparator,
|
||||
FlagRemoveTrailingSlash: removeTrailingSlash,
|
||||
FlagAddTrailingSlash: addTrailingSlash,
|
||||
}
|
||||
|
||||
// MustNormalizeURLString returns the normalized string, and panics if an error occurs.
|
||||
// It takes an URL string as input, as well as the normalization flags.
|
||||
func MustNormalizeURLString(u string, f NormalizationFlags) string {
|
||||
result, e := NormalizeURLString(u, f)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NormalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object.
|
||||
// It takes an URL string as input, as well as the normalization flags.
|
||||
func NormalizeURLString(u string, f NormalizationFlags) (string, error) {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if f&FlagLowercaseHost == FlagLowercaseHost {
|
||||
parsed.Host = strings.ToLower(parsed.Host)
|
||||
}
|
||||
|
||||
// The idna package doesn't fully conform to RFC 5895
|
||||
// (https://tools.ietf.org/html/rfc5895), so we do it here.
|
||||
// Taken from Go 1.8 cycle source, courtesy of bradfitz.
|
||||
// TODO: Remove when (if?) idna package conforms to RFC 5895.
|
||||
parsed.Host = width.Fold.String(parsed.Host)
|
||||
parsed.Host = norm.NFC.String(parsed.Host)
|
||||
if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return NormalizeURL(parsed, f), nil
|
||||
}
|
||||
|
||||
// NormalizeURL returns the normalized string.
|
||||
// It takes a parsed URL object as input, as well as the normalization flags.
|
||||
func NormalizeURL(u *url.URL, f NormalizationFlags) string {
|
||||
for _, k := range flagsOrder {
|
||||
if f&k == k {
|
||||
flags[k](u)
|
||||
}
|
||||
}
|
||||
return urlesc.Escape(u)
|
||||
}
|
||||
|
||||
func lowercaseScheme(u *url.URL) {
|
||||
if len(u.Scheme) > 0 {
|
||||
u.Scheme = strings.ToLower(u.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
func lowercaseHost(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
u.Host = strings.ToLower(u.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func removeDefaultPort(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
u.Host = rxPort.ReplaceAllStringFunc(u.Host, func(val string) string {
|
||||
if (scheme == "http" && val == defaultHttpPort) || (scheme == "https" && val == defaultHttpsPort) {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeTrailingSlash(u *url.URL) {
|
||||
if l := len(u.Path); l > 0 {
|
||||
if strings.HasSuffix(u.Path, "/") {
|
||||
u.Path = u.Path[:l-1]
|
||||
}
|
||||
} else if l = len(u.Host); l > 0 {
|
||||
if strings.HasSuffix(u.Host, "/") {
|
||||
u.Host = u.Host[:l-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addTrailingSlash(u *url.URL) {
|
||||
if l := len(u.Path); l > 0 {
|
||||
if !strings.HasSuffix(u.Path, "/") {
|
||||
u.Path += "/"
|
||||
}
|
||||
} else if l = len(u.Host); l > 0 {
|
||||
if !strings.HasSuffix(u.Host, "/") {
|
||||
u.Host += "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeDotSegments(u *url.URL) {
|
||||
if len(u.Path) > 0 {
|
||||
var dotFree []string
|
||||
var lastIsDot bool
|
||||
|
||||
sections := strings.Split(u.Path, "/")
|
||||
for _, s := range sections {
|
||||
if s == ".." {
|
||||
if len(dotFree) > 0 {
|
||||
dotFree = dotFree[:len(dotFree)-1]
|
||||
}
|
||||
} else if s != "." {
|
||||
dotFree = append(dotFree, s)
|
||||
}
|
||||
lastIsDot = (s == "." || s == "..")
|
||||
}
|
||||
// Special case if host does not end with / and new path does not begin with /
|
||||
u.Path = strings.Join(dotFree, "/")
|
||||
if u.Host != "" && !strings.HasSuffix(u.Host, "/") && !strings.HasPrefix(u.Path, "/") {
|
||||
u.Path = "/" + u.Path
|
||||
}
|
||||
// Special case if the last segment was a dot, make sure the path ends with a slash
|
||||
if lastIsDot && !strings.HasSuffix(u.Path, "/") {
|
||||
u.Path += "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeDirectoryIndex(u *url.URL) {
|
||||
if len(u.Path) > 0 {
|
||||
u.Path = rxDirIndex.ReplaceAllString(u.Path, "$1")
|
||||
}
|
||||
}
|
||||
|
||||
func removeFragment(u *url.URL) {
|
||||
u.Fragment = ""
|
||||
}
|
||||
|
||||
func forceHTTP(u *url.URL) {
|
||||
if strings.ToLower(u.Scheme) == "https" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
func removeDuplicateSlashes(u *url.URL) {
|
||||
if len(u.Path) > 0 {
|
||||
u.Path = rxDupSlashes.ReplaceAllString(u.Path, "/")
|
||||
}
|
||||
}
|
||||
|
||||
func removeWWW(u *url.URL) {
|
||||
if len(u.Host) > 0 && strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||
u.Host = u.Host[4:]
|
||||
}
|
||||
}
|
||||
|
||||
func addWWW(u *url.URL) {
|
||||
if len(u.Host) > 0 && !strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||
u.Host = "www." + u.Host
|
||||
}
|
||||
}
|
||||
|
||||
func sortQuery(u *url.URL) {
|
||||
q := u.Query()
|
||||
|
||||
if len(q) > 0 {
|
||||
arKeys := make([]string, len(q))
|
||||
i := 0
|
||||
for k, _ := range q {
|
||||
arKeys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(arKeys)
|
||||
buf := new(bytes.Buffer)
|
||||
for _, k := range arKeys {
|
||||
sort.Strings(q[k])
|
||||
for _, v := range q[k] {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteRune('&')
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%s=%s", k, urlesc.QueryEscape(v)))
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the raw query string
|
||||
u.RawQuery = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDWORDHost(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
if matches := rxDWORDHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||
var parts [4]int64
|
||||
|
||||
dword, _ := strconv.ParseInt(matches[1], 10, 0)
|
||||
for i, shift := range []uint{24, 16, 8, 0} {
|
||||
parts[i] = dword >> shift & 0xFF
|
||||
}
|
||||
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeOctalHost(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
if matches := rxOctalHost.FindStringSubmatch(u.Host); len(matches) > 5 {
|
||||
var parts [4]int64
|
||||
|
||||
for i := 1; i <= 4; i++ {
|
||||
parts[i-1], _ = strconv.ParseInt(matches[i], 8, 0)
|
||||
}
|
||||
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[5])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeHexHost(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
if matches := rxHexHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||
// Conversion is safe because of regex validation
|
||||
parsed, _ := strconv.ParseInt(matches[1], 16, 0)
|
||||
// Set host as DWORD (base 10) encoded host
|
||||
u.Host = fmt.Sprintf("%d%s", parsed, matches[2])
|
||||
// The rest is the same as decoding a DWORD host
|
||||
decodeDWORDHost(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeUnncessaryHostDots(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
if matches := rxHostDots.FindStringSubmatch(u.Host); len(matches) > 1 {
|
||||
// Trim the leading and trailing dots
|
||||
u.Host = strings.Trim(matches[1], ".")
|
||||
if len(matches) > 2 {
|
||||
u.Host += matches[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEmptyPortSeparator(u *url.URL) {
|
||||
if len(u.Host) > 0 {
|
||||
u.Host = rxEmptyPort.ReplaceAllString(u.Host, "")
|
||||
}
|
||||
}
|
15
vendor/github.com/PuerkitoBio/urlesc/.travis.yml
generated
vendored
15
vendor/github.com/PuerkitoBio/urlesc/.travis.yml
generated
vendored
@ -1,15 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go build .
|
||||
|
||||
script:
|
||||
- go test -v
|
27
vendor/github.com/PuerkitoBio/urlesc/LICENSE
generated
vendored
27
vendor/github.com/PuerkitoBio/urlesc/LICENSE
generated
vendored
@ -1,27 +0,0 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
16
vendor/github.com/PuerkitoBio/urlesc/README.md
generated
vendored
16
vendor/github.com/PuerkitoBio/urlesc/README.md
generated
vendored
@ -1,16 +0,0 @@
|
||||
urlesc [](https://travis-ci.org/PuerkitoBio/urlesc) [](http://godoc.org/github.com/PuerkitoBio/urlesc)
|
||||
======
|
||||
|
||||
Package urlesc implements query escaping as per RFC 3986.
|
||||
|
||||
It contains some parts of the net/url package, modified so as to allow
|
||||
some reserved characters incorrectly escaped by net/url (see [issue 5684](https://github.com/golang/go/issues/5684)).
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/PuerkitoBio/urlesc
|
||||
|
||||
## License
|
||||
|
||||
Go license (BSD-3-Clause)
|
||||
|
180
vendor/github.com/PuerkitoBio/urlesc/urlesc.go
generated
vendored
180
vendor/github.com/PuerkitoBio/urlesc/urlesc.go
generated
vendored
@ -1,180 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package urlesc implements query escaping as per RFC 3986.
|
||||
// It contains some parts of the net/url package, modified so as to allow
|
||||
// some reserved characters incorrectly escaped by net/url.
|
||||
// See https://github.com/golang/go/issues/5684
|
||||
package urlesc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type encoding int
|
||||
|
||||
const (
|
||||
encodePath encoding = 1 + iota
|
||||
encodeUserPassword
|
||||
encodeQueryComponent
|
||||
encodeFragment
|
||||
)
|
||||
|
||||
// Return true if the specified character should be escaped when
|
||||
// appearing in a URL string, according to RFC 3986.
|
||||
func shouldEscape(c byte, mode encoding) bool {
|
||||
// §2.3 Unreserved characters (alphanum)
|
||||
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
|
||||
return false
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '-', '.', '_', '~': // §2.3 Unreserved characters (mark)
|
||||
return false
|
||||
|
||||
// §2.2 Reserved characters (reserved)
|
||||
case ':', '/', '?', '#', '[', ']', '@', // gen-delims
|
||||
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // sub-delims
|
||||
// Different sections of the URL allow a few of
|
||||
// the reserved characters to appear unescaped.
|
||||
switch mode {
|
||||
case encodePath: // §3.3
|
||||
// The RFC allows sub-delims and : @.
|
||||
// '/', '[' and ']' can be used to assign meaning to individual path
|
||||
// segments. This package only manipulates the path as a whole,
|
||||
// so we allow those as well. That leaves only ? and # to escape.
|
||||
return c == '?' || c == '#'
|
||||
|
||||
case encodeUserPassword: // §3.2.1
|
||||
// The RFC allows : and sub-delims in
|
||||
// userinfo. The parsing of userinfo treats ':' as special so we must escape
|
||||
// all the gen-delims.
|
||||
return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
|
||||
|
||||
case encodeQueryComponent: // §3.4
|
||||
// The RFC allows / and ?.
|
||||
return c != '/' && c != '?'
|
||||
|
||||
case encodeFragment: // §4.1
|
||||
// The RFC text is silent but the grammar allows
|
||||
// everything, so escape nothing but #
|
||||
return c == '#'
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else must be escaped.
|
||||
return true
|
||||
}
|
||||
|
||||
// QueryEscape escapes the string so it can be safely placed
|
||||
// inside a URL query.
|
||||
func QueryEscape(s string) string {
|
||||
return escape(s, encodeQueryComponent)
|
||||
}
|
||||
|
||||
func escape(s string, mode encoding) string {
|
||||
spaceCount, hexCount := 0, 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if shouldEscape(c, mode) {
|
||||
if c == ' ' && mode == encodeQueryComponent {
|
||||
spaceCount++
|
||||
} else {
|
||||
hexCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if spaceCount == 0 && hexCount == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
t := make([]byte, len(s)+2*hexCount)
|
||||
j := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch c := s[i]; {
|
||||
case c == ' ' && mode == encodeQueryComponent:
|
||||
t[j] = '+'
|
||||
j++
|
||||
case shouldEscape(c, mode):
|
||||
t[j] = '%'
|
||||
t[j+1] = "0123456789ABCDEF"[c>>4]
|
||||
t[j+2] = "0123456789ABCDEF"[c&15]
|
||||
j += 3
|
||||
default:
|
||||
t[j] = s[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
|
||||
var uiReplacer = strings.NewReplacer(
|
||||
"%21", "!",
|
||||
"%27", "'",
|
||||
"%28", "(",
|
||||
"%29", ")",
|
||||
"%2A", "*",
|
||||
)
|
||||
|
||||
// unescapeUserinfo unescapes some characters that need not to be escaped as per RFC3986.
|
||||
func unescapeUserinfo(s string) string {
|
||||
return uiReplacer.Replace(s)
|
||||
}
|
||||
|
||||
// Escape reassembles the URL into a valid URL string.
|
||||
// The general form of the result is one of:
|
||||
//
|
||||
// scheme:opaque
|
||||
// scheme://userinfo@host/path?query#fragment
|
||||
//
|
||||
// If u.Opaque is non-empty, String uses the first form;
|
||||
// otherwise it uses the second form.
|
||||
//
|
||||
// In the second form, the following rules apply:
|
||||
// - if u.Scheme is empty, scheme: is omitted.
|
||||
// - if u.User is nil, userinfo@ is omitted.
|
||||
// - if u.Host is empty, host/ is omitted.
|
||||
// - if u.Scheme and u.Host are empty and u.User is nil,
|
||||
// the entire scheme://userinfo@host/ is omitted.
|
||||
// - if u.Host is non-empty and u.Path begins with a /,
|
||||
// the form host/path does not add its own /.
|
||||
// - if u.RawQuery is empty, ?query is omitted.
|
||||
// - if u.Fragment is empty, #fragment is omitted.
|
||||
func Escape(u *url.URL) string {
|
||||
var buf bytes.Buffer
|
||||
if u.Scheme != "" {
|
||||
buf.WriteString(u.Scheme)
|
||||
buf.WriteByte(':')
|
||||
}
|
||||
if u.Opaque != "" {
|
||||
buf.WriteString(u.Opaque)
|
||||
} else {
|
||||
if u.Scheme != "" || u.Host != "" || u.User != nil {
|
||||
buf.WriteString("//")
|
||||
if ui := u.User; ui != nil {
|
||||
buf.WriteString(unescapeUserinfo(ui.String()))
|
||||
buf.WriteByte('@')
|
||||
}
|
||||
if h := u.Host; h != "" {
|
||||
buf.WriteString(h)
|
||||
}
|
||||
}
|
||||
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
|
||||
buf.WriteByte('/')
|
||||
}
|
||||
buf.WriteString(escape(u.Path, encodePath))
|
||||
}
|
||||
if u.RawQuery != "" {
|
||||
buf.WriteByte('?')
|
||||
buf.WriteString(u.RawQuery)
|
||||
}
|
||||
if u.Fragment != "" {
|
||||
buf.WriteByte('#')
|
||||
buf.WriteString(escape(u.Fragment, encodeFragment))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
3
vendor/github.com/cavaliercoder/go-cpio/.gitignore
generated
vendored
3
vendor/github.com/cavaliercoder/go-cpio/.gitignore
generated
vendored
@ -1,3 +0,0 @@
|
||||
.fuzz/
|
||||
*.zip
|
||||
|
10
vendor/github.com/cavaliercoder/go-cpio/.travis.yml
generated
vendored
10
vendor/github.com/cavaliercoder/go-cpio/.travis.yml
generated
vendored
@ -1,10 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.4
|
||||
- 1.6.4
|
||||
- 1.7.6
|
||||
- 1.8.3
|
||||
|
||||
script: make check
|
26
vendor/github.com/cavaliercoder/go-cpio/LICENSE
generated
vendored
26
vendor/github.com/cavaliercoder/go-cpio/LICENSE
generated
vendored
@ -1,26 +0,0 @@
|
||||
Copyright (c) 2017 Ryan Armstrong. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
18
vendor/github.com/cavaliercoder/go-cpio/Makefile
generated
vendored
18
vendor/github.com/cavaliercoder/go-cpio/Makefile
generated
vendored
@ -1,18 +0,0 @@
|
||||
PACKAGE = github.com/cavaliercoder/go-cpio
|
||||
|
||||
all: check
|
||||
|
||||
check:
|
||||
go test -v
|
||||
|
||||
cpio-fuzz.zip: *.go
|
||||
go-fuzz-build $(PACKAGE)
|
||||
|
||||
fuzz: cpio-fuzz.zip
|
||||
go-fuzz -bin=./cpio-fuzz.zip -workdir=.fuzz/
|
||||
|
||||
clean-fuzz:
|
||||
rm -rf cpio-fuzz.zip .fuzz/crashers/* .fuzz/suppressions/*
|
||||
|
||||
|
||||
.PHONY: all check
|
62
vendor/github.com/cavaliercoder/go-cpio/README.md
generated
vendored
62
vendor/github.com/cavaliercoder/go-cpio/README.md
generated
vendored
@ -1,62 +0,0 @@
|
||||
# go-cpio [](https://godoc.org/github.com/cavaliercoder/go-cpio) [](https://travis-ci.org/cavaliercoder/go-cpio) [](https://goreportcard.com/report/github.com/cavaliercoder/go-cpio)
|
||||
|
||||
This package provides a Go native implementation of the CPIO archive file
|
||||
format.
|
||||
|
||||
Currently, only the SVR4 (New ASCII) format is supported, both with and without
|
||||
checksums.
|
||||
|
||||
```go
|
||||
// Create a buffer to write our archive to.
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Create a new cpio archive.
|
||||
w := cpio.NewWriter(buf)
|
||||
|
||||
// Add some files to the archive.
|
||||
var files = []struct {
|
||||
Name, Body string
|
||||
}{
|
||||
{"readme.txt", "This archive contains some text files."},
|
||||
{"gopher.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
|
||||
{"todo.txt", "Get animal handling license."},
|
||||
}
|
||||
for _, file := range files {
|
||||
hdr := &cpio.Header{
|
||||
Name: file.Name,
|
||||
Mode: 0600,
|
||||
Size: int64(len(file.Body)),
|
||||
}
|
||||
if err := w.WriteHeader(hdr); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if _, err := w.Write([]byte(file.Body)); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
// Make sure to check the error on Close.
|
||||
if err := w.Close(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Open the cpio archive for reading.
|
||||
b := bytes.NewReader(buf.Bytes())
|
||||
r := cpio.NewReader(b)
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
for {
|
||||
hdr, err := r.Next()
|
||||
if err == io.EOF {
|
||||
// end of cpio archive
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Printf("Contents of %s:\n", hdr.Name)
|
||||
if _, err := io.Copy(os.Stdout, r); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
```
|
8
vendor/github.com/cavaliercoder/go-cpio/cpio.go
generated
vendored
8
vendor/github.com/cavaliercoder/go-cpio/cpio.go
generated
vendored
@ -1,8 +0,0 @@
|
||||
/*
|
||||
Package cpio implements access to CPIO archives. Currently, only the SVR4 (New
|
||||
ASCII) format is supported, both with and without checksums.
|
||||
|
||||
References:
|
||||
https://www.freebsd.org/cgi/man.cgi?query=cpio&sektion=5
|
||||
*/
|
||||
package cpio
|
75
vendor/github.com/cavaliercoder/go-cpio/fileinfo.go
generated
vendored
75
vendor/github.com/cavaliercoder/go-cpio/fileinfo.go
generated
vendored
@ -1,75 +0,0 @@
|
||||
package cpio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// headerFileInfo implements os.FileInfo.
|
||||
type headerFileInfo struct {
|
||||
h *Header
|
||||
}
|
||||
|
||||
// Name returns the base name of the file.
|
||||
func (fi headerFileInfo) Name() string {
|
||||
if fi.IsDir() {
|
||||
return path.Base(path.Clean(fi.h.Name))
|
||||
}
|
||||
return path.Base(fi.h.Name)
|
||||
}
|
||||
|
||||
func (fi headerFileInfo) Size() int64 { return fi.h.Size }
|
||||
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
|
||||
func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime }
|
||||
func (fi headerFileInfo) Sys() interface{} { return fi.h }
|
||||
|
||||
func (fi headerFileInfo) Mode() (mode os.FileMode) {
|
||||
// Set file permission bits.
|
||||
mode = os.FileMode(fi.h.Mode).Perm()
|
||||
|
||||
// Set setuid, setgid and sticky bits.
|
||||
if fi.h.Mode&ModeSetuid != 0 {
|
||||
// setuid
|
||||
mode |= os.ModeSetuid
|
||||
}
|
||||
if fi.h.Mode&ModeSetgid != 0 {
|
||||
// setgid
|
||||
mode |= os.ModeSetgid
|
||||
}
|
||||
if fi.h.Mode&ModeSticky != 0 {
|
||||
// sticky
|
||||
mode |= os.ModeSticky
|
||||
}
|
||||
|
||||
// Set file mode bits.
|
||||
// clear perm, setuid, setgid and sticky bits.
|
||||
m := os.FileMode(fi.h.Mode) & 0170000
|
||||
if m == ModeDir {
|
||||
// directory
|
||||
mode |= os.ModeDir
|
||||
}
|
||||
if m == ModeNamedPipe {
|
||||
// named pipe (FIFO)
|
||||
mode |= os.ModeNamedPipe
|
||||
}
|
||||
if m == ModeSymlink {
|
||||
// symbolic link
|
||||
mode |= os.ModeSymlink
|
||||
}
|
||||
if m == ModeDevice {
|
||||
// device file
|
||||
mode |= os.ModeDevice
|
||||
}
|
||||
if m == ModeCharDevice {
|
||||
// Unix character device
|
||||
mode |= os.ModeDevice
|
||||
mode |= os.ModeCharDevice
|
||||
}
|
||||
if m == ModeSocket {
|
||||
// Unix domain socket
|
||||
mode |= os.ModeSocket
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
35
vendor/github.com/cavaliercoder/go-cpio/fuzz.go
generated
vendored
35
vendor/github.com/cavaliercoder/go-cpio/fuzz.go
generated
vendored
@ -1,35 +0,0 @@
|
||||
// +build gofuzz
|
||||
|
||||
package cpio
|
||||
|
||||
import "bytes"
|
||||
import "io"
|
||||
|
||||
// Fuzz tests the parsing and error handling of random byte arrays using
|
||||
// https://github.com/dvyukov/go-fuzz.
|
||||
func Fuzz(data []byte) int {
|
||||
r := NewReader(bytes.NewReader(data))
|
||||
h := NewHash()
|
||||
for {
|
||||
hdr, err := r.Next()
|
||||
if err != nil {
|
||||
if hdr != nil {
|
||||
panic("hdr != nil on error")
|
||||
}
|
||||
if err == io.EOF {
|
||||
// everything worked with random input... interesting
|
||||
return 1
|
||||
}
|
||||
// error returned for random input. Good!
|
||||
return -1
|
||||
}
|
||||
|
||||
// hash file
|
||||
h.Reset()
|
||||
io.CopyN(h, r, hdr.Size)
|
||||
h.Sum32()
|
||||
|
||||
// convert file header
|
||||
FileInfoHeader(hdr.FileInfo())
|
||||
}
|
||||
}
|
45
vendor/github.com/cavaliercoder/go-cpio/hash.go
generated
vendored
45
vendor/github.com/cavaliercoder/go-cpio/hash.go
generated
vendored
@ -1,45 +0,0 @@
|
||||
package cpio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
)
|
||||
|
||||
type digest struct {
|
||||
sum uint32
|
||||
}
|
||||
|
||||
// NewHash returns a new hash.Hash32 computing the SVR4 checksum.
|
||||
func NewHash() hash.Hash32 {
|
||||
return &digest{}
|
||||
}
|
||||
|
||||
func (d *digest) Write(p []byte) (n int, err error) {
|
||||
for _, b := range p {
|
||||
d.sum += uint32(b & 0xFF)
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (d *digest) Sum(b []byte) []byte {
|
||||
out := [4]byte{}
|
||||
binary.LittleEndian.PutUint32(out[:], d.sum)
|
||||
return append(b, out[:]...)
|
||||
}
|
||||
|
||||
func (d *digest) Sum32() uint32 {
|
||||
return d.sum
|
||||
}
|
||||
|
||||
func (d *digest) Reset() {
|
||||
d.sum = 0
|
||||
}
|
||||
|
||||
func (d *digest) Size() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
func (d *digest) BlockSize() int {
|
||||
return 1
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user