Compare commits

...

5 Commits

80 changed files with 2876 additions and 261 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ dist/
/state.json
/emissary.sqlite
/.gitea-release
/agent-key.json

View File

@ -53,11 +53,13 @@ archives:
files:
- README.md
- migrations
- misc/packaging/common/config-server.yml
- id: agent
builds: ["emissary-agent"]
name_template: '{{ .ProjectName }}-agent_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- README.md
- misc/packaging/common/config-agent.yml
checksum:
name_template: 'checksums.txt'
snapshot:
@ -99,6 +101,18 @@ nfpms:
file_info:
mode: 0755
packager: apk
- dst: /var/lib/emissary
type: dir
file_info:
mode: 0700
- dst: /usr/share/emissary
type: dir
file_info:
mode: 0700
- dst: /var/log/emissary
type: dir
file_info:
mode: 0700
scripts:
postinstall: "misc/packaging/common/postinstall-server.sh"
- id: emissary-agent

View File

@ -5,6 +5,7 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command"
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
"forge.cadoles.com/Cadoles/emissary/internal/command/client"
)
// nolint: gochecknoglobals
@ -16,5 +17,5 @@ var (
)
func main() {
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, agent.Root())
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, agent.Root(), client.Root())
}

View File

@ -4,6 +4,7 @@ import (
"time"
"forge.cadoles.com/Cadoles/emissary/internal/command"
"forge.cadoles.com/Cadoles/emissary/internal/command/client"
"forge.cadoles.com/Cadoles/emissary/internal/command/server"
_ "github.com/jackc/pgx/v5/stdlib"
@ -19,5 +20,5 @@ var (
)
func main() {
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, server.Root())
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, server.Root(), client.Root())
}

18
go.mod
View File

@ -8,15 +8,19 @@ require (
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/davecgh/go-spew v1.1.1
github.com/denisbrodbeck/machineid v1.0.1
github.com/evanphx/json-patch/v5 v5.6.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.2.1
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/jackc/pgx/v5 v5.2.0
github.com/jedib0t/go-pretty/v6 v6.4.4
github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonschema v0.2.1
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1
github.com/urfave/cli/v2 v2.23.7
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.20.3
)
@ -27,11 +31,13 @@ require (
cdr.dev/slog v1.4.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dlclark/regexp2 v1.8.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -40,11 +46,19 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.1.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect

44
go.sum
View File

@ -395,6 +395,8 @@ 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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
@ -452,6 +454,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
@ -550,6 +554,8 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
@ -785,6 +791,8 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg=
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc=
github.com/jedib0t/go-pretty/v6 v6.4.4/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@ -850,8 +858,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -896,6 +916,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
@ -1029,6 +1051,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
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/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -1067,9 +1090,15 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -1084,6 +1113,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
@ -1152,9 +1182,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@ -1207,8 +1239,8 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPS
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 h1:6JlkcdjYVQglPWYuemK2MoZAtRE4vFx85zLXflGIyI8=
gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3 h1:ddXRTeqEr7LcHQEtkd6gogZOh9tI1Y6Gappr0a1oa2I=
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
@ -1294,6 +1326,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1409,6 +1442,7 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

View File

@ -4,13 +4,21 @@ import (
"context"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/client"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type Agent struct {
thumbprint string
privateKey jwk.Key
client *client.Client
controllers []Controller
interval time.Duration
collectors []metadata.Collector
}
func (a *Agent) Run(ctx context.Context) error {
@ -21,10 +29,20 @@ func (a *Agent) Run(ctx context.Context) error {
ticker := time.NewTicker(a.interval)
defer ticker.Stop()
ctx = withClient(ctx, a.client)
for {
select {
case <-ticker.C:
logger.Debug(ctx, "registering agent")
if err := a.registerAgent(ctx, state); err != nil {
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
continue
}
logger.Debug(ctx, "state before reconciliation", logger.F("state", state))
if err := a.Reconcile(ctx, state); err != nil {
@ -58,14 +76,67 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
return nil
}
func New(funcs ...OptionFunc) *Agent {
func (a *Agent) registerAgent(ctx context.Context, state *State) error {
meta, err := a.collectMetadata(ctx)
if err != nil {
return errors.WithStack(err)
}
sorted := metadata.Sort(meta)
agent, err := a.client.RegisterAgent(ctx, a.privateKey, a.thumbprint, sorted)
if err != nil {
return errors.WithStack(err)
}
state.agentID = agent.ID
return nil
}
func (a *Agent) collectMetadata(ctx context.Context) (map[string]any, error) {
metadata := make(map[string]any)
for _, collector := range a.collectors {
name, value, err := collector.Collect(ctx)
if err != nil {
logger.Error(
ctx, "could not collect metadata",
logger.E(errors.WithStack(err)), logger.F("name", name),
)
continue
}
metadata[name] = value
}
return metadata, nil
}
func isAPIError(err error, code api.ErrorCode) (bool, any) {
apiError := &api.Error{}
if errors.As(err, &apiError) && apiError.Code == code {
return true, apiError.Data
}
return false, nil
}
func New(serverURL string, privateKey jwk.Key, thumbprint string, funcs ...OptionFunc) *Agent {
opt := defaultOption()
for _, fn := range funcs {
fn(opt)
}
client := client.New(serverURL)
return &Agent{
privateKey: privateKey,
thumbprint: thumbprint,
client: client,
controllers: opt.Controllers,
interval: opt.Interval,
collectors: opt.Collectors,
}
}

27
internal/agent/context.go Normal file
View File

@ -0,0 +1,27 @@
package agent
import (
"context"
"forge.cadoles.com/Cadoles/emissary/internal/client"
"github.com/pkg/errors"
)
type contextKey string
const (
contextKeyClient contextKey = "client"
)
func withClient(ctx context.Context, client *client.Client) context.Context {
return context.WithValue(ctx, contextKeyClient, client)
}
func Client(ctx context.Context) *client.Client {
client, ok := ctx.Value(contextKeyClient).(*client.Client)
if !ok {
panic(errors.New("could not retrieve client from context"))
}
return client
}

View File

@ -4,13 +4,13 @@ import (
"context"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"forge.cadoles.com/Cadoles/emissary/internal/spec/gateway"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Controller struct {
proxies map[spec.GatewayID]*ReverseProxy
proxies map[gateway.ID]*ReverseProxy
currentSpecRevision int
}
@ -21,9 +21,9 @@ func (c *Controller) Name() string {
// Reconcile implements node.Controller.
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
gatewaySpec := spec.NewGatewaySpec()
gatewaySpec := gateway.NewSpec()
if err := state.GetSpec(spec.NameGateway, gatewaySpec); err != nil {
if err := state.GetSpec(gateway.NameGateway, gatewaySpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find gateway spec, stopping all remaining proxies")
@ -67,7 +67,7 @@ func (c *Controller) stopAllProxies(ctx context.Context) {
}
}
func (c *Controller) updateProxies(ctx context.Context, spec *spec.Gateway) {
func (c *Controller) updateProxies(ctx context.Context, spec *gateway.Spec) {
// Stop and remove obsolete gateways
for gatewayID, proxy := range c.proxies {
if _, exists := spec.Gateways[gatewayID]; exists {
@ -116,7 +116,7 @@ func (c *Controller) updateProxies(ctx context.Context, spec *spec.Gateway) {
func NewController() *Controller {
return &Controller{
proxies: make(map[spec.GatewayID]*ReverseProxy),
proxies: make(map[gateway.ID]*ReverseProxy),
currentSpecRevision: -1,
}
}

View File

@ -8,7 +8,7 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
ucispec "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -25,9 +25,9 @@ func (*UCIController) Name() string {
// Reconcile implements node.Controller.
func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error {
uciSpec := spec.NewUCISpec()
uciSpec := ucispec.NewSpec()
if err := state.GetSpec(spec.NameUCI, uciSpec); err != nil {
if err := state.GetSpec(ucispec.NameUCI, uciSpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find uci spec, doing nothing")
@ -57,7 +57,7 @@ func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error
return nil
}
func (c *UCIController) updateConfiguration(ctx context.Context, spec *spec.UCI) error {
func (c *UCIController) updateConfiguration(ctx context.Context, spec *ucispec.Spec) error {
logger.Info(ctx, "importing uci config")
if err := c.importConfig(ctx, spec.Config); err != nil {
@ -91,7 +91,7 @@ func (c *UCIController) importConfig(ctx context.Context, uci *uci.UCI) error {
return nil
}
func (c *UCIController) execPostImportCommands(ctx context.Context, commands []*spec.UCIPostImportCommand) error {
func (c *UCIController) execPostImportCommands(ctx context.Context, commands []*ucispec.UCIPostImportCommand) error {
for _, postImportCmd := range commands {
cmd := exec.CommandContext(ctx, postImportCmd.Command, postImportCmd.Args...)

View File

@ -4,18 +4,13 @@ import (
"context"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/machineid"
"forge.cadoles.com/Cadoles/emissary/internal/client"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/server"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type Controller struct {
client *client.Client
}
type Controller struct{}
// Name implements node.Controller.
func (c *Controller) Name() string {
@ -24,22 +19,12 @@ func (c *Controller) Name() string {
// Reconcile implements node.Controller.
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
machineID, err := machineid.Get()
if err != nil {
return errors.WithStack(err)
}
cl := agent.Client(ctx)
ctx = logger.With(ctx, logger.F("machineID", machineID))
agent, err := c.client.RegisterAgent(ctx, machineID)
isAlreadyRegisteredErr, _ := isAPIError(err, server.ErrCodeAlreadyRegistered)
switch {
case isAlreadyRegisteredErr:
agents, _, err := c.client.QueryAgents(
agents, _, err := cl.QueryAgents(
ctx,
client.WithQueryAgentsLimit(1),
client.WithQueryAgentsRemoteID(machineID),
client.WithQueryAgentsID(state.AgentID()),
)
if err != nil {
return errors.WithStack(err)
@ -51,29 +36,14 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
return nil
}
if err := c.reconcileAgent(ctx, state, agents[0]); err != nil {
if err := c.reconcileAgent(ctx, cl, state, agents[0]); err != nil {
return errors.WithStack(err)
}
return nil
case agent != nil:
if err := c.reconcileAgent(ctx, state, agent); err != nil {
return errors.WithStack(err)
}
return nil
case err != nil:
logger.Error(ctx, "could not contact server", logger.E(errors.WithStack(err)))
return nil
}
return nil
}
func (c *Controller) reconcileAgent(ctx context.Context, state *agent.State, agent *datastore.Agent) error {
func (c *Controller) reconcileAgent(ctx context.Context, client *client.Client, state *agent.State, agent *datastore.Agent) error {
ctx = logger.With(ctx, logger.F("agentID", agent.ID))
if agent.Status != datastore.AgentStatusAccepted {
@ -82,7 +52,7 @@ func (c *Controller) reconcileAgent(ctx context.Context, state *agent.State, age
return nil
}
specs, err := c.client.GetAgentSpecs(ctx, agent.ID)
specs, err := client.GetAgentSpecs(ctx, agent.ID)
if err != nil {
logger.Error(ctx, "could not retrieve agent specs", logger.E(errors.WithStack(err)))
@ -98,19 +68,8 @@ func (c *Controller) reconcileAgent(ctx context.Context, state *agent.State, age
return nil
}
func NewController(serverURL string) *Controller {
client := client.New(serverURL)
return &Controller{client}
}
func isAPIError(err error, code api.ErrorCode) (bool, any) {
apiError := &api.Error{}
if errors.As(err, &apiError) && apiError.Code == code {
return true, apiError.Data
}
return false, nil
func NewController() *Controller {
return &Controller{}
}
var _ agent.Controller = &Controller{}

View File

@ -0,0 +1,12 @@
package metadata
import (
"context"
"errors"
)
var ErrMetadataNotAvailable = errors.New("metadata not available")
type Collector interface {
Collect(context.Context) (string, string, error)
}

View File

@ -0,0 +1,31 @@
package buildinfo
import (
"context"
"runtime/debug"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"github.com/pkg/errors"
)
const (
MetadataBuildInfo = "buildinfo"
)
type Collector struct{}
// Collect implements agent.MetadataCollector
func (c *Collector) Collect(ctx context.Context) (string, string, error) {
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return "", "", errors.WithStack(metadata.ErrMetadataNotAvailable)
}
return MetadataBuildInfo, buildInfo.String(), nil
}
func NewCollector() *Collector {
return &Collector{}
}
var _ metadata.Collector = &Collector{}

View File

@ -0,0 +1,46 @@
package shell
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"github.com/pkg/errors"
)
type Collector struct {
name string
command string
args []string
}
// Collect implements agent.MetadataCollector
func (c *Collector) Collect(ctx context.Context) (string, string, error) {
cmd := exec.CommandContext(ctx, c.command, c.args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", "", errors.WithStack(err)
}
value := strings.TrimSpace(buf.String())
return c.name, value, nil
}
func NewCollector(name string, command string, args ...string) *Collector {
return &Collector{
name: name,
command: command,
args: args,
}
}
var _ metadata.Collector = &Collector{}

View File

@ -0,0 +1,3 @@
package metadata
type Metadata map[string]any

View File

@ -0,0 +1,37 @@
package metadata
import (
"sort"
)
type Tuple struct {
Key string `json:"key"`
Value any `json:"value"`
}
func Sort(metadata map[string]any) []Tuple {
keys := make([]string, 0, len(metadata))
for k := range metadata {
keys = append(keys, k)
}
sort.Strings(keys)
tuples := make([]Tuple, len(keys))
for i, k := range keys {
tuples[i] = Tuple{k, metadata[k]}
}
return tuples
}
func FromSorted(tuples []Tuple) map[string]any {
metadata := make(map[string]any)
for _, t := range tuples {
metadata[t.Key] = t.Value
}
return metadata
}

View File

@ -1,31 +0,0 @@
package agent
import (
"time"
)
type Option struct {
Interval time.Duration
Controllers []Controller
}
type OptionFunc func(*Option)
func defaultOption() *Option {
return &Option{
Controllers: make([]Controller, 0),
Interval: 10 * time.Second,
}
}
func WithControllers(controllers ...Controller) OptionFunc {
return func(opt *Option) {
opt.Controllers = controllers
}
}
func WithInterval(interval time.Duration) OptionFunc {
return func(opt *Option) {
opt.Interval = interval
}
}

43
internal/agent/options.go Normal file
View File

@ -0,0 +1,43 @@
package agent
import (
"time"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/client"
)
type Options struct {
Client *client.Client
Interval time.Duration
Controllers []Controller
Collectors []metadata.Collector
}
type OptionFunc func(*Options)
func defaultOption() *Options {
return &Options{
Controllers: make([]Controller, 0),
Interval: 10 * time.Second,
Collectors: make([]metadata.Collector, 0),
}
}
func WithControllers(controllers ...Controller) OptionFunc {
return func(opt *Options) {
opt.Controllers = controllers
}
}
func WithInterval(interval time.Duration) OptionFunc {
return func(opt *Options) {
opt.Interval = interval
}
}
func WithCollectors(collectors ...metadata.Collector) OptionFunc {
return func(opts *Options) {
opts.Collectors = collectors
}
}

View File

@ -3,6 +3,7 @@ package agent
import (
"encoding/json"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
@ -13,7 +14,8 @@ var ErrSpecNotFound = errors.New("spec not found")
type Specs map[spec.Name]spec.Spec
type State struct {
specs Specs `json:"specs"`
agentID datastore.AgentID
specs Specs
}
func NewState() *State {
@ -24,8 +26,10 @@ func NewState() *State {
func (s *State) MarshalJSON() ([]byte, error) {
state := struct {
ID datastore.AgentID `json:"agentId"`
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
}{
ID: s.agentID,
Specs: func(specs map[spec.Name]spec.Spec) map[spec.Name]*spec.RawSpec {
rawSpecs := make(map[spec.Name]*spec.RawSpec)
@ -51,6 +55,7 @@ func (s *State) MarshalJSON() ([]byte, error) {
func (s *State) UnmarshalJSON(data []byte) error {
state := struct {
AgentID datastore.AgentID `json:"agentId"`
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
}{}
@ -71,6 +76,10 @@ func (s *State) UnmarshalJSON(data []byte) error {
return nil
}
func (s *State) AgentID() datastore.AgentID {
return s.agentID
}
func (s *State) Specs() Specs {
return s.specs
}

View File

@ -14,6 +14,7 @@ import (
type Client struct {
http *http.Client
token string
serverURL string
}
@ -33,6 +34,22 @@ func (c *Client) apiPost(ctx context.Context, path string, payload any, result a
return nil
}
func (c *Client) apiPut(ctx context.Context, path string, payload any, result any) error {
if err := c.apiDo(ctx, http.MethodPut, path, payload, result); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Client) apiDelete(ctx context.Context, path string, payload any, result any) error {
if err := c.apiDo(ctx, http.MethodDelete, path, payload, result); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Client) apiDo(ctx context.Context, method string, path string, payload any, response any) error {
url := c.serverURL + path

View File

@ -14,7 +14,7 @@ type QueryAgentsOptionFunc func(*QueryAgentsOptions)
type QueryAgentsOptions struct {
Limit *int
Offset *int
RemoteIDs []string
Thumbprints []string
IDs []datastore.AgentID
Statuses []datastore.AgentStatus
}
@ -31,9 +31,9 @@ func WithQueryAgentsOffset(offset int) QueryAgentsOptionFunc {
}
}
func WithQueryAgentsRemoteID(remoteIDs ...string) QueryAgentsOptionFunc {
func WithQueryAgentsThumbprints(thumbprints ...string) QueryAgentsOptionFunc {
return func(opts *QueryAgentsOptions) {
opts.RemoteIDs = remoteIDs
opts.Thumbprints = thumbprints
}
}
@ -61,11 +61,11 @@ func (c *Client) QueryAgents(ctx context.Context, funcs ...QueryAgentsOptionFunc
query.Set("ids", joinSlice(options.IDs))
}
if options.RemoteIDs != nil && len(options.RemoteIDs) > 0 {
query.Set("remoteIds", joinSlice(options.RemoteIDs))
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
query.Set("thumbprints", joinSlice(options.Thumbprints))
}
if options.Statuses != nil && len(options.RemoteIDs) > 0 {
if options.Statuses != nil && len(options.Statuses) > 0 {
query.Set("statuses", joinSlice(options.Statuses))
}

View File

@ -3,15 +3,33 @@ package client
import (
"context"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"github.com/pkg/errors"
)
func (c *Client) RegisterAgent(ctx context.Context, remoteID string) (*datastore.Agent, error) {
func (c *Client) RegisterAgent(ctx context.Context, key jwk.Key, thumbprint string, meta []metadata.Tuple) (*datastore.Agent, error) {
keySet, err := jwk.PublicKeySet(key)
if err != nil {
return nil, errors.WithStack(err)
}
signature, err := jwk.Sign(key, thumbprint, meta)
if err != nil {
return nil, errors.WithStack(err)
}
payload := struct {
RemoteID string `json:"remoteId"`
KeySet jwk.Set `json:"keySet"`
Thumbprint string `json:"thumbprint"`
Metadata []metadata.Tuple `json:"metadata"`
Signature string `json:"signature"`
}{
RemoteID: remoteID,
Thumbprint: thumbprint,
Metadata: meta,
Signature: signature,
KeySet: keySet,
}
response := withResponse[struct {

View File

@ -0,0 +1,50 @@
package client
import (
"context"
"fmt"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/pkg/errors"
)
type UpdateAgentOptions struct {
Status *int
}
type UpdateAgentOptionFunc func(*UpdateAgentOptions)
func WithAgentStatus(status int) UpdateAgentOptionFunc {
return func(opts *UpdateAgentOptions) {
opts.Status = &status
}
}
func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, funcs ...UpdateAgentOptionFunc) (*datastore.Agent, error) {
opts := &UpdateAgentOptions{}
for _, fn := range funcs {
fn(opts)
}
payload := map[string]any{}
if opts.Status != nil {
payload["status"] = *opts.Status
}
response := withResponse[struct {
Agent *datastore.Agent `json:"agent"`
}]()
path := fmt.Sprintf("/api/v1/agents/%d", agentID)
if err := c.apiPut(ctx, path, payload, &response); err != nil {
return nil, errors.WithStack(err)
}
if response.Error != nil {
return nil, errors.WithStack(response.Error)
}
return response.Data.Agent, nil
}

View File

@ -0,0 +1,39 @@
package client
import (
"context"
"fmt"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
func (c *Client) UpdateAgentSpec(ctx context.Context, agentID datastore.AgentID, spc spec.Spec) (*datastore.Spec, error) {
payload := struct {
Name spec.Name `json:"name"`
Revision int `json:"revision"`
Data metadata.Metadata `json:"data"`
}{
Name: spc.SpecName(),
Revision: spc.SpecRevision(),
Data: spc.SpecData(),
}
response := withResponse[struct {
Spec *datastore.Spec `json:"spec"`
}]()
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
if err := c.apiPost(ctx, path, payload, &response); err != nil {
return nil, errors.WithStack(err)
}
if response.Error != nil {
return nil, errors.WithStack(response.Error)
}
return response.Data.Spec, nil
}

View File

@ -2,6 +2,7 @@ package agent
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/agent/openwrt"
"forge.cadoles.com/Cadoles/emissary/internal/command/config"
"github.com/urfave/cli/v2"
)
@ -11,6 +12,7 @@ func Root() *cli.Command {
Usage: "Agent related commands",
Subcommands: []*cli.Command{
openwrt.Root(),
config.Root(),
RunCommand(),
},
}

View File

@ -8,7 +8,13 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/spec"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/buildinfo"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/shell"
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/config"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"forge.cadoles.com/Cadoles/emissary/internal/machineid"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
@ -40,7 +46,7 @@ func RunCommand() *cli.Command {
}
if ctrlConf.Spec.Enabled {
controllers = append(controllers, spec.NewController(string(ctrlConf.Spec.ServerURL)))
controllers = append(controllers, spec.NewController())
}
if ctrlConf.Gateway.Enabled {
@ -53,9 +59,26 @@ func RunCommand() *cli.Command {
))
}
key, err := jwk.LoadOrGenerate(string(conf.Agent.PrivateKeyPath), jwk.DefaultKeySize)
if err != nil {
return errors.WithStack(err)
}
thumbprint, err := machineid.Get()
if err != nil {
return errors.WithStack(err)
}
collectors := createShellCollectors(&conf.Agent)
collectors = append(collectors, buildinfo.NewCollector())
agent := agent.New(
string(conf.Agent.ServerURL),
key,
thumbprint,
agent.WithInterval(time.Duration(conf.Agent.ReconciliationInterval)*time.Second),
agent.WithControllers(controllers...),
agent.WithCollectors(collectors...),
)
if err := agent.Run(ctx.Context); err != nil {
@ -66,3 +89,15 @@ func RunCommand() *cli.Command {
},
}
}
func createShellCollectors(conf *config.AgentConfig) []metadata.Collector {
collectors := make([]metadata.Collector, 0)
for _, c := range conf.Collectors {
collector := shell.NewCollector(string(c.Name), string(c.Command), c.Args...)
collectors = append(collectors, collector)
}
return collectors
}

View File

@ -0,0 +1,47 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func CountCommand() *cli.Command {
return &cli.Command{
Name: "count",
Usage: "Count agents",
Flags: clientFlag.ComposeFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
client := client.New(baseFlags.ServerURL)
_, total, err := client.QueryAgents(ctx.Context)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
results := []struct {
Total int `json:"total"`
}{
{
Total: total,
},
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(results)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,34 @@
package flag
import (
"errors"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/urfave/cli/v2"
)
func WithAgentFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := clientFlag.ComposeFlags(
&cli.Int64Flag{
Name: "agent-id",
Aliases: []string{"a"},
Usage: "use `AGENT_ID` as selected agent",
Value: -1,
},
)
flags = append(flags, baseFlags...)
return flags
}
func AssertAgentID(ctx *cli.Context) (datastore.AgentID, error) {
rawAgentID := ctx.Int64("agent-id")
if rawAgentID == -1 {
return -1, errors.New("flag 'agent-id' is required")
}
return datastore.AgentID(rawAgentID), nil
}

View File

@ -0,0 +1,44 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func GetCommand() *cli.Command {
return &cli.Command{
Name: "get",
Usage: "Get agent",
Flags: agentFlag.WithAgentFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
client := client.New(baseFlags.ServerURL)
agent, err := client.GetAgent(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,37 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func QueryCommand() *cli.Command {
return &cli.Command{
Name: "query",
Usage: "Query agents",
Flags: clientFlag.ComposeFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
client := client.New(baseFlags.ServerURL)
agents, _, err := client.QueryAgents(ctx.Context)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(agents)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,20 @@
package agent
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/spec"
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "agent",
Usage: "Agents related commands",
Subcommands: []*cli.Command{
QueryCommand(),
CountCommand(),
UpdateCommand(),
GetCommand(),
spec.Root(),
},
}
}

View File

@ -0,0 +1,45 @@
package spec
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func GetCommand() *cli.Command {
return &cli.Command{
Name: "get",
Usage: "Get agent specifications",
Flags: agentFlag.WithAgentFlags(),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
client := client.New(baseFlags.ServerURL)
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(specs)...); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,16 @@
package spec
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "spec",
Usage: "Specifications related commands",
Subcommands: []*cli.Command{
GetCommand(),
UpdateCommand(),
},
}
}

View File

@ -0,0 +1,180 @@
package spec
import (
"encoding/json"
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func UpdateCommand() *cli.Command {
return &cli.Command{
Name: "update",
Usage: "Update agent specification",
Flags: agentFlag.WithAgentFlags(
&cli.StringFlag{
Name: "spec-name",
Usage: "use `NAME` as spec name",
},
&cli.StringFlag{
Name: "spec-data",
Usage: "use `DATA` as spec data, '-' to read from STDIN",
},
&cli.BoolFlag{
Name: "no-patch",
Usage: "Dont use spec-data as a patch to existing specification",
},
&cli.IntFlag{
Name: "revision",
Usage: "Use `REVISION` as specification revision number",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
specName, err := assertSpecName(ctx)
if err != nil {
return errors.WithStack(err)
}
specData, err := assertSpecData(ctx)
if err != nil {
return errors.WithStack(err)
}
noPatch := ctx.Bool("no-patch")
client := client.New(baseFlags.ServerURL)
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
var existingSpec spec.Spec
for _, s := range specs {
if s.SpecName() != specName {
continue
}
existingSpec = s
break
}
revision := 0
if existingSpec != nil {
originSpecData := existingSpec.SpecData()
if !noPatch {
specData, err = applyPatch(originSpecData, specData)
if err != nil {
return errors.WithStack(err)
}
}
revision = existingSpec.SpecRevision()
}
if specificRevision := ctx.Int("revision"); specificRevision != 0 {
revision = specificRevision
}
rawSpec := &spec.RawSpec{
Name: specName,
Revision: revision,
Data: specData,
}
if err := spec.Validate(ctx.Context, rawSpec); err != nil {
return errors.WithStack(apierr.Wrap(err))
}
spec, err := client.UpdateAgentSpec(ctx.Context, agentID, rawSpec)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := format.Hints{
OutputMode: baseFlags.OutputMode,
}
if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
func assertSpecName(ctx *cli.Context) (spec.Name, error) {
specName := ctx.String("spec-name")
if specName == "" {
return "", errors.New("flag 'spec-name' is required")
}
return spec.Name(specName), nil
}
func assertSpecData(ctx *cli.Context) (map[string]any, error) {
rawSpecData := ctx.String("spec-data")
if rawSpecData == "" {
return nil, errors.New("flag 'spec-data' is required")
}
var specData map[string]any
if rawSpecData == "-" {
decoder := json.NewDecoder(os.Stdin)
if err := decoder.Decode(&specData); err != nil {
return nil, errors.WithStack(err)
}
} else {
if err := json.Unmarshal([]byte(rawSpecData), &specData); err != nil {
return nil, errors.WithStack(err)
}
}
return specData, nil
}
func applyPatch(origin any, patch any) (map[string]any, error) {
originJSON, err := json.Marshal(origin)
if err != nil {
return nil, errors.WithStack(err)
}
patchJSON, err := json.Marshal(patch)
if err != nil {
return nil, errors.WithStack(err)
}
result, err := jsonpatch.MergePatch(originJSON, patchJSON)
if err != nil {
return nil, errors.WithStack(err)
}
var specData map[string]any
if err := json.Unmarshal(result, &specData); err != nil {
}
return specData, nil
}

View File

@ -0,0 +1,57 @@
package agent
import (
"os"
"forge.cadoles.com/Cadoles/emissary/internal/client"
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func UpdateCommand() *cli.Command {
return &cli.Command{
Name: "update",
Usage: "Updata agent",
Flags: agentFlag.WithAgentFlags(
&cli.IntFlag{
Name: "status",
Usage: "Set `STATUS` to selected agent",
Value: -1,
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
agentID, err := agentFlag.AssertAgentID(ctx)
if err != nil {
return errors.WithStack(err)
}
options := make([]client.UpdateAgentOptionFunc, 0)
status := ctx.Int("status")
if status != -1 {
options = append(options, client.WithAgentStatus(status))
}
client := client.New(baseFlags.ServerURL)
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}
hints := agentHints(baseFlags.OutputMode)
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

View File

@ -0,0 +1,16 @@
package agent
import "forge.cadoles.com/Cadoles/emissary/internal/format"
func agentHints(outputMode format.OutputMode) format.Hints {
return format.Hints{
OutputMode: outputMode,
Props: []format.Prop{
format.NewProp("ID", "ID"),
format.NewProp("Thumbprint", "Thumbprint"),
format.NewProp("Status", "Status"),
format.NewProp("CreatedAt", "CreatedAt"),
format.NewProp("UpdatedAt", "UpdatedAt"),
},
}
}

View File

@ -0,0 +1,91 @@
package apierr
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
)
func Wrap(err error) error {
apiErr := &api.Error{}
if !errors.As(err, &apiErr) {
return err
}
switch apiErr.Code {
case api.ErrCodeInvalidFieldValue:
return wrapInvalidFieldValueErr(apiErr)
default:
return wrapApiErrorWithMessage(apiErr)
}
}
func wrapApiErrorWithMessage(err *api.Error) error {
data, ok := err.Data.(map[string]any)
if !ok {
return err
}
rawMessage, exists := data["message"]
if !exists {
return err
}
message, ok := rawMessage.(string)
if !ok {
return err
}
return errors.Wrapf(err, message)
}
func wrapInvalidFieldValueErr(err *api.Error) error {
data, ok := err.Data.(map[string]any)
if !ok {
return err
}
rawFields, exists := data["Fields"]
if !exists {
return err
}
fields, ok := rawFields.([]any)
if !ok {
return err
}
var (
field string
rule string
)
if len(fields) == 0 {
return err
}
firstField, ok := fields[0].(map[string]any)
if !ok {
return err
}
param, ok := firstField["Param"].(string)
if !ok {
return err
}
tag, ok := firstField["Tag"].(string)
if !ok {
return err
}
fieldName, ok := firstField["Field"].(string)
if !ok {
return err
}
field = fieldName
rule = tag + "=" + param
return errors.Wrapf(err, "server expected field '%s' to match rule '%s'", field, rule)
}

View File

@ -0,0 +1,54 @@
package flag
import (
"fmt"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"forge.cadoles.com/Cadoles/emissary/internal/format/table"
"github.com/urfave/cli/v2"
)
func ComposeFlags(flags ...cli.Flag) []cli.Flag {
baseFlags := []cli.Flag{
&cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Usage: "use `SERVER` as server url",
Value: "http://127.0.0.1:3000",
},
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: fmt.Sprintf("use `FORMAT` as output format (available: %s)", format.Available()),
Value: string(table.Format),
},
&cli.StringFlag{
Name: "output-mode",
Aliases: []string{"m"},
Usage: fmt.Sprintf("use `MODE` as output mode (available: %s)", []format.OutputMode{format.OutputModeCompact, format.OutputModeWide}),
Value: string(format.OutputModeCompact),
},
}
flags = append(flags, baseFlags...)
return flags
}
type BaseFlags struct {
ServerURL string
Format format.Format
OutputMode format.OutputMode
}
func GetBaseFlags(ctx *cli.Context) *BaseFlags {
serverURL := ctx.String("server")
rawFormat := ctx.String("format")
rawOutputMode := ctx.String("output-mode")
return &BaseFlags{
ServerURL: serverURL,
Format: format.Format(rawFormat),
OutputMode: format.OutputMode(rawOutputMode),
}
}

View File

@ -0,0 +1,11 @@
package flag
func AsAnySlice[T any](src []T) []any {
dst := make([]any, len(src))
for i, s := range src {
dst[i] = s
}
return dst
}

View File

@ -0,0 +1,20 @@
package client
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/client/agent"
"github.com/urfave/cli/v2"
// Output format
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
)
func Root() *cli.Command {
return &cli.Command{
Name: "client",
Usage: "Client related commands",
Subcommands: []*cli.Command{
agent.Root(),
},
}
}

View File

@ -9,6 +9,10 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Spec validation
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/gateway"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) {

View File

@ -1,8 +1,17 @@
package config
type AgentConfig struct {
ServerURL InterpolatedString `yaml:"serverUrl"`
PrivateKeyPath InterpolatedString `yaml:"privateKeyPath"`
ReconciliationInterval InterpolatedInt `yaml:"reconciliationInterval"`
Controllers ControllersConfig `yaml:"controllers"`
Collectors []ShellCollectorConfig `yaml:"collectors"`
}
type ShellCollectorConfig struct {
Name InterpolatedString `yaml:"name"`
Command InterpolatedString `yaml:"command"`
Args InterpolatedStringSlice `yaml:"args"`
}
type ControllersConfig struct {
@ -19,9 +28,7 @@ type PersistenceControllerConfig struct {
type SpecControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
ServerURL InterpolatedString `yaml:"serverUrl"`
}
type GatewayControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
}
@ -34,11 +41,12 @@ type UCIControllerConfig struct {
func NewDefaultAgentConfig() AgentConfig {
return AgentConfig{
ServerURL: "http://127.0.0.1:3000",
PrivateKeyPath: "agent-key.json",
ReconciliationInterval: 5,
Controllers: ControllersConfig{
Spec: SpecControllerConfig{
Enabled: true,
ServerURL: "http://127.0.0.1:3000",
},
Persistence: PersistenceControllerConfig{
Enabled: true,
@ -53,5 +61,12 @@ func NewDefaultAgentConfig() AgentConfig {
BinPath: "uci",
},
},
Collectors: []ShellCollectorConfig{
{
Name: "uname",
Command: "uname",
Args: []string{"-a"},
},
},
}
}

View File

@ -1,7 +1,11 @@
package datastore
import (
"encoding/json"
"time"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
)
type AgentID int64
@ -17,8 +21,35 @@ const (
type Agent struct {
ID AgentID `json:"id"`
RemoteID string `json:"remoteId"`
Thumbprint string `json:"thumbprint"`
KeySet *SerializableKeySet `json:"keyset,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Status AgentStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type SerializableKeySet struct {
jwk.Set
}
func (s *SerializableKeySet) UnmarshalJSON(data []byte) error {
keySet := jwk.NewSet()
if err := json.Unmarshal(data, &keySet); err != nil {
return errors.WithStack(err)
}
s.Set = keySet
return nil
}
func (s *SerializableKeySet) MarshalJSON() ([]byte, error) {
data, err := json.Marshal(s.Set)
if err != nil {
return nil, errors.WithStack(err)
}
return data, nil
}

View File

@ -1,9 +1,13 @@
package datastore
import "context"
import (
"context"
"github.com/lestrrat-go/jwx/v2/jwk"
)
type AgentRepository interface {
Create(ctx context.Context, remoteID string, state AgentStatus) (*Agent, error)
Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*Agent, error)
Get(ctx context.Context, id AgentID) (*Agent, error)
Update(ctx context.Context, id AgentID, updates ...AgentUpdateOptionFunc) (*Agent, error)
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
@ -19,8 +23,9 @@ type AgentQueryOptionFunc func(*AgentQueryOptions)
type AgentQueryOptions struct {
Limit *int
Offset *int
RemoteIDs []string
IDs []AgentID
Thumbprints []string
Metadata *map[string]any
Statuses []AgentStatus
}
@ -36,9 +41,9 @@ func WithAgentQueryOffset(offset int) AgentQueryOptionFunc {
}
}
func WithAgentQueryRemoteID(remoteIDs ...string) AgentQueryOptionFunc {
func WithAgentQueryMetadata(metadata map[string]any) AgentQueryOptionFunc {
return func(opts *AgentQueryOptions) {
opts.RemoteIDs = remoteIDs
opts.Metadata = &metadata
}
}
@ -54,10 +59,19 @@ func WithAgentQueryStatus(statuses ...AgentStatus) AgentQueryOptionFunc {
}
}
func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
return func(opts *AgentQueryOptions) {
opts.Thumbprints = thumbprints
}
}
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
type AgentUpdateOptions struct {
Status *AgentStatus
Metadata *map[string]any
KeySet *jwk.Set
Thumbprint *string
}
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
@ -65,3 +79,21 @@ func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
opts.Status = &status
}
}
func WithAgentUpdateMetadata(metadata map[string]any) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.Metadata = &metadata
}
}
func WithAgentUpdateKeySet(keySet jwk.Set) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.KeySet = &keySet
}
}
func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.Thumbprint = &thumbprint
}
}

View File

@ -4,7 +4,6 @@ import (
"time"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
type SpecID int64
@ -26,10 +25,6 @@ func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() any {
func (s *Spec) SpecData() map[string]any {
return s.Data
}
func (s *Spec) SpecValid() (bool, error) {
return false, errors.WithStack(spec.ErrSchemaUnknown)
}

View File

@ -3,10 +3,13 @@ package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -116,7 +119,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
count := 0
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `SELECT id, remote_id, status, created_at, updated_at FROM agents`
query := `SELECT id, thumbprint, status, created_at, updated_at FROM agents`
limit := 10
if options.Limit != nil {
@ -133,20 +136,18 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
args := []any{offset, limit}
if options.IDs != nil && len(options.IDs) > 0 {
filters += "id in ("
filter, newArgs, newParamIndex := inFilter("id", paramIndex, options.RemoteIDs)
filter, newArgs, newParamIndex := inFilter("id", paramIndex, options.IDs)
filters += filter
paramIndex = newParamIndex
args = append(args, newArgs...)
}
if options.RemoteIDs != nil && len(options.RemoteIDs) > 0 {
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
if filters != "" {
filters += " AND "
}
filter, newArgs, newParamIndex := inFilter("remote_id", paramIndex, options.RemoteIDs)
filter, newArgs, newParamIndex := inFilter("thumbprint", paramIndex, options.Thumbprints)
filters += filter
paramIndex = newParamIndex
args = append(args, newArgs...)
@ -180,10 +181,14 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
for rows.Next() {
agent := &datastore.Agent{}
if err := rows.Scan(&agent.ID, &agent.RemoteID, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
metadata := JSONMap{}
if err := rows.Scan(&agent.ID, &agent.Thumbprint, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
return errors.WithStack(err)
}
agent.Metadata = metadata
agents = append(agents, agent)
}
@ -202,12 +207,12 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
}
// Create implements datastore.AgentRepository
func (r *AgentRepository) Create(ctx context.Context, remoteID string, status datastore.AgentStatus) (*datastore.Agent, error) {
func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*datastore.Agent, error) {
agent := &datastore.Agent{}
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `SELECT count(id) FROM agents WHERE remote_id = $1`
row := tx.QueryRowContext(ctx, query, remoteID)
query := `SELECT count(id) FROM agents WHERE thumbprint = $1`
row := tx.QueryRowContext(ctx, query, thumbprint)
var count int
@ -222,21 +227,37 @@ func (r *AgentRepository) Create(ctx context.Context, remoteID string, status da
now := time.Now().UTC()
query = `
INSERT INTO agents (remote_id, status, created_at, updated_at)
VALUES($1, $2, $3, $3)
RETURNING "id", "remote_id", "status", "created_at", "updated_at"
INSERT INTO agents (thumbprint, keyset, metadata, status, created_at, updated_at)
VALUES($1, $2, $3, $4, $5, $5)
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
`
row = tx.QueryRowContext(
ctx, query,
remoteID, status, now,
)
err := row.Scan(&agent.ID, &agent.RemoteID, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt)
rawKeySet, err := json.Marshal(keySet)
if err != nil {
return errors.WithStack(err)
}
row = tx.QueryRowContext(
ctx, query,
thumbprint, rawKeySet, JSONMap(metadata), datastore.AgentStatusPending, now,
)
metadata := JSONMap{}
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt)
if err != nil {
return errors.WithStack(err)
}
agent.Metadata = metadata
keySet, err = jwk.Parse(rawKeySet)
if err != nil {
return errors.WithStack(err)
}
agent.KeySet = &datastore.SerializableKeySet{keySet}
return nil
})
if err != nil {
@ -266,14 +287,17 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
SELECT "remote_id", "status", "created_at", "updated_at"
SELECT "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
FROM agents
WHERE id = $1
`
row := r.db.QueryRowContext(ctx, query, id)
if err := row.Scan(&agent.RemoteID, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
metadata := JSONMap{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
@ -281,6 +305,15 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
return errors.WithStack(err)
}
agent.Metadata = metadata
keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
return errors.WithStack(err)
}
agent.KeySet = &datastore.SerializableKeySet{keySet}
return nil
})
if err != nil {
@ -313,23 +346,60 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
if options.Status != nil {
query += fmt.Sprintf(`, status = $%d`, index)
args = append(args, *options.Status)
index++
}
if options.KeySet != nil {
rawKeySet, err := json.Marshal(*options.KeySet)
if err != nil {
return errors.WithStack(err)
}
query += fmt.Sprintf(`, keyset = $%d`, index)
args = append(args, rawKeySet)
index++
}
if options.Thumbprint != nil {
query += fmt.Sprintf(`, thumbprint = $%d`, index)
args = append(args, *options.Thumbprint)
index++
}
if options.Metadata != nil {
query += fmt.Sprintf(`, metadata = $%d`, index)
args = append(args, JSONMap(*options.Metadata))
index++
}
query += `
WHERE id = $1
RETURNING "id","remote_id","status","updated_at","created_at"
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
`
row := tx.QueryRowContext(ctx, query, args...)
if err := row.Scan(&agent.ID, &agent.RemoteID, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
metadata := JSONMap{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
return errors.WithStack(err)
}
agent.Metadata = metadata
keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
return errors.WithStack(err)
}
agent.KeySet = &datastore.SerializableKeySet{keySet}
return nil
})
if err != nil {

View File

@ -0,0 +1,38 @@
package json
import (
"encoding/json"
"io"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
)
const Format format.Format = "json"
func init() {
format.Register(Format, NewWriter())
}
type Writer struct{}
// Format implements format.Writer.
func (*Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
encoder := json.NewEncoder(writer)
if hints.OutputMode == format.OutputModeWide {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(data); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewWriter() *Writer {
return &Writer{}
}
var _ format.Writer = &Writer{}

18
internal/format/prop.go Normal file
View File

@ -0,0 +1,18 @@
package format
type Prop struct {
name string
label string
}
func (p *Prop) Name() string {
return p.name
}
func (p *Prop) Label() string {
return p.label
}
func NewProp(name, label string) Prop {
return Prop{name, label}
}

View File

@ -0,0 +1,46 @@
package format
import (
"io"
"github.com/pkg/errors"
)
type Format string
type Registry map[Format]Writer
var defaultRegistry = Registry{}
var ErrUnknownFormat = errors.New("unknown format")
func Write(format Format, writer io.Writer, hints Hints, data ...any) error {
formatWriter, exists := defaultRegistry[format]
if !exists {
return errors.WithStack(ErrUnknownFormat)
}
if hints.OutputMode == "" {
hints.OutputMode = OutputModeCompact
}
if err := formatWriter.Write(writer, hints, data...); err != nil {
return errors.WithStack(err)
}
return nil
}
func Available() []Format {
formats := make([]Format, 0, len(defaultRegistry))
for f := range defaultRegistry {
formats = append(formats, f)
}
return formats
}
func Register(format Format, writer Writer) {
defaultRegistry[format] = writer
}

View File

@ -0,0 +1,49 @@
package table
import (
"encoding/json"
"fmt"
"reflect"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
)
func getProps(d any) []format.Prop {
props := make([]format.Prop, 0)
v := reflect.Indirect(reflect.ValueOf(d))
typeOf := v.Type()
for i := 0; i < v.NumField(); i++ {
name := typeOf.Field(i).Name
props = append(props, format.NewProp(name, name))
}
return props
}
func getFieldValue(obj any, name string) string {
v := reflect.Indirect(reflect.ValueOf(obj))
fieldValue := v.FieldByName(name)
switch fieldValue.Kind() {
case reflect.Map:
fallthrough
case reflect.Struct:
fallthrough
case reflect.Slice:
fallthrough
case reflect.Interface:
json, err := json.Marshal(fieldValue.Interface())
if err != nil {
panic(errors.WithStack(err))
}
return string(json)
default:
return fmt.Sprintf("%v", fieldValue.Interface())
}
}

View File

@ -0,0 +1,75 @@
package table
import (
"io"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/jedib0t/go-pretty/v6/table"
)
const Format format.Format = "table"
const DefaultCompactModeMaxColumnWidth = 30
func init() {
format.Register(Format, NewWriter(DefaultCompactModeMaxColumnWidth))
}
type Writer struct {
compactModeMaxColumnWidth int
}
// Write implements format.Writer.
func (w *Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
t := table.NewWriter()
t.SetOutputMirror(writer)
var props []format.Prop
if hints.Props != nil {
props = hints.Props
} else {
if len(data) > 0 {
props = getProps(data[0])
} else {
props = make([]format.Prop, 0)
}
}
labels := table.Row{}
for _, p := range props {
labels = append(labels, p.Label())
}
t.AppendHeader(labels)
isCompactMode := hints.OutputMode == format.OutputModeCompact
for _, d := range data {
row := table.Row{}
for _, p := range props {
value := getFieldValue(d, p.Name())
if isCompactMode && len(value) > w.compactModeMaxColumnWidth {
value = value[:w.compactModeMaxColumnWidth] + "..."
}
row = append(row, value)
}
t.AppendRow(row)
}
t.Render()
return nil
}
func NewWriter(compactModeMaxColumnWidth int) *Writer {
return &Writer{compactModeMaxColumnWidth}
}
var _ format.Writer = &Writer{}

View File

@ -0,0 +1,86 @@
package table
import (
"bytes"
"strings"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
)
type dummyItem struct {
MyString string
MyInt int
MySub subItem
}
type subItem struct {
MyBool bool
}
var dummyItems = []any{
dummyItem{
MyString: "Foo",
MyInt: 1,
MySub: subItem{
MyBool: false,
},
},
dummyItem{
MyString: "Bar",
MyInt: 0,
MySub: subItem{
MyBool: true,
},
},
}
func TestWriterNoHints(t *testing.T) {
var buf bytes.Buffer
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
if err := writer.Write(&buf, format.Hints{}, dummyItems...); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
expected := `+----------+-------+------------------+
| MYSTRING | MYINT | MYSUB |
+----------+-------+------------------+
| Foo | 1 | {"MyBool":false} |
| Bar | 0 | {"MyBool":true} |
+----------+-------+------------------+`
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
}
}
func TestWriterWithPropHints(t *testing.T) {
var buf bytes.Buffer
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
hints := format.Hints{
Props: []format.Prop{
format.NewProp("MyString", "MyString"),
format.NewProp("MyInt", "MyInt"),
},
}
if err := writer.Write(&buf, hints, dummyItems...); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
expected := `+----------+-------+
| MYSTRING | MYINT |
+----------+-------+
| Foo | 1 |
| Bar | 0 |
+----------+-------+`
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
}
}

19
internal/format/writer.go Normal file
View File

@ -0,0 +1,19 @@
package format
import "io"
type OutputMode string
const (
OutputModeWide OutputMode = "wide"
OutputModeCompact OutputMode = "compact"
)
type Hints struct {
Props []Prop
OutputMode OutputMode
}
type Writer interface {
Write(writer io.Writer, hints Hints, data ...any) error
}

133
internal/jwk/jwk.go Normal file
View File

@ -0,0 +1,133 @@
package jwk
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"io/ioutil"
"os"
"github.com/btcsuite/btcd/btcutil/base58"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/pkg/errors"
)
const DefaultKeySize = 2048
type (
Key = jwk.Key
Set = jwk.Set
ParseOption = jwk.ParseOption
)
func Parse(src []byte, options ...jwk.ParseOption) (Set, error) {
return jwk.Parse(src, options...)
}
func PublicKeySet(keys ...jwk.Key) (jwk.Set, error) {
set := jwk.NewSet()
for _, k := range keys {
pubkey, err := k.PublicKey()
if err != nil {
return nil, errors.WithStack(err)
}
if err := pubkey.Set(jwk.AlgorithmKey, jwa.RS256); err != nil {
return nil, errors.WithStack(err)
}
if err := set.AddKey(pubkey); err != nil {
return nil, errors.WithStack(err)
}
}
return set, nil
}
func LoadOrGenerate(path string, size int) (jwk.Key, error) {
data, err := ioutil.ReadFile(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
if errors.Is(err, os.ErrNotExist) {
key, err := Generate(size)
if err != nil {
return nil, errors.WithStack(err)
}
data, err = json.Marshal(key)
if err != nil {
return nil, errors.WithStack(err)
}
if err := ioutil.WriteFile(path, data, 0o640); err != nil {
return nil, errors.WithStack(err)
}
}
key, err := jwk.ParseKey(data)
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func Generate(size int) (jwk.Key, error) {
privKey, err := rsa.GenerateKey(rand.Reader, size)
if err != nil {
return nil, errors.WithStack(err)
}
key, err := jwk.FromRaw(privKey)
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func Sign(key jwk.Key, payload ...any) (string, error) {
json, err := json.Marshal(payload)
if err != nil {
return "", errors.WithStack(err)
}
rawSignature, err := jws.Sign(
nil,
jws.WithKey(jwa.RS256, key),
jws.WithDetachedPayload(json),
)
if err != nil {
return "", errors.WithStack(err)
}
signature := base58.Encode(rawSignature)
return signature, nil
}
func Verify(jwks jwk.Set, signature string, payload ...any) (bool, error) {
json, err := json.Marshal(payload)
if err != nil {
return false, errors.WithStack(err)
}
decoded := base58.Decode(signature)
_, err = jws.Verify(
decoded,
jws.WithKeySet(jwks, jws.WithRequireKid(false)),
jws.WithDetachedPayload(json),
)
if err != nil {
return false, errors.WithStack(err)
}
return true, nil
}

40
internal/jwk/jwk_test.go Normal file
View File

@ -0,0 +1,40 @@
package jwk
import (
"testing"
"github.com/pkg/errors"
)
func TestJWK(t *testing.T) {
privateKey, err := Generate(DefaultKeySize)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
keySet, err := PublicKeySet(privateKey)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
metadata := map[string]any{
"Foo": "bar",
"Test": 1,
}
signature, err := Sign(privateKey, metadata)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
t.Logf("Signature: %s", signature)
matches, err := Verify(keySet, signature, metadata)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if !matches {
t.Error("signature should match")
}
}

View File

@ -1,11 +1,14 @@
package server
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
@ -15,11 +18,14 @@ import (
const (
ErrCodeUnknownError api.ErrorCode = "unknown-error"
ErrCodeNotFound api.ErrorCode = "not-found"
ErrCodeAlreadyRegistered api.ErrorCode = "already-registered"
ErrInvalidSignature api.ErrorCode = "invalid-signature"
)
type registerAgentRequest struct {
RemoteID string `json:"remoteId"`
KeySet json.RawMessage `json:"keySet" validate:"required"`
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
Thumbprint string `json:"thumbprint" validate:"required"`
Signature string `json:"signature" validate:"required"`
}
func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
@ -30,25 +36,81 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
agent, err := s.agentRepo.Create(
ctx,
registerAgentReq.RemoteID,
datastore.AgentStatusPending,
)
keySet, err := jwk.Parse(registerAgentReq.KeySet)
if err != nil {
if errors.Is(err, datastore.ErrAlreadyExist) {
logger.Error(ctx, "agent already registered", logger.F("remoteID", registerAgentReq.RemoteID))
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyRegistered, nil)
logger.Error(ctx, "could not parse key set", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
if err != nil {
logger.Error(ctx, "could not validate signature", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
if !validSignature {
logger.Error(ctx, "invalid signature", logger.F("signature", registerAgentReq.Signature))
api.ErrorResponse(w, http.StatusBadRequest, ErrInvalidSignature, nil)
return
}
metadata := metadata.FromSorted(registerAgentReq.Metadata)
agent, err := s.agentRepo.Create(
ctx,
registerAgentReq.Thumbprint,
keySet,
metadata,
)
if err != nil {
if !errors.Is(err, datastore.ErrAlreadyExist) {
logger.Error(ctx, "could not create agent", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
agents, _, err := s.agentRepo.Query(
ctx,
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
datastore.WithAgentQueryLimit(1),
)
if err != nil {
logger.Error(ctx, "could not retrieve agents", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
if len(agents) == 0 {
logger.Error(ctx, "could not retrieve matching agent", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
return
}
agent, err = s.agentRepo.Update(
ctx, agents[0].ID,
datastore.WithAgentUpdateKeySet(keySet),
datastore.WithAgentUpdateMetadata(metadata),
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
)
if err != nil {
logger.Error(ctx, "could not update agent", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
}
api.DataResponse(w, http.StatusCreated, struct {
Agent *datastore.Agent `json:"agent"`
}{
@ -57,7 +119,7 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
}
type updateAgentRequest struct {
Status *datastore.AgentStatus `json:"status"`
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
}
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
@ -132,13 +194,13 @@ func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
options = append(options, datastore.WithAgentQueryID(agentIDs...))
}
remoteIDs, ok := getStringSliceValues(w, r, "remoteIds", nil)
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
if !ok {
return
}
if remoteIDs != nil {
options = append(options, datastore.WithAgentQueryRemoteID(remoteIDs...))
if thumbprints != nil {
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
}
statuses, ok := getIntSliceValues(w, r, "statuses", nil)

View File

@ -5,6 +5,7 @@ import (
"strconv"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
@ -16,9 +17,7 @@ const (
)
type updateSpecRequest struct {
Name string `json:"name"`
Revision int `json:"revision"`
Data map[string]any `json:"data"`
spec.RawSpec
}
func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
@ -34,12 +33,29 @@ func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
return
}
if err := spec.Validate(ctx, updateSpecReq); err != nil {
data := struct {
Message string `json:"message"`
}{}
var validationErr *spec.ValidationError
if errors.As(err, &validationErr) {
data.Message = validationErr.Error()
}
logger.Error(ctx, "could not validate spec", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
return
}
spec, err := s.agentRepo.UpdateSpec(
ctx,
datastore.AgentID(agentID),
updateSpecReq.Name,
updateSpecReq.Revision,
updateSpecReq.Data,
string(updateSpecReq.SpecName()),
updateSpecReq.SpecRevision(),
updateSpecReq.SpecData(),
)
if err != nil {
if errors.Is(err, datastore.ErrUnexpectedRevision) {

View File

@ -1,5 +1,36 @@
package spec
import "errors"
import (
"strings"
var ErrSchemaUnknown = errors.New("schema unknown")
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
var ErrUnknownSchema = errors.New("unknown schema")
type ValidationError struct {
keyErrors []jsonschema.KeyError
}
func (e *ValidationError) Error() string {
var sb strings.Builder
if _, err := sb.WriteString("validation error: "); err != nil {
panic(errors.WithStack(err))
}
for i, err := range e.keyErrors {
if i != 0 {
if _, err := sb.WriteString(", "); err != nil {
panic(errors.WithStack(err))
}
}
if _, err := sb.WriteString(err.Error()); err != nil {
panic(errors.WithStack(err))
}
}
return sb.String()
}

View File

@ -1,35 +0,0 @@
package spec
const NameGateway Name = "gateway.emissary.cadoles.com"
type GatewayID string
type Gateway struct {
Revision int `json:"revision"`
Gateways map[GatewayID]GatewayEntry `json:"gateways"`
}
type GatewayEntry struct {
Address string `json:"address"`
Target string `json:"target"`
}
func (g *Gateway) SpecName() Name {
return NameGateway
}
func (g *Gateway) SpecRevision() int {
return g.Revision
}
func (g *Gateway) SpecData() any {
return struct {
Gateways map[GatewayID]GatewayEntry
}{Gateways: g.Gateways}
}
func NewGatewaySpec() *Gateway {
return &Gateway{
Gateways: make(map[GatewayID]GatewayEntry),
}
}

View File

@ -0,0 +1,17 @@
package gateway
import (
_ "embed"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
//go:embed schema.json
var schema []byte
func init() {
if err := spec.Register(NameGateway, schema); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gateway.emissary.cadoles.com/spec.json",
"title": "GatewaySpec",
"description": "Emissary 'Gateway' specification",
"type": "object",
"properties": {
"gateways": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"target": {
"type": "string"
}
},
"required": ["address", "target"],
"additionalProperties": false
}
}
}
},
"required": ["gateways"],
"additionalProperties": false
}

View File

@ -0,0 +1,37 @@
package gateway
import "forge.cadoles.com/Cadoles/emissary/internal/spec"
const NameGateway spec.Name = "gateway.emissary.cadoles.com"
type ID string
type Spec struct {
Revision int `json:"revision"`
Gateways map[ID]GatewayEntry `json:"gateways"`
}
type GatewayEntry struct {
Address string `json:"address"`
Target string `json:"target"`
}
func (s *Spec) SpecName() spec.Name {
return NameGateway
}
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() any {
return struct {
Gateways map[ID]GatewayEntry
}{Gateways: s.Gateways}
}
func NewSpec() *Spec {
return &Spec{
Gateways: make(map[ID]GatewayEntry),
}
}

View File

@ -0,0 +1,13 @@
{
"name": "gateway.emissary.cadoles.com",
"data": {
"gateways": {
"cadoles.com": {
"address": ":3003",
"target": "https://www.cadoles.com",
"foo": "bar"
}
}
},
"revision": 0
}

View File

@ -0,0 +1,11 @@
{
"name": "gateway.emissary.cadoles.com",
"data": {
"gateways": {
"cadoles.com": {
"address": ":3003"
}
}
},
"revision": 0
}

View File

@ -0,0 +1,12 @@
{
"name": "gateway.emissary.cadoles.com",
"data": {
"gateways": {
"cadoles.com": {
"address": ":3003",
"target": "https://www.cadoles.com"
}
}
},
"revision": 0
}

View File

@ -0,0 +1,75 @@
package gateway
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
type validatorTestCase struct {
Name string
Source string
ShouldFail bool
}
var validatorTestCases = []validatorTestCase{
{
Name: "SpecOK",
Source: "testdata/spec-ok.json",
ShouldFail: false,
},
{
Name: "SpecMissingProp",
Source: "testdata/spec-missing-prop.json",
ShouldFail: true,
},
{
Name: "SpecAdditionalProp",
Source: "testdata/spec-additional-prop.json",
ShouldFail: true,
},
}
func TestValidator(t *testing.T) {
t.Parallel()
validator := spec.NewValidator()
if err := validator.Register(NameGateway, schema); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
for _, tc := range validatorTestCases {
func(tc validatorTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
rawSpec, err := ioutil.ReadFile(tc.Source)
if err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
var spec spec.RawSpec
if err := json.Unmarshal(rawSpec, &spec); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
ctx := context.Background()
err = validator.Validate(ctx, &spec)
if !tc.ShouldFail && err != nil {
t.Errorf("+%v", errors.WithStack(err))
}
if tc.ShouldFail && err == nil {
t.Error("validation should have failed")
}
})
}(tc)
}
}

View File

@ -1,15 +1,17 @@
package spec
import "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
type Spec interface {
SpecName() Name
SpecRevision() int
SpecData() any
SpecData() metadata.Metadata
}
type RawSpec struct {
Name Name `json:"name"`
Revision int `json:"revision"`
Data any `json:"data"`
Data metadata.Metadata `json:"data"`
}
func (s *RawSpec) SpecName() Name {
@ -20,6 +22,6 @@ func (s *RawSpec) SpecRevision() int {
return s.Revision
}
func (s *RawSpec) SpecData() any {
func (s *RawSpec) SpecData() metadata.Metadata {
return s.Data
}

17
internal/spec/uci/init.go Normal file
View File

@ -0,0 +1,17 @@
package uci
import (
_ "embed"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
//go:embed schema.json
var schema []byte
func init() {
if err := spec.Register(NameUCI, schema); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -0,0 +1,97 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://uci.emissary.cadoles.com/spec.json",
"title": "UCISpec",
"description": "Emissary 'UCI' specification",
"type": "object",
"properties": {
"config": {
"type": "object",
"properties": {
"packages": {
"type": "array",
"items": {
"$ref": "#/$defs/package"
}
}
},
"required": ["packages"],
"additionalProperties": false
},
"postImportCommands": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["command", "args"],
"additionalProperties": false
}
}
},
"required": ["config", "postImportCommands"],
"additionalProperties": false,
"$defs": {
"package": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"configs": {
"type": "array",
"items": {
"$ref": "#/$defs/config"
}
}
},
"required": ["name", "configs"],
"additionalProperties": false
},
"config": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"section": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/$defs/option"
}
}
},
"required": ["name", "section", "options"],
"additionalProperties": false
},
"option": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["list", "option"]
},
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": ["type", "name", "value"],
"additionalProperties": false
}
}
}

View File

@ -1,10 +1,13 @@
package spec
package uci
import "forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
import (
"forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
)
const NameUCI Name = "uci.emissary.cadoles.com"
const NameUCI spec.Name = "uci.emissary.cadoles.com"
type UCI struct {
type Spec struct {
Revision int `json:"revisions"`
Config *uci.UCI `json:"config"`
PostImportCommands []*UCIPostImportCommand `json:"postImportCommands"`
@ -15,23 +18,23 @@ type UCIPostImportCommand struct {
Args []string `json:"args"`
}
func (u *UCI) SpecName() Name {
func (s *Spec) SpecName() spec.Name {
return NameUCI
}
func (u *UCI) SpecRevision() int {
return u.Revision
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (u *UCI) SpecData() any {
func (s *Spec) SpecData() any {
return struct {
Config *uci.UCI `json:"config"`
PostImportCommands []*UCIPostImportCommand `json:"postImportCommands"`
}{Config: u.Config, PostImportCommands: u.PostImportCommands}
}{Config: s.Config, PostImportCommands: s.PostImportCommands}
}
func NewUCISpec() *UCI {
return &UCI{
func NewSpec() *Spec {
return &Spec{
PostImportCommands: make([]*UCIPostImportCommand, 0),
}
}

View File

@ -0,0 +1,162 @@
{
"name": "uci.emissary.cadoles.com",
"data": {
"config": {
"packages": [
{
"name": "uhttpd",
"configs": [
{
"name": "uhttpd",
"section": "main",
"options": [
{
"type": "list",
"name": "listen_http",
"value": "0.0.0.0:8080"
},
{
"type": "list",
"name": "listen_http",
"value": "[::]:8080"
},
{
"type": "list",
"name": "listen_https",
"value": "0.0.0.0:8443"
},
{
"type": "list",
"name": "listen_https",
"value": "[::]:8443"
},
{
"type": "option",
"name": "redirect_https",
"value": "0"
},
{
"type": "option",
"name": "home",
"value": "/www"
},
{
"type": "option",
"name": "rfc1918_filter",
"value": "1"
},
{
"type": "option",
"name": "max_requests",
"value": "3"
},
{
"type": "option",
"name": "max_connections",
"value": "100"
},
{
"type": "option",
"name": "cert",
"value": "/etc/uhttpd.crt"
},
{
"type": "option",
"name": "key",
"value": "/etc/uhttpd.key"
},
{
"type": "option",
"name": "cgi_prefix",
"value": "/cgi-bin"
},
{
"type": "list",
"name": "lua_prefix",
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
},
{
"type": "option",
"name": "script_timeout",
"value": "60"
},
{
"type": "option",
"name": "network_timeout",
"value": "30"
},
{
"type": "option",
"name": "http_keepalive",
"value": "20"
},
{
"type": "option",
"name": "tcp_keepalive",
"value": "1"
},
{
"type": "option",
"name": "ubus_prefix"
}
]
},
{
"name": "cert",
"section": "defaults",
"options": [
{
"type": "option",
"name": "days",
"value": "730"
},
{
"type": "option",
"name": "key_type",
"value": "ec"
},
{
"type": "option",
"name": "bits",
"value": "2048"
},
{
"type": "option",
"name": "ec_curve",
"value": "P-256"
},
{
"type": "option",
"name": "country",
"value": "ZZ"
},
{
"type": "option",
"name": "state",
"value": "Somewhere"
},
{
"type": "option",
"name": "location",
"value": "Unknown"
},
{
"type": "option",
"name": "commonname",
"value": "OpenWrt"
}
]
}
]
}
]
},
"postImportCommands": [
{
"command": "reload_config",
"args": []
}
]
},
"revision": 0
}

163
internal/spec/uci/testdata/spec-ok.json vendored Normal file
View File

@ -0,0 +1,163 @@
{
"name": "uci.emissary.cadoles.com",
"data": {
"config": {
"packages": [
{
"name": "uhttpd",
"configs": [
{
"name": "uhttpd",
"section": "main",
"options": [
{
"type": "list",
"name": "listen_http",
"value": "0.0.0.0:8080"
},
{
"type": "list",
"name": "listen_http",
"value": "[::]:8080"
},
{
"type": "list",
"name": "listen_https",
"value": "0.0.0.0:8443"
},
{
"type": "list",
"name": "listen_https",
"value": "[::]:8443"
},
{
"type": "option",
"name": "redirect_https",
"value": "0"
},
{
"type": "option",
"name": "home",
"value": "/www"
},
{
"type": "option",
"name": "rfc1918_filter",
"value": "1"
},
{
"type": "option",
"name": "max_requests",
"value": "3"
},
{
"type": "option",
"name": "max_connections",
"value": "100"
},
{
"type": "option",
"name": "cert",
"value": "/etc/uhttpd.crt"
},
{
"type": "option",
"name": "key",
"value": "/etc/uhttpd.key"
},
{
"type": "option",
"name": "cgi_prefix",
"value": "/cgi-bin"
},
{
"type": "list",
"name": "lua_prefix",
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
},
{
"type": "option",
"name": "script_timeout",
"value": "60"
},
{
"type": "option",
"name": "network_timeout",
"value": "30"
},
{
"type": "option",
"name": "http_keepalive",
"value": "20"
},
{
"type": "option",
"name": "tcp_keepalive",
"value": "1"
},
{
"type": "option",
"name": "ubus_prefix",
"value": "/ubus"
}
]
},
{
"name": "cert",
"section": "defaults",
"options": [
{
"type": "option",
"name": "days",
"value": "730"
},
{
"type": "option",
"name": "key_type",
"value": "ec"
},
{
"type": "option",
"name": "bits",
"value": "2048"
},
{
"type": "option",
"name": "ec_curve",
"value": "P-256"
},
{
"type": "option",
"name": "country",
"value": "ZZ"
},
{
"type": "option",
"name": "state",
"value": "Somewhere"
},
{
"type": "option",
"name": "location",
"value": "Unknown"
},
{
"type": "option",
"name": "commonname",
"value": "OpenWrt"
}
]
}
]
}
]
},
"postImportCommands": [
{
"command": "reload_config",
"args": []
}
]
},
"revision": 0
}

View File

@ -0,0 +1,70 @@
package uci
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
type validatorTestCase struct {
Name string
Source string
ShouldFail bool
}
var validatorTestCases = []validatorTestCase{
{
Name: "SpecOK",
Source: "testdata/spec-ok.json",
ShouldFail: false,
},
{
Name: "SpecMissingProp",
Source: "testdata/spec-missing-prop.json",
ShouldFail: true,
},
}
func TestValidator(t *testing.T) {
t.Parallel()
validator := spec.NewValidator()
if err := validator.Register(NameUCI, schema); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
for _, tc := range validatorTestCases {
func(tc validatorTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
rawSpec, err := ioutil.ReadFile(tc.Source)
if err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
var spec spec.RawSpec
if err := json.Unmarshal(rawSpec, &spec); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
ctx := context.Background()
err = validator.Validate(ctx, &spec)
if !tc.ShouldFail && err != nil {
t.Errorf("+%v", errors.WithStack(err))
}
if tc.ShouldFail && err == nil {
t.Error("validation should have failed")
}
})
}(tc)
}
}

View File

@ -0,0 +1,62 @@
package spec
import (
"context"
"encoding/json"
"github.com/pkg/errors"
"github.com/qri-io/jsonschema"
)
type Validator struct {
schemas map[Name]*jsonschema.Schema
}
func (v *Validator) Register(name Name, rawSchema []byte) error {
schema := &jsonschema.Schema{}
if err := json.Unmarshal(rawSchema, schema); err != nil {
return errors.Wrapf(err, "could not register spec shema '%s'", name)
}
v.schemas[name] = schema
return nil
}
func (v *Validator) Validate(ctx context.Context, spec Spec) error {
schema, exists := v.schemas[spec.SpecName()]
if !exists {
return errors.WithStack(ErrUnknownSchema)
}
state := schema.Validate(ctx, map[string]any(spec.SpecData()))
if !state.IsValid() {
return errors.WithStack(&ValidationError{*state.Errs})
}
return nil
}
func NewValidator() *Validator {
return &Validator{
schemas: make(map[Name]*jsonschema.Schema),
}
}
var defaultValidator = NewValidator()
func Register(name Name, rawSchema []byte) error {
if err := defaultValidator.Register(name, rawSchema); err != nil {
return errors.WithStack(err)
}
return nil
}
func Validate(ctx context.Context, spec Spec) error {
if err := defaultValidator.Validate(ctx, spec); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -2,7 +2,9 @@
CREATE TABLE agents (
id INTEGER PRIMARY KEY,
remote_id TEXT NOT NULL UNIQUE,
thumbprint TEXT UNIQUE,
keyset TEXT,
metadata TEXT,
status INTEGER NOT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL

View File

@ -1,4 +1,6 @@
agent:
serverUrl: http://127.0.0.1:3000
privateKeyPath: /var/lib/emissary/agent-key.json
reconciliationInterval: 5
controllers:
persistence:
@ -6,10 +8,14 @@ agent:
stateFile: /var/lib/emissary/state.json
spec:
enabled: true
serverUrl: http://192.168.0.45:3000
gateway:
enabled: true
uci:
enabled: true
binPath: uci
configBackupFile: /var/lib/emissary/uci-backup.conf
collectors:
- name: uname
command: uname
args:
- -a

View File

@ -1,9 +1,10 @@
#!/sbin/openrc-run
command="/usr/bin/emissary"
command_args="--config /etc/emissary/config.yml agent run"
start_stop_daemon_args='--chdir /usr/share/emissary'
command_args="--workdir /usr/share/emissary --config /etc/emissary/agent.yml agent run"
supervisor=supervise-daemon
output_log="/var/log/emissary/agent.log"
error_log="$output_log"
depend() {
need net

View File

@ -1,9 +1,14 @@
#!/sbin/openrc-run
command="/usr/bin/emissary"
command_args="--config /etc/emissary/config.yml server run"
start_stop_daemon_args='--chdir /usr/share/emissary'
command_args="--workdir /usr/share/emissary --config /etc/emissary/server.yml server run"
supervisor=supervise-daemon
output_log="/var/log/emissary/server.log"
error_log="$output_log"
start_pre() {
/usr/bin/emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server database migrate
}
depend() {
need net

View File

@ -6,6 +6,7 @@ tmp/config.yml
prep: make build-emissary
prep: make tmp/server.yml
prep: make tmp/agent.yml
prep: make run-emissary-server EMISSARY_CMD="--debug --config tmp/agent.yml server database migrate"
daemon: make run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server run"
daemon: make run-emissary-agent EMISSARY_CMD="--debug --config tmp/agent.yml agent run"
}