Compare commits
46 Commits
v2023.3.27
...
v2023.4.13
Author | SHA1 | Date | |
---|---|---|---|
41b1619fc1 | |||
35d5ee868f | |||
765257b4b1 | |||
2315ee7b61 | |||
86a6d81e1d | |||
c4427dfd2b | |||
280b0fbd50 | |||
8fb86c600f | |||
12f8b3aa25 | |||
2d2dc29c84 | |||
4cf53d9f15 | |||
34e4769b49 | |||
47c2546d54 | |||
21173911fb | |||
b213b8d1ae | |||
9dcddc5566 | |||
9a46c9d3d0 | |||
69f183d126 | |||
e8829170e5 | |||
253c93dbac | |||
d2f865ccbb | |||
7ee4344adc | |||
06b1235707 | |||
2e1ee44e6a | |||
242a247222 | |||
562d698066 | |||
909549f056 | |||
7d551a8312 | |||
d02eb91b11 | |||
d2bcdd2999 | |||
c638fe102b | |||
273265c3ef | |||
3e02a9f031 | |||
b52c687643 | |||
8119a01bf6 | |||
e5b6c5e949 | |||
9c69dc7ec8 | |||
4e6b450338 | |||
351f22e216 | |||
854a6ae41b | |||
a48c2ebe14 | |||
cdd78e4031 | |||
e832b7dedd | |||
3d0b0102c2 | |||
e60c441d43 | |||
f7d70da174 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,7 @@ dist/
|
||||
/tools
|
||||
/tmp
|
||||
/state.json
|
||||
/emissary.sqlite
|
||||
/emissary.sqlite*
|
||||
/.gitea-release
|
||||
/agent-key.json
|
||||
/apps
|
||||
|
@ -46,6 +46,9 @@ builds:
|
||||
- arm64
|
||||
- arm
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
main: ./cmd/agent
|
||||
archives:
|
||||
- id: server
|
||||
|
85
Jenkinsfile
vendored
Normal file
85
Jenkinsfile
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
@Library('cadoles') _
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
label 'docker'
|
||||
filename 'Dockerfile'
|
||||
dir 'misc/jenkins'
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Cancel older jobs') {
|
||||
steps {
|
||||
script {
|
||||
def buildNumber = env.BUILD_NUMBER as int
|
||||
if (buildNumber > 1) milestone(buildNumber - 1)
|
||||
milestone(buildNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Run unit tests') {
|
||||
steps {
|
||||
script {
|
||||
withCredentials([
|
||||
usernamePassword([
|
||||
credentialsId: 'forge-jenkins',
|
||||
usernameVariable: 'GIT_USERNAME',
|
||||
passwordVariable: 'GIT_PASSWORD'
|
||||
])
|
||||
]) {
|
||||
sh '''
|
||||
git config --global credential.https://forge.cadoles.com.username "$GIT_USERNAME"
|
||||
git config --global credential.https://forge.cadoles.com.helper '!f() { test "$1" = get && echo "password=$GIT_PASSWORD"; }; f'
|
||||
|
||||
export GOPRIVATE=forge.cadoles.com/arcad/edge
|
||||
make test
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Release') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch 'develop'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
withCredentials([
|
||||
usernamePassword([
|
||||
credentialsId: 'forge-jenkins',
|
||||
usernameVariable: 'GITEA_RELEASE_USERNAME',
|
||||
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
||||
])
|
||||
]) {
|
||||
sh 'make gitea-release'
|
||||
}
|
||||
def currentVersion = sh(returnStdout: true, script: 'make full-version').trim()
|
||||
if (currentVersion.endsWith('-dirty')) {
|
||||
unstable('Could not trigger emissary-firmware build, dirty version !')
|
||||
} else {
|
||||
build(
|
||||
job: "../emissary-firmware/${env.GIT_BRANCH}",
|
||||
parameters: [
|
||||
[$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
|
||||
],
|
||||
wait: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
23
Makefile
23
Makefile
@ -135,7 +135,7 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_BODY="" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$(shell find .gitea-release/* -type f)" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||
tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
tools/gitea-release/bin/gitea-release.sh:
|
||||
@ -144,4 +144,23 @@ tools/gitea-release/bin/gitea-release.sh:
|
||||
chmod +x tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
.emissary-token:
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
|
||||
|
||||
AGENT_ID ?= 1
|
||||
|
||||
load-sample-specs:
|
||||
cat misc/spec-samples/app.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
|
||||
cat misc/spec-samples/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
||||
cat misc/spec-samples/mdns.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
|
||||
|
||||
full-version:
|
||||
@echo $(FULL_VERSION)
|
||||
|
||||
update-edge-lib:
|
||||
git pull --rebase
|
||||
GOPRIVATE=forge.cadoles.com/arcad/edge go get -u forge.cadoles.com/arcad/edge
|
||||
go mod tidy
|
||||
$(MAKE) test
|
||||
git add go.mod go.sum
|
||||
git commit -m "feat: update arcad/edge dependency"
|
||||
git push
|
@ -7,6 +7,7 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
||||
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
||||
)
|
||||
|
||||
|
12
go.mod
12
go.mod
@ -3,9 +3,11 @@ module forge.cadoles.com/Cadoles/emissary
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230413090224-4db7576b1296
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||
github.com/brutella/dnssd v1.2.6
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
@ -29,6 +31,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 // indirect
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230320130059-dcf93ba651dd // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
@ -37,10 +41,16 @@ require (
|
||||
github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/mdns v1.0.5 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/igm/sockjs-go/v3 v3.0.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/miekg/dns v1.1.51 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/orcaman/concurrent-map v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
26
go.sum
26
go.sum
@ -54,8 +54,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077 h1:vsYcNHZevZrs0VeOTasvJoqvPynb8OvH+MMpIUvNT6Q=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230413090224-4db7576b1296 h1:qaRJLG4z3wcHngziNqWZHtS9pJeWEWt6NYzZPFI2tow=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230413090224-4db7576b1296/go.mod h1:Vx4iq/oewXUOkGyi8QKc14clTLNO1sWpb0SjBYELlAs=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
@ -84,6 +84,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@ -195,6 +201,8 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
|
||||
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
@ -761,6 +769,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -771,6 +781,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
|
||||
@ -969,11 +980,14 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
|
||||
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
@ -987,6 +1001,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
|
||||
@ -1180,6 +1196,7 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
@ -1204,6 +1221,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
@ -1379,6 +1398,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.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1496,6 +1516,7 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/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-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -1786,6 +1807,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
|
@ -44,8 +44,6 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
|
||||
if err := a.registerAgent(ctx, client, state); err != nil {
|
||||
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "state before reconciliation", logger.F("state", state))
|
||||
@ -81,7 +79,7 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
|
||||
)
|
||||
|
||||
if err := ctrl.Reconcile(ctrlCtx, state); err != nil {
|
||||
return errors.WithStack(err)
|
||||
logger.Error(ctx, "could not reconcile", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}
|
||||
|
||||
|
251
internal/agent/controller/app/app_handler.go
Normal file
251
internal/agent/controller/app/app_handler.go
Normal file
@ -0,0 +1,251 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
|
||||
|
||||
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
dbFile := filepath.Join(dataDir, appKey+".sqlite")
|
||||
db, err := sqlite.Open(dbFile + defaultSQLiteParams)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
keySet, err := getAuthKeySet(specs.Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve auth key set")
|
||||
}
|
||||
|
||||
bus := memory.NewBus()
|
||||
modules := c.getAppModules(bus, db, specs, keySet)
|
||||
|
||||
options := []edgeHTTP.HandlerOptionFunc{
|
||||
edgeHTTP.WithBus(bus),
|
||||
edgeHTTP.WithServerModules(modules...),
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
|
||||
keySet := jwk.NewSet()
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
auth := config.Auth
|
||||
|
||||
if auth == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var (
|
||||
key jwk.Key
|
||||
err error
|
||||
)
|
||||
|
||||
switch typedKey := auth.Local.Key.(type) {
|
||||
case string:
|
||||
key, err = jwk.FromRaw([]byte(typedKey))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse local auth key")
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected key type '%T'", auth.Local.Key)
|
||||
}
|
||||
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return keySet, nil
|
||||
}
|
||||
|
||||
func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) {
|
||||
rawIfaceMappings := make(map[string]string, 0)
|
||||
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil {
|
||||
rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings
|
||||
}
|
||||
|
||||
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings))
|
||||
for iface, rawTemplate := range rawIfaceMappings {
|
||||
tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse iface '%s' template", iface)
|
||||
}
|
||||
|
||||
ifaceMappings[iface] = tmpl
|
||||
}
|
||||
|
||||
defaultRawTemplate := `http://{{ .DeviceIP }}:{{ .AppPort }}`
|
||||
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.DefaultURLTemplate != "" {
|
||||
defaultRawTemplate = specs.Config.AppURLResolving.DefaultURLTemplate
|
||||
}
|
||||
|
||||
defaultTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(defaultRawTemplate)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return func(ctx context.Context, manifest *app.Manifest, from string) (string, error) {
|
||||
var (
|
||||
urlTemplate *template.Template
|
||||
deviceIP net.IP
|
||||
)
|
||||
|
||||
fromIP := net.ParseIP(from)
|
||||
|
||||
if fromIP != nil {
|
||||
LOOP:
|
||||
for ifaceName, ifaceTmpl := range ifaceMappings {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not find interface",
|
||||
logger.E(errors.WithStack(err)), logger.F("iface", ifaceName),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
addresses, err := iface.Addrs()
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not list interface addresses",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("iface", iface.Name),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
ifaIP, network, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not parse interface ip",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("iface", iface.Name),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !network.Contains(fromIP) {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceIP = ifaIP
|
||||
urlTemplate = ifaceTmpl
|
||||
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if urlTemplate == nil {
|
||||
urlTemplate = defaultTemplate
|
||||
}
|
||||
|
||||
if deviceIP == nil {
|
||||
deviceIP = net.ParseIP("127.0.0.1")
|
||||
}
|
||||
|
||||
var appEntry *spec.AppEntry
|
||||
for appID, entry := range specs.Apps {
|
||||
if manifest.ID != app.ID(appID) {
|
||||
continue
|
||||
}
|
||||
|
||||
appEntry = &entry
|
||||
break
|
||||
}
|
||||
|
||||
if appEntry == nil {
|
||||
return "", errors.Errorf("could not find app '%s' in specs", manifest.ID)
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(appEntry.Address)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Manifest *app.Manifest
|
||||
Specs *spec.Spec
|
||||
DeviceIP string
|
||||
AppPort string
|
||||
}{
|
||||
Manifest: manifest,
|
||||
Specs: specs,
|
||||
DeviceIP: deviceIP.String(),
|
||||
AppPort: port,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := urlTemplate.Execute(&buf, data); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
netModule.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
blob.ModuleFactory(bus, bs),
|
||||
authModule(keySet),
|
||||
appModule.ModuleFactory(c.appRepository),
|
||||
fetchModule.ModuleFactory(bus),
|
||||
}
|
||||
}
|
73
internal/agent/controller/app/app_handler_test.go
Normal file
73
internal/agent/controller/app/app_handler_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestCreateResolveAppURL(t *testing.T) {
|
||||
specs := &spec.Spec{
|
||||
Apps: map[string]spec.AppEntry{
|
||||
"app.arcad.test": {
|
||||
Address: ":8080",
|
||||
},
|
||||
"app.arcad.foo": {
|
||||
Address: ":8081",
|
||||
},
|
||||
"app.arcad.bar": {
|
||||
Address: ":8082",
|
||||
},
|
||||
},
|
||||
Config: &spec.Config{
|
||||
AppURLResolving: &spec.AppURLResolving{
|
||||
IfaceMappings: map[string]string{
|
||||
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
"does-not-exists": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
},
|
||||
DefaultURLTemplate: `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.arcad.local`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resolveAppURL, err := createResolveAppURL(specs)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
manifest := &app.Manifest{
|
||||
ID: "app.arcad.test",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
url, err := resolveAppURL(ctx, manifest, "127.0.0.2")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://127.0.0.1:8080", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
|
||||
url, err = resolveAppURL(ctx, manifest, "")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://test.arcad.local", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
|
||||
url, err = resolveAppURL(ctx, manifest, "192.168.0.100")
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := "http://test.arcad.local", url; e != g {
|
||||
t.Errorf("url: expected '%s', got '%s", e, g)
|
||||
}
|
||||
}
|
137
internal/agent/controller/app/app_repository.go
Normal file
137
internal/agent/controller/app/app_repository.go
Normal file
@ -0,0 +1,137 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error)
|
||||
|
||||
type AppRepository struct {
|
||||
resolveAppURL ResolveAppURLFunc
|
||||
bundles []string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Get implements app.Repository
|
||||
func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetURL implements app.Repository
|
||||
func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (string, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
url, err := r.resolveAppURL(ctx, manifest, from)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// List implements app.Repository
|
||||
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
manifests := make([]*app.Manifest, 0)
|
||||
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
|
||||
sort.Sort(ByID(manifests))
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
r.resolveAppURL = resolveAppURL
|
||||
r.bundles = bundles
|
||||
}
|
||||
|
||||
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if manifest.ID != id {
|
||||
continue
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(appModule.ErrNotFound)
|
||||
}
|
||||
|
||||
func NewAppRepository() *AppRepository {
|
||||
return &AppRepository{
|
||||
resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) {
|
||||
return "", errors.New("unavailable")
|
||||
},
|
||||
bundles: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ appModule.Repository = &AppRepository{}
|
||||
|
||||
type ByID []*app.Manifest
|
||||
|
||||
func (a ByID) Len() int { return len(a) }
|
||||
func (a ByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByID) Less(i, j int) bool { return a[i].ID > a[j].ID }
|
65
internal/agent/controller/app/auth_module.go
Normal file
65
internal/agent/controller/app/auth_module.go
Normal file
@ -0,0 +1,65 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
RoleVisitor string = "visitor"
|
||||
RoleUser string = "user"
|
||||
RoleSuperuser string = "superuser"
|
||||
RoleAdmin string = "admin"
|
||||
RoleSuperadmin string = "superadmin"
|
||||
)
|
||||
|
||||
func authModule(keySet jwk.Set) app.ServerModuleFactory {
|
||||
return module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(func() (jwk.Set, error) {
|
||||
return keySet, nil
|
||||
}),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_VISITOR", RoleVisitor); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_VISITOR' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_USER", RoleUser); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_USER' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_SUPERUSER", RoleSuperuser); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_SUPERUSER' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_ADMIN", RoleAdmin); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_ADMIN' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("ROLE_SUPERADMIN", RoleSuperadmin); err != nil {
|
||||
panic(errors.New("could not set 'ROLE_SUPERADMIN' property"))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -8,24 +8,25 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type serverEntry struct {
|
||||
SpecHash uint64
|
||||
Server *Server
|
||||
AppDefHash uint64
|
||||
Server *Server
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*serverEntry
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*serverEntry
|
||||
appRepository *AppRepository
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
@ -35,9 +36,9 @@ func (c *Controller) Name() string {
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
appSpec := app.NewSpec()
|
||||
appSpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(app.NameApp, appSpec); err != nil {
|
||||
if err := state.GetSpec(spec.Name, appSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find app spec")
|
||||
|
||||
@ -56,7 +57,7 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *spec.Spec) {
|
||||
if len(c.servers) > 0 {
|
||||
logger.Info(ctx, "stopping all apps")
|
||||
}
|
||||
@ -76,122 +77,169 @@ func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
||||
// Stop and remove obsolete apps
|
||||
for appID, entry := range c.servers {
|
||||
if _, exists := spec.Apps[appID]; exists {
|
||||
for appKey, server := range c.servers {
|
||||
if _, exists := specs.Apps[appKey]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info(ctx, "stopping app", logger.F("appID", appID))
|
||||
logger.Info(ctx, "stopping app", logger.F("appKey", appKey))
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "error while stopping app",
|
||||
logger.F("gatewayID", appID),
|
||||
logger.F("appKey", appKey),
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
delete(c.servers, appID)
|
||||
delete(c.servers, appKey)
|
||||
}
|
||||
}
|
||||
|
||||
// (Re)start apps
|
||||
for appID, appSpec := range spec.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appID", appID))
|
||||
if err := c.updateAppRepository(ctx, specs); err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not update app repository",
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
if err := c.updateApp(ctx, appID, appSpec, spec.Auth); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// (Re)start apps if necessary
|
||||
for appKey := range specs.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appKey", appKey))
|
||||
|
||||
if err := c.updateApp(ctx, specs, appKey); err != nil {
|
||||
logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry, auth *app.Auth) (err error) {
|
||||
newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil)
|
||||
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error {
|
||||
bundles := make([]string, 0, len(specs.Apps))
|
||||
for appKey, app := range specs.Apps {
|
||||
path := c.getAppBundlePath(appKey, app.Format)
|
||||
bundles = append(bundles, path)
|
||||
}
|
||||
|
||||
resolveAppURL, err := createResolveAppURL(specs)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
}
|
||||
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
var entry *serverEntry
|
||||
|
||||
entry, exists := c.servers[appID]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appSpec.SHA256Sum {
|
||||
logger.Info(
|
||||
ctx, "bundle hash mismatch, stopping app",
|
||||
logger.F("currentHash", sha256sum),
|
||||
logger.F("specHash", appSpec.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
entry = nil
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
dbFile := filepath.Join(dataDir, appID+".sqlite")
|
||||
db, err := sqlite.Open(dbFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not opend database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
entry = &serverEntry{
|
||||
Server: NewServer(bundle, db, auth),
|
||||
SpecHash: 0,
|
||||
}
|
||||
|
||||
c.servers[appID] = entry
|
||||
}
|
||||
|
||||
specChanged := newAppSpecHash != entry.SpecHash
|
||||
|
||||
if entry.Server.Running() && !specChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if specChanged && entry.SpecHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := entry.Server.Start(ctx, appSpec.Address); err != nil {
|
||||
delete(c.servers, appID)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
entry.SpecHash = newAppSpecHash
|
||||
c.appRepository.Update(resolveAppURL, bundles)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app.AppEntry) (bundle.Bundle, string, error) {
|
||||
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
|
||||
appEntry := specs.Apps[appKey]
|
||||
|
||||
appDef := struct {
|
||||
App spec.AppEntry
|
||||
Config *spec.Config
|
||||
}{
|
||||
App: appEntry,
|
||||
Config: specs.Config,
|
||||
}
|
||||
|
||||
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appKey, appEntry)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
}
|
||||
|
||||
server, exists := c.servers[appKey]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appEntry.SHA256Sum {
|
||||
logger.Info(
|
||||
ctx, "bundle hash mismatch, stopping app",
|
||||
logger.F("currentHash", sha256sum),
|
||||
logger.F("specHash", appEntry.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
server = nil
|
||||
}
|
||||
|
||||
newServerEntry := func() (*serverEntry, error) {
|
||||
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create handler options")
|
||||
}
|
||||
|
||||
server = &serverEntry{
|
||||
Server: NewServer(bundle, specs.Config, options...),
|
||||
AppDefHash: 0,
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
serverEntry, err := newServerEntry()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.servers[appKey] = serverEntry
|
||||
}
|
||||
|
||||
defChanged := newAppDefHash != server.AppDefHash
|
||||
if server.Server.Running() && !defChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if defChanged && server.AppDefHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
serverEntry, err := newServerEntry()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.servers[appKey] = serverEntry
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := server.Server.Start(ctx, appEntry.Address); err != nil {
|
||||
delete(c.servers, appKey)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
server.AppDefHash = newAppDefHash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec spec.AppEntry) (bundle.Bundle, string, error) {
|
||||
if err := os.MkdirAll(c.downloadDir, os.ModePerm); err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundlePath := filepath.Join(c.downloadDir, appID+"."+spec.Format)
|
||||
bundlePath := c.getAppBundlePath(appID, spec.Format)
|
||||
|
||||
_, err := os.Stat(bundlePath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
@ -229,7 +277,21 @@ func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return bdle, "", nil
|
||||
manifest, err := app.LoadManifest(bdle)
|
||||
if err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
valid, err := validateManifest(manifest)
|
||||
if err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return nil, "", errors.New("bundle's manifest is invalid")
|
||||
}
|
||||
|
||||
return bdle, spec.SHA256Sum, nil
|
||||
}
|
||||
|
||||
func (c *Controller) downloadFile(url string, sha256sum string, dest string) error {
|
||||
@ -285,6 +347,10 @@ func (c *Controller) ensureAppDataDir(ctx context.Context, appID string) (string
|
||||
return dataDir, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getAppBundlePath(appKey string, format string) string {
|
||||
return filepath.Join(c.downloadDir, appKey+"."+format)
|
||||
}
|
||||
|
||||
func NewController(funcs ...OptionFunc) *Controller {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
@ -292,10 +358,11 @@ func NewController(funcs ...OptionFunc) *Controller {
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
servers: make(map[string]*serverEntry),
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
servers: make(map[string]*serverEntry),
|
||||
appRepository: NewAppRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
|
19
internal/agent/controller/app/manifest.go
Normal file
19
internal/agent/controller/app/manifest.go
Normal file
@ -0,0 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app/metadata"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func validateManifest(manifest *app.Manifest) (bool, error) {
|
||||
valid, err := manifest.Validate(
|
||||
metadata.WithMinimumRoleValidator(RoleVisitor, RoleUser, RoleSuperuser, RoleAdmin, RoleSuperadmin),
|
||||
metadata.WithNamedPathsValidator(metadata.NamedPathAdmin, metadata.NamedPathIcon),
|
||||
)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return valid, nil
|
||||
}
|
@ -2,26 +2,20 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
@ -31,67 +25,48 @@ import (
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/passwd"
|
||||
)
|
||||
|
||||
const defaultCookieDuration time.Duration = 24 * time.Hour
|
||||
|
||||
type Server struct {
|
||||
bundle bundle.Bundle
|
||||
db *sql.DB
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
auth *appSpec.Auth
|
||||
keySet jwk.Set
|
||||
bundle bundle.Bundle
|
||||
handlerOptions []edgeHTTP.HandlerOptionFunc
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
config *appSpec.Config
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
if s.server != nil {
|
||||
if s.Running() {
|
||||
if err := s.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
s.serverMutex.Lock()
|
||||
defer s.serverMutex.Unlock()
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
bus := memory.NewBus()
|
||||
ds := sqlite.NewDocumentStoreWithDB(s.db)
|
||||
bs := sqlite.NewBlobStoreWithDB(s.db)
|
||||
|
||||
handler := edgeHTTP.NewHandler(
|
||||
edgeHTTP.WithBus(bus),
|
||||
edgeHTTP.WithServerModules(s.getAppModules(bus, ds, bs)...),
|
||||
)
|
||||
handler := edgeHTTP.NewHandler(s.handlerOptions...)
|
||||
if err := handler.Load(s.bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
if s.auth != nil {
|
||||
if s.auth.Local != nil {
|
||||
var rawKey any = s.auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.keySet = keySet
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(s.auth.Local.Accounts...),
|
||||
if s.config != nil {
|
||||
if s.config.UnexpectedHostRedirect != nil {
|
||||
router.Use(unexpectedHostRedirect(
|
||||
s.config.UnexpectedHostRedirect.HostTarget,
|
||||
s.config.UnexpectedHostRedirect.AcceptedHostPatterns...,
|
||||
))
|
||||
}
|
||||
|
||||
if s.config.Auth != nil {
|
||||
if err := s.configureAuth(router, s.config.Auth); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.Handle("/*", handler)
|
||||
@ -125,9 +100,7 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
s.serverMutex.Lock()
|
||||
s.server = server
|
||||
s.serverMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -140,66 +113,116 @@ func (s *Server) Running() bool {
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
if !s.Running() {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.serverMutex.Lock()
|
||||
defer s.serverMutex.Unlock()
|
||||
|
||||
if s.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.serverMutex.Lock()
|
||||
s.server = nil
|
||||
s.serverMutex.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.server.Close(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
s.server = nil
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.server = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var rawKey any = auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
cookieDuration := defaultCookieDuration
|
||||
if auth.Local.CookieDuration != "" {
|
||||
cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(auth.Local.Accounts...),
|
||||
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
|
||||
))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(s.getJWTKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getJWTKeySet() (jwk.Set, error) {
|
||||
return s.keySet, nil
|
||||
}
|
||||
|
||||
func NewServer(bundle bundle.Bundle, db *sql.DB, auth *appSpec.Auth) *Server {
|
||||
func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
|
||||
return &Server{
|
||||
bundle: bundle,
|
||||
db: db,
|
||||
auth: auth,
|
||||
bundle: bundle,
|
||||
config: config,
|
||||
handlerOptions: handlerOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func getCookieDomain(r *http.Request) (string, error) {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
// If host is an IP address
|
||||
if wildcard.Match(host, "*.*.*.*") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If host is an domain, return top level domain
|
||||
domainParts := strings.Split(host, ".")
|
||||
if len(domainParts) >= 2 {
|
||||
topLevelDomain := strings.Join(domainParts[len(domainParts)-2:], ".")
|
||||
return topLevelDomain, nil
|
||||
}
|
||||
|
||||
// By default, return host
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func unexpectedHostRedirect(hostTarget string, acceptedHostPatterns ...string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
host, port, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
|
||||
|
||||
if !matched {
|
||||
url := r.URL
|
||||
|
||||
url.Host = hostTarget
|
||||
if port != "" {
|
||||
url.Host += ":" + port
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app
|
||||
package spec
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
@ -11,7 +11,7 @@ import (
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(NameApp, schema); err != nil {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
135
internal/agent/controller/app/spec/schema.json
Normal file
135
internal/agent/controller/app/spec/schema.json
Normal file
@ -0,0 +1,135 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256sum": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zip",
|
||||
"tar.gz"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"address",
|
||||
"format"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appUrlResolving": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ifaceMappings": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultUrlTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["defaultUrlTemplate"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"acceptedHostPatterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hostTarget": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["acceptedHostPatterns", "hostTarget"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cookieDomain": {
|
||||
"type": "string"
|
||||
},
|
||||
"cookieDuration": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
71
internal/agent/controller/app/spec/spec.go
Normal file
71
internal/agent/controller/app/spec/spec.go
Normal file
@ -0,0 +1,71 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
)
|
||||
|
||||
const Name spec.Name = "app.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Apps map[string]AppEntry `json:"apps"`
|
||||
Config *Config `json:"config"`
|
||||
}
|
||||
|
||||
type AppEntry struct {
|
||||
URL string `json:"url"`
|
||||
SHA256Sum string `json:"sha256sum"`
|
||||
Address string `json:"address"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Key any `json:"key"`
|
||||
Accounts []edgeAuth.LocalAccount `json:"accounts"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
CookieDuration string `json:"cookieDuration"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth *Auth `json:"auth"`
|
||||
UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"`
|
||||
AppURLResolving *AppURLResolving `json:"appUrlResolving"`
|
||||
}
|
||||
|
||||
type UnexpectedHostRedirect struct {
|
||||
AcceptedHostPatterns []string `json:"acceptedHostPatterns"`
|
||||
HostTarget string `json:"hostTarget"`
|
||||
}
|
||||
|
||||
type AppURLResolving struct {
|
||||
IfaceMappings map[string]string `json:"ifaceMappings"`
|
||||
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"apps": s.Apps,
|
||||
"config": s.Config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
54
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
54
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "app.emissary.cadoles.com",
|
||||
"data": {
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"address": ":8081",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"acceptedHostPatterns": ["arcad.local", "*.arcad.local", "arcad-*.local", "*.*.*.*"],
|
||||
"hostTarget": "arcad.local"
|
||||
},
|
||||
"appUrlResolving": {
|
||||
"ifaceMappings": {
|
||||
"eth0": "http://{{ .DeviceIP }}:{{ .AppHost }}"
|
||||
},
|
||||
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app
|
||||
package spec
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -28,7 +28,7 @@ func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(NameApp, schema); err != nil {
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
181
internal/agent/controller/mdns/controller.go
Normal file
181
internal/agent/controller/mdns/controller.go
Normal file
@ -0,0 +1,181 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
mdns "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
"github.com/brutella/dnssd"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDomain = "local"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
serviceDefHash uint64
|
||||
cancel context.CancelFunc
|
||||
responder dnssd.Responder
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
func (c *Controller) Name() string {
|
||||
return "mdns-controller"
|
||||
}
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
mdnsSpec := mdns.NewSpec()
|
||||
|
||||
if err := state.GetSpec(mdns.Name, mdnsSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find mdns spec")
|
||||
|
||||
c.stopResponder(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", mdnsSpec.SpecName()), logger.F("revision", mdnsSpec.SpecRevision()))
|
||||
|
||||
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
|
||||
return errors.Wrap(err, "could not update responder")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopResponder(ctx context.Context) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.responder == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.cancel()
|
||||
c.responder = nil
|
||||
c.cancel = nil
|
||||
}
|
||||
|
||||
func (c *Controller) updateResponder(ctx context.Context, spec *mdns.Spec) error {
|
||||
serviceDef := struct {
|
||||
Services map[string]mdns.Service
|
||||
}{
|
||||
Services: spec.Services,
|
||||
}
|
||||
|
||||
newServerDefHash, err := hashstructure.Hash(serviceDef, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.mutex.RLock()
|
||||
if newServerDefHash == c.serviceDefHash && c.responder != nil {
|
||||
c.mutex.RUnlock()
|
||||
return nil
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
c.stopResponder(ctx)
|
||||
|
||||
defaultIfaces, err := c.getDefaultIfaces()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
services := make([]dnssd.Service, 0, len(spec.Services))
|
||||
|
||||
for name, service := range spec.Services {
|
||||
domain := service.Domain
|
||||
if domain == "" {
|
||||
domain = DefaultDomain
|
||||
}
|
||||
|
||||
ifaces := service.Ifaces
|
||||
if len(ifaces) == 0 {
|
||||
ifaces = defaultIfaces
|
||||
}
|
||||
|
||||
config := dnssd.Config{
|
||||
Name: name,
|
||||
Type: service.Type,
|
||||
Domain: domain,
|
||||
Host: service.Host,
|
||||
Ifaces: ifaces,
|
||||
Port: service.Port,
|
||||
}
|
||||
|
||||
service, err := dnssd.NewService(config)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not create mdns service", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
responder, err := dnssd.NewResponder()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if _, err := responder.Add(service); err != nil {
|
||||
logger.Error(ctx, "could not add mdns service", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
c.responder = responder
|
||||
c.cancel = cancel
|
||||
c.serviceDefHash = newServerDefHash
|
||||
|
||||
go func() {
|
||||
defer c.stopResponder(ctx)
|
||||
|
||||
if err := responder.Respond(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.Error(ctx, "could not respond to mdns queries", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) getDefaultIfaces() ([]string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ifaceNames := make([]string, len(ifaces))
|
||||
|
||||
for idx, ifa := range ifaces {
|
||||
ifaceNames[idx] = ifa.Name
|
||||
}
|
||||
|
||||
return ifaceNames, nil
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
cancel: nil,
|
||||
responder: nil,
|
||||
serviceDefHash: 0,
|
||||
}
|
||||
}
|
||||
|
||||
var _ agent.Controller = &Controller{}
|
17
internal/agent/controller/mdns/spec/init.go
Normal file
17
internal/agent/controller/mdns/spec/init.go
Normal file
@ -0,0 +1,17 @@
|
||||
package spec
|
||||
|
||||
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(Name, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
47
internal/agent/controller/mdns/spec/schema.json
Normal file
47
internal/agent/controller/mdns/spec/schema.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "MDNSSpec",
|
||||
"description": "Emissary 'MDNS' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"ifaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"services"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
42
internal/agent/controller/mdns/spec/spec.go
Normal file
42
internal/agent/controller/mdns/spec/spec.go
Normal file
@ -0,0 +1,42 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
)
|
||||
|
||||
const Name spec.Name = "mdns.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Services map[string]Service `json:"services"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Type string `json:"type"`
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Ifaces []string `json:"ifaces"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"services": s.Services,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
15
internal/agent/controller/mdns/spec/testdata/spec-ok.json
vendored
Normal file
15
internal/agent/controller/mdns/spec/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mdns.emissary.cadoles.com",
|
||||
"data": {
|
||||
"services": {
|
||||
"My Website": {
|
||||
"type": "_http._tcp",
|
||||
"domain": "local",
|
||||
"host": "mywebsite",
|
||||
"ifaces": ["lo", "eth0"],
|
||||
"port": 80
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
65
internal/agent/controller/mdns/spec/validator_test.go
Normal file
65
internal/agent/controller/mdns/spec/validator_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package spec
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(Name, 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)
|
||||
}
|
||||
}
|
@ -5,8 +5,8 @@ import (
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
edgeProxy "forge.cadoles.com/arcad/edge/pkg/proxy"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
|
||||
spec "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -18,7 +18,7 @@ type proxyEntry struct {
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
proxies map[proxy.ID]*proxyEntry
|
||||
proxies map[spec.ID]*proxyEntry
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
@ -28,9 +28,9 @@ func (c *Controller) Name() string {
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
proxySpec := proxy.NewSpec()
|
||||
proxySpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(proxy.NameProxy, proxySpec); err != nil {
|
||||
if err := state.GetSpec(spec.NameProxy, proxySpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find proxy spec")
|
||||
|
||||
@ -69,7 +69,7 @@ func (c *Controller) stopAllProxies(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
|
||||
func (c *Controller) updateProxies(ctx context.Context, spec *spec.Spec) {
|
||||
// Stop and remove obsolete proxys
|
||||
for proxyID, entry := range c.proxies {
|
||||
if _, exists := spec.Proxies[proxyID]; exists {
|
||||
@ -100,7 +100,7 @@ func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpec proxy.ProxyEntry) (err error) {
|
||||
func (c *Controller) updateProxy(ctx context.Context, proxyID spec.ID, proxySpec spec.ProxyEntry) (err error) {
|
||||
newProxySpecHash, err := hashstructure.Hash(proxySpec, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -140,7 +140,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
)
|
||||
}
|
||||
|
||||
options := make([]edgeProxy.OptionFunc, 0)
|
||||
options := make([]proxy.OptionFunc, 0)
|
||||
allowedHosts := make([]string, len(proxySpec.Mappings))
|
||||
mappings := make(map[string]*url.URL, len(proxySpec.Mappings))
|
||||
|
||||
@ -156,8 +156,8 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
|
||||
options = append(
|
||||
options,
|
||||
edgeProxy.WithAllowedHosts(allowedHosts...),
|
||||
edgeProxy.WithRewriteHosts(mappings),
|
||||
proxy.WithAllowedHosts(allowedHosts...),
|
||||
proxy.WithRewriteHosts(mappings),
|
||||
)
|
||||
|
||||
if err := entry.Proxy.Start(ctx, proxySpec.Address, options...); err != nil {
|
||||
@ -173,7 +173,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
proxies: make(map[proxy.ID]*proxyEntry),
|
||||
proxies: make(map[spec.ID]*proxyEntry),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,9 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/proxy"
|
||||
)
|
||||
|
||||
type ReverseProxy struct {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
@ -13,8 +14,11 @@ import (
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
|
||||
type Authenticator struct {
|
||||
repo datastore.AgentRepository
|
||||
repo datastore.AgentRepository
|
||||
acceptableSkew time.Duration
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
@ -71,11 +75,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithAcceptableSkew(a.acceptableSkew),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
contactedAt := time.Now()
|
||||
|
||||
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
agent: agent,
|
||||
}
|
||||
@ -83,9 +95,10 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
|
||||
func NewAuthenticator(repo datastore.AgentRepository, acceptableSkew time.Duration) *Authenticator {
|
||||
return &Authenticator{
|
||||
repo: repo,
|
||||
repo: repo,
|
||||
acceptableSkew: acceptableSkew,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ func GenerateToken(key jwk.Key, thumbprint string) (string, error) {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
|
17
internal/auth/thirdparty/authenticator.go
vendored
17
internal/auth/thirdparty/authenticator.go
vendored
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
@ -11,9 +12,12 @@ import (
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
|
||||
type Authenticator struct {
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
acceptableSkew time.Duration
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
@ -30,7 +34,7 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken, a.acceptableSkew)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -57,10 +61,11 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
|
||||
func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator {
|
||||
return &Authenticator{
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
acceptableSkew: acceptableSkew,
|
||||
}
|
||||
}
|
||||
|
||||
|
5
internal/auth/thirdparty/jwt.go
vendored
5
internal/auth/thirdparty/jwt.go
vendored
@ -13,12 +13,13 @@ import (
|
||||
|
||||
const keyRole = "role"
|
||||
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithAcceptableSkew(acceptableSkew),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@ -42,7 +43,7 @@ func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, rol
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
|
||||
type UpdateAgentOptions struct {
|
||||
Status *int
|
||||
Label *string
|
||||
Options []OptionFunc
|
||||
}
|
||||
|
||||
@ -21,6 +22,12 @@ func WithAgentStatus(status int) UpdateAgentOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentLabel(label string) UpdateAgentOptionFunc {
|
||||
return func(opts *UpdateAgentOptions) {
|
||||
opts.Label = &label
|
||||
}
|
||||
}
|
||||
|
||||
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
|
||||
return func(opts *UpdateAgentOptions) {
|
||||
opts.Options = funcs
|
||||
@ -39,6 +46,10 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
|
||||
payload["status"] = *opts.Status
|
||||
}
|
||||
|
||||
if opts.Label != nil {
|
||||
payload["label"] = *opts.Label
|
||||
}
|
||||
|
||||
response := withResponse[struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}]()
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns"
|
||||
"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/proxy"
|
||||
@ -49,10 +50,6 @@ func RunCommand() *cli.Command {
|
||||
controllers = append(controllers, spec.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.Proxy.Enabled {
|
||||
controllers = append(controllers, proxy.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.UCI.Enabled {
|
||||
controllers = append(controllers, openwrt.NewUCIController(
|
||||
string(ctrlConf.UCI.BinPath),
|
||||
@ -66,6 +63,14 @@ func RunCommand() *cli.Command {
|
||||
))
|
||||
}
|
||||
|
||||
if ctrlConf.Proxy.Enabled {
|
||||
controllers = append(controllers, proxy.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.MDNS.Enabled {
|
||||
controllers = append(controllers, mdns.NewController())
|
||||
}
|
||||
|
||||
if ctrlConf.SysUpgrade.Enabled {
|
||||
sysUpgradeArgs := make([]string, 0)
|
||||
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
|
||||
|
@ -22,6 +22,11 @@ func UpdateCommand() *cli.Command {
|
||||
Usage: "Set `STATUS` to selected agent",
|
||||
Value: -1,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "label",
|
||||
Usage: "Set `LABEL` to selected agent",
|
||||
Value: "",
|
||||
},
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
@ -43,6 +48,11 @@ func UpdateCommand() *cli.Command {
|
||||
options = append(options, client.WithAgentStatus(status))
|
||||
}
|
||||
|
||||
label := ctx.String("label")
|
||||
if label != "" {
|
||||
options = append(options, client.WithAgentLabel(label))
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
|
||||
|
@ -7,9 +7,10 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
||||
OutputMode: outputMode,
|
||||
Props: []format.Prop{
|
||||
format.NewProp("ID", "ID"),
|
||||
format.NewProp("Label", "Label"),
|
||||
format.NewProp("Thumbprint", "Thumbprint"),
|
||||
format.NewProp("Status", "Status"),
|
||||
format.NewProp("CreatedAt", "CreatedAt"),
|
||||
format.NewProp("ContactedAt", "ContactedAt"),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt"),
|
||||
},
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ type ControllersConfig struct {
|
||||
UCI UCIControllerConfig `yaml:"uci"`
|
||||
App AppControllerConfig `yaml:"app"`
|
||||
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
|
||||
MDNS MDNSControllerConfig `yaml:"mdns"`
|
||||
}
|
||||
|
||||
type PersistenceControllerConfig struct {
|
||||
@ -55,6 +56,10 @@ type SysUpgradeControllerConfig struct {
|
||||
FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"`
|
||||
}
|
||||
|
||||
type MDNSControllerConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
func NewDefaultAgentConfig() AgentConfig {
|
||||
return AgentConfig{
|
||||
ServerURL: "http://127.0.0.1:3000",
|
||||
@ -86,6 +91,9 @@ func NewDefaultAgentConfig() AgentConfig {
|
||||
SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate},
|
||||
FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`},
|
||||
},
|
||||
MDNS: MDNSControllerConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Collectors: []ShellCollectorConfig{
|
||||
{
|
||||
|
@ -15,6 +15,6 @@ type DatabaseConfig struct {
|
||||
func NewDefaultDatabaseConfig() DatabaseConfig {
|
||||
return DatabaseConfig{
|
||||
Driver: "sqlite",
|
||||
DSN: "sqlite://emissary.sqlite",
|
||||
DSN: "sqlite://emissary.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,15 @@ const (
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
ID AgentID `json:"id"`
|
||||
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"`
|
||||
ID AgentID `json:"id"`
|
||||
Label string `json:"label"`
|
||||
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"`
|
||||
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
||||
}
|
||||
|
||||
type SerializableKeySet struct {
|
||||
|
@ -2,6 +2,7 @@ package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
@ -68,10 +69,12 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
|
||||
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
|
||||
|
||||
type AgentUpdateOptions struct {
|
||||
Status *AgentStatus
|
||||
Metadata *map[string]any
|
||||
KeySet *jwk.Set
|
||||
Thumbprint *string
|
||||
Label *string
|
||||
Status *AgentStatus
|
||||
ContactedAt *time.Time
|
||||
Metadata *map[string]any
|
||||
KeySet *jwk.Set
|
||||
Thumbprint *string
|
||||
}
|
||||
|
||||
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
||||
@ -97,3 +100,15 @@ func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
|
||||
opts.Thumbprint = &thumbprint
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateLabel(label string) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.Label = &label
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateContactedAt(contactedAt time.Time) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.ContactedAt = &contactedAt
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,24 @@ type AgentRepository struct {
|
||||
|
||||
// DeleteSpec implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
|
||||
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, agentID, name)
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
|
||||
|
||||
if _, err = tx.ExecContext(ctx, query, agentID, name); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -34,31 +49,55 @@ func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.Agen
|
||||
func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentID) ([]*datastore.Spec, error) {
|
||||
specs := make([]*datastore.Spec, 0)
|
||||
|
||||
query := `
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, name, revision, data, created_at, updated_at
|
||||
FROM specs
|
||||
WHERE agent_id = $1
|
||||
`
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, agentID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
rows, err := tx.QueryContext(ctx, query, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
spec.Data = data
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
specs = append(specs, spec)
|
||||
for rows.Next() {
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
spec.Data = data
|
||||
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
@ -69,6 +108,15 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
query := `
|
||||
@ -88,7 +136,7 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||
|
||||
data := JSONMap{}
|
||||
|
||||
err := row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
|
||||
err = row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errors.WithStack(datastore.ErrUnexpectedRevision)
|
||||
@ -119,7 +167,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, thumbprint, status, created_at, updated_at FROM agents`
|
||||
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
|
||||
|
||||
limit := 10
|
||||
if options.Limit != nil {
|
||||
@ -176,22 +224,34 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
agent := &datastore.Agent{}
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
|
||||
if err := rows.Scan(&agent.ID, &agent.Thumbprint, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents `+filters, args...)
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -299,7 +359,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
SELECT "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
||||
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
FROM agents
|
||||
WHERE id = $1
|
||||
`
|
||||
@ -307,9 +367,10 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
row := r.db.QueryRowContext(ctx, query, id)
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -318,6 +379,9 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||
@ -346,15 +410,11 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
UPDATE agents SET updated_at = $2
|
||||
UPDATE agents SET id = $1
|
||||
`
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
args := []any{
|
||||
id, now,
|
||||
}
|
||||
index := 3
|
||||
args := []any{id}
|
||||
index := 2
|
||||
|
||||
if options.Status != nil {
|
||||
query += fmt.Sprintf(`, status = $%d`, index)
|
||||
@ -379,23 +439,51 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
index++
|
||||
}
|
||||
|
||||
if options.Label != nil {
|
||||
query += fmt.Sprintf(`, label = $%d`, index)
|
||||
args = append(args, *options.Label)
|
||||
index++
|
||||
}
|
||||
|
||||
if options.ContactedAt != nil {
|
||||
query += fmt.Sprintf(`, contacted_at = $%d`, index)
|
||||
utc := options.ContactedAt.UTC()
|
||||
args = append(args, utc)
|
||||
index++
|
||||
}
|
||||
|
||||
if options.Metadata != nil {
|
||||
query += fmt.Sprintf(`, metadata = $%d`, index)
|
||||
args = append(args, JSONMap(*options.Metadata))
|
||||
index++
|
||||
}
|
||||
|
||||
updated := options.Metadata != nil ||
|
||||
options.Status != nil ||
|
||||
options.Label != nil ||
|
||||
options.KeySet != nil ||
|
||||
options.Thumbprint != nil
|
||||
if updated {
|
||||
now := time.Now().UTC()
|
||||
query += fmt.Sprintf(`, updated_at = $%d`, index)
|
||||
args = append(args, now)
|
||||
index++
|
||||
}
|
||||
|
||||
query += `
|
||||
WHERE id = $1
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
||||
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
`
|
||||
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
|
||||
row := tx.QueryRowContext(ctx, query, args...)
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -404,6 +492,9 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
if contactedAt.Valid {
|
||||
agent.ContactedAt = &contactedAt.Time
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
|
||||
@ -421,8 +512,28 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (r *AgentRepository) agentExists(ctx context.Context, tx *sql.Tx, agentID datastore.AgentID) (bool, error) {
|
||||
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents WHERE id = $1`, agentID)
|
||||
|
||||
var count int
|
||||
|
||||
if err := row.Scan(&count); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return false, errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *AgentRepository) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||
tx, err := r.db.Begin()
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
46
internal/datastore/sqlite/agent_repository_test.go
Normal file
46
internal/datastore/sqlite/agent_repository_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestSQLiteAgentRepository(t *testing.T) {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
file := "testdata/agent_repository_test.sqlite"
|
||||
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||
|
||||
migr, err := migrate.New("../../../migrations", "sqlite", "sqlite://"+dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := migr.Up(); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
repo := NewAgentRepository(db)
|
||||
|
||||
testsuite.TestAgentRepository(t, repo)
|
||||
}
|
1
internal/datastore/sqlite/testdata/.gitignore
vendored
Normal file
1
internal/datastore/sqlite/testdata/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
14
internal/datastore/testsuite/agent_repository.go
Normal file
14
internal/datastore/testsuite/agent_repository.go
Normal file
@ -0,0 +1,14 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
)
|
||||
|
||||
func TestAgentRepository(t *testing.T, repo datastore.AgentRepository) {
|
||||
t.Run("Cases", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runAgentRepositoryTests(t, repo)
|
||||
})
|
||||
}
|
129
internal/datastore/testsuite/agent_repository_cases.go
Normal file
129
internal/datastore/testsuite/agent_repository_cases.go
Normal file
@ -0,0 +1,129 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type agentRepositoryTestCase struct {
|
||||
Name string
|
||||
Skip bool
|
||||
Run func(ctx context.Context, repo datastore.AgentRepository) error
|
||||
}
|
||||
|
||||
var agentRepositoryTestCases = []agentRepositoryTestCase{
|
||||
{
|
||||
Name: "Create a new agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
thumbprint := "foo"
|
||||
keySet := jwk.NewSet()
|
||||
var metadata map[string]any
|
||||
|
||||
agent, err := repo.Create(ctx, thumbprint, keySet, metadata)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if agent.CreatedAt.IsZero() {
|
||||
return errors.Errorf("agent.CreatedAt should not be zero time")
|
||||
}
|
||||
|
||||
if agent.UpdatedAt.IsZero() {
|
||||
return errors.Errorf("agent.UpdatedAt should not be zero time")
|
||||
}
|
||||
|
||||
if e, g := datastore.AgentStatusPending, agent.Status; e != g {
|
||||
return errors.Errorf("agent.Status: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to update spec for an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
var specData map[string]any
|
||||
|
||||
agent, err := repo.UpdateSpec(ctx, unexistantAgentID, string(spec.Name), 0, specData)
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
if agent != nil {
|
||||
return errors.New("agent should be nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to delete spec of an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
|
||||
err := repo.DeleteSpec(ctx, unexistantAgentID, string(spec.Name))
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to get specs of an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
|
||||
specs, err := repo.GetSpecs(ctx, unexistantAgentID)
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, datastore.ErrNotFound) {
|
||||
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
if specs != nil {
|
||||
return errors.Errorf("specs should be nil, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runAgentRepositoryTests(t *testing.T, repo datastore.AgentRepository) {
|
||||
for _, tc := range agentRepositoryTestCases {
|
||||
func(tc agentRepositoryTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if tc.Skip {
|
||||
t.SkipNow()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err := tc.Run(ctx, repo); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt/spec/sysupgrade"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
|
||||
)
|
||||
|
@ -23,6 +23,13 @@ type (
|
||||
ParseOption = jwk.ParseOption
|
||||
)
|
||||
|
||||
var (
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
)
|
||||
|
||||
const AlgorithmKey = jwk.AlgorithmKey
|
||||
|
||||
func Parse(src []byte, options ...jwk.ParseOption) (Set, error) {
|
||||
return jwk.Parse(src, options...)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
@ -23,8 +22,6 @@ func New(migrationDir, driver, dsn string) (*migrate.Migrate, error) {
|
||||
fmt.Sprintf("file://%s/%s", migrationDir, driver),
|
||||
dsn,
|
||||
)
|
||||
|
||||
log.Println(migrationDir, driver, dsn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
29
internal/proxy/host_filter.go
Normal file
29
internal/proxy/host_filter.go
Normal file
@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
)
|
||||
|
||||
func FilterHosts(allowedHostPatterns ...string) Middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if matches := wildcard.MatchAny(r.Host, allowedHostPatterns...); !matches {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func WithAllowedHosts(allowedHostPatterns ...string) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.Middlewares = append(o.Middlewares, FilterHosts(allowedHostPatterns...))
|
||||
}
|
||||
}
|
66
internal/proxy/host_rewrite.go
Normal file
66
internal/proxy/host_rewrite.go
Normal file
@ -0,0 +1,66 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func RewriteHosts(mappings map[string]*url.URL) Middleware {
|
||||
patterns := make([]string, len(mappings))
|
||||
|
||||
for p := range mappings {
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
|
||||
sort.Strings(patterns)
|
||||
reverse(patterns)
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var match *url.URL
|
||||
|
||||
for _, p := range patterns {
|
||||
logger.Debug(ctx, "matching host to pattern", logger.F("host", r.Host), logger.F("pattern", p))
|
||||
|
||||
if matches := wildcard.Match(r.Host, p); !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
match = mappings[p]
|
||||
break
|
||||
}
|
||||
|
||||
if match == nil {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("originalHost", r.Host))
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
originalURL := r.URL.String()
|
||||
|
||||
r.URL.Host = match.Host
|
||||
r.URL.Scheme = match.Scheme
|
||||
|
||||
logger.Debug(ctx, "rewriting url", logger.F("from", originalURL), logger.F("to", r.URL.String()))
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func WithRewriteHosts(mappings map[string]*url.URL) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.Middlewares = append(o.Middlewares, RewriteHosts(mappings))
|
||||
}
|
||||
}
|
33
internal/proxy/middleware.go
Normal file
33
internal/proxy/middleware.go
Normal file
@ -0,0 +1,33 @@
|
||||
package proxy
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Middleware func(h http.Handler) http.Handler
|
||||
|
||||
type ProxyResponseTransformer interface {
|
||||
TransformResponse(*http.Response) error
|
||||
}
|
||||
|
||||
type defaultProxyResponseTransformer struct{}
|
||||
|
||||
// TransformResponse implements ProxyResponseTransformer
|
||||
func (*defaultProxyResponseTransformer) TransformResponse(*http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ ProxyResponseTransformer = &defaultProxyResponseTransformer{}
|
||||
|
||||
type ProxyResponseMiddleware func(ProxyResponseTransformer) ProxyResponseTransformer
|
||||
|
||||
type ProxyRequestTransformer interface {
|
||||
TransformRequest(*http.Request)
|
||||
}
|
||||
|
||||
type ProxyRequestMiddleware func(ProxyRequestTransformer) ProxyRequestTransformer
|
||||
|
||||
type defaultProxyRequestTransformer struct{}
|
||||
|
||||
// TransformRequest implements ProxyRequestTransformer
|
||||
func (*defaultProxyRequestTransformer) TransformRequest(*http.Request) {}
|
||||
|
||||
var _ ProxyRequestTransformer = &defaultProxyRequestTransformer{}
|
29
internal/proxy/options.go
Normal file
29
internal/proxy/options.go
Normal file
@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
type Options struct {
|
||||
Middlewares []Middleware
|
||||
ProxyRequestMiddlewares []ProxyRequestMiddleware
|
||||
ProxyResponseMiddlewares []ProxyResponseMiddleware
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
Middlewares: make([]Middleware, 0),
|
||||
ProxyRequestMiddlewares: make([]ProxyRequestMiddleware, 0),
|
||||
ProxyResponseMiddlewares: make([]ProxyResponseMiddleware, 0),
|
||||
}
|
||||
}
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func WithProxyRequestMiddlewares(middlewares ...ProxyRequestMiddleware) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.ProxyRequestMiddlewares = middlewares
|
||||
}
|
||||
}
|
||||
|
||||
func WithproxyResponseMiddlewares(middlewares ...ProxyResponseMiddleware) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.ProxyResponseMiddlewares = middlewares
|
||||
}
|
||||
}
|
131
internal/proxy/proxy.go
Normal file
131
internal/proxy/proxy.go
Normal file
@ -0,0 +1,131 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
reversers sync.Map
|
||||
handler http.Handler
|
||||
proxyResponseTransformer ProxyResponseTransformer
|
||||
proxyRequestTransformer ProxyRequestTransformer
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Proxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var reverser *httputil.ReverseProxy
|
||||
|
||||
key := fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Host)
|
||||
|
||||
createAndStore := func() {
|
||||
target := &url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.URL.Host,
|
||||
}
|
||||
|
||||
reverser = httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
originalDirector := reverser.Director
|
||||
|
||||
if p.proxyRequestTransformer != nil {
|
||||
reverser.Director = func(r *http.Request) {
|
||||
originalURL := r.URL.String()
|
||||
originalDirector(r)
|
||||
p.proxyRequestTransformer.TransformRequest(r)
|
||||
logger.Debug(ctx, "proxying request", logger.F("targetURL", r.URL.String()), logger.F("originalURL", originalURL))
|
||||
}
|
||||
}
|
||||
|
||||
if p.proxyResponseTransformer != nil {
|
||||
reverser.ModifyResponse = func(r *http.Response) error {
|
||||
if err := p.proxyResponseTransformer.TransformResponse(r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
p.reversers.Store(key, reverser)
|
||||
}
|
||||
|
||||
raw, exists := p.reversers.Load(key)
|
||||
if !exists {
|
||||
createAndStore()
|
||||
}
|
||||
|
||||
reverser, ok := raw.(*httputil.ReverseProxy)
|
||||
if !ok {
|
||||
createAndStore()
|
||||
}
|
||||
|
||||
reverser.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Proxy {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
proxy := &Proxy{}
|
||||
|
||||
handler := http.HandlerFunc(proxy.proxyRequest)
|
||||
proxy.handler = createMiddlewareChain(handler, opts.Middlewares)
|
||||
|
||||
proxy.proxyRequestTransformer = createProxyRequestChain(&defaultProxyRequestTransformer{}, opts.ProxyRequestMiddlewares)
|
||||
proxy.proxyResponseTransformer = createProxyResponseChain(&defaultProxyResponseTransformer{}, opts.ProxyResponseMiddlewares)
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
var _ http.Handler = &Proxy{}
|
||||
|
||||
func createMiddlewareChain(handler http.Handler, middlewares []Middleware) http.Handler {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
handler = m(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func createProxyResponseChain(transformer ProxyResponseTransformer, middlewares []ProxyResponseMiddleware) ProxyResponseTransformer {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
transformer = m(transformer)
|
||||
}
|
||||
|
||||
return transformer
|
||||
}
|
||||
|
||||
func createProxyRequestChain(transformer ProxyRequestTransformer, middlewares []ProxyRequestMiddleware) ProxyRequestTransformer {
|
||||
reverse(middlewares)
|
||||
|
||||
for _, m := range middlewares {
|
||||
transformer = m(transformer)
|
||||
}
|
||||
|
||||
return transformer
|
||||
}
|
||||
|
||||
func reverse[S ~[]E, E any](s S) {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
}
|
44
internal/proxy/wildcard/match.go
Normal file
44
internal/proxy/wildcard/match.go
Normal file
@ -0,0 +1,44 @@
|
||||
package wildcard
|
||||
|
||||
const wildcard = '*'
|
||||
|
||||
func Match(str, pattern string) bool {
|
||||
if pattern == "" {
|
||||
return str == pattern
|
||||
}
|
||||
|
||||
if pattern == string(wildcard) {
|
||||
return true
|
||||
}
|
||||
|
||||
return deepMatchRune([]rune(str), []rune(pattern))
|
||||
}
|
||||
|
||||
func MatchAny(str string, patterns ...string) bool {
|
||||
for _, p := range patterns {
|
||||
if matches := Match(str, p); matches {
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func deepMatchRune(str, pattern []rune) bool {
|
||||
for len(pattern) > 0 {
|
||||
switch pattern[0] {
|
||||
default:
|
||||
if len(str) == 0 || str[0] != pattern[0] {
|
||||
return false
|
||||
}
|
||||
case wildcard:
|
||||
return deepMatchRune(str, pattern[1:]) ||
|
||||
(len(str) > 0 && deepMatchRune(str[1:], pattern))
|
||||
}
|
||||
|
||||
str = str[1:]
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
|
||||
return len(str) == 0 && len(pattern) == 0
|
||||
}
|
@ -145,6 +145,7 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type updateAgentRequest struct {
|
||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Label *string `json:"label" validate:"omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
@ -166,6 +167,10 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||
}
|
||||
|
||||
if updateAgentReq.Label != nil {
|
||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||
}
|
||||
|
||||
agent, err := s.agentRepo.Update(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
|
@ -105,8 +105,8 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
|
||||
agent.NewAuthenticator(s.agentRepo),
|
||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer), thirdparty.DefaultAcceptableSkew),
|
||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
|
@ -1,87 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256sum": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zip",
|
||||
"tar.gz"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"address",
|
||||
"format"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
)
|
||||
|
||||
const NameApp spec.Name = "app.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Apps map[string]AppEntry `json:"apps"`
|
||||
Auth *Auth `json:"auth"`
|
||||
}
|
||||
|
||||
type AppEntry struct {
|
||||
URL string `json:"url"`
|
||||
SHA256Sum string `json:"sha256sum"`
|
||||
Address string `json:"address"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Key any `json:"key"`
|
||||
Accounts []edgeAuth.LocalAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return NameApp
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"apps": s.Apps,
|
||||
"auth": s.Auth,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
42
internal/spec/app/testdata/spec-ok.json
vendored
42
internal/spec/app/testdata/spec-ok.json
vendored
@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "app.emissary.cadoles.com",
|
||||
"data": {
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"address": ":8081",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
1
migrations/sqlite/0000001_agent_label.down.sql
Normal file
1
migrations/sqlite/0000001_agent_label.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents DROP COLUMN label;
|
1
migrations/sqlite/0000001_agent_label.up.sql
Normal file
1
migrations/sqlite/0000001_agent_label.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents ADD COLUMN label TEXT DEFAULT "";
|
1
migrations/sqlite/0000002_agent_contactedat.down.sql
Normal file
1
migrations/sqlite/0000002_agent_contactedat.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents DROP COLUMN contacted_at;
|
1
migrations/sqlite/0000002_agent_contactedat.up.sql
Normal file
1
migrations/sqlite/0000002_agent_contactedat.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE agents ADD COLUMN contacted_at datetime;
|
24
misc/jenkins/Dockerfile
Normal file
24
misc/jenkins/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04
|
||||
|
||||
ARG HTTP_PROXY=
|
||||
ARG HTTPS_PROXY=
|
||||
ARG http_proxy=
|
||||
ARG https_proxy=
|
||||
ARG GO_VERSION=1.19.2
|
||||
|
||||
# Install dev environment dependencies
|
||||
RUN export DEBIAN_FRONTEND=noninteractive &&\
|
||||
apt-get update -y &&\
|
||||
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq
|
||||
|
||||
# Install Go
|
||||
RUN mkdir -p /tmp \
|
||||
&& wget -O /tmp/go${GO_VERSION}.linux-amd64.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
|
||||
&& rm -rf /usr/local/go \
|
||||
&& mkdir -p /usr/local \
|
||||
&& tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz
|
||||
|
||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||
|
||||
# Add LetsEncrypt certificates
|
||||
RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash
|
@ -9,7 +9,7 @@ server:
|
||||
port: 3000
|
||||
database:
|
||||
driver: sqlite
|
||||
dsn: sqlite:///var/lib/emissary/data.sqlite
|
||||
dsn: sqlite:///var/lib/emissary/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000
|
||||
cors:
|
||||
allowedOrigins: []
|
||||
allowCredentials: true
|
||||
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://localhost:3001/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"format": "zip",
|
||||
"address": "127.0.0.1:8081"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
@baseUrl = http://localhost:3000
|
||||
|
||||
### Get agents
|
||||
|
||||
# @name getAgents
|
||||
GET {{ baseUrl }}/api/v1/agents
|
||||
Content-Type: application/json
|
||||
|
||||
@agentId = {{ getAgents.response.body.Data.Agents.0.ID }}
|
||||
|
||||
### Update an agent (accept it)
|
||||
|
||||
PUT {{ baseUrl }}/api/v1/agents/{{ agentId }}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Status": 1
|
||||
}
|
||||
|
||||
### Get an agent
|
||||
|
||||
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}
|
||||
Content-Type: application/json
|
||||
|
||||
### Get an agent specs
|
||||
|
||||
# @name getSpecs
|
||||
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
@specName = {{ getSpecs.response.body.Data.Specs.0.Name }}
|
||||
|
||||
### Update an agent specs
|
||||
|
||||
POST {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Name": "gateway.emissary.cadoles.com",
|
||||
"Revision": 2,
|
||||
"Data": {
|
||||
"gateways": {
|
||||
"cadoles.com":{
|
||||
"address":":3003",
|
||||
"target":"https://www.cadoles.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
### Delete an agent spec
|
||||
|
||||
DELETE {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"Name": "gateway.emissary.cadoles.com"
|
||||
}
|
||||
|
||||
### Update UCI spec with uhttpd config
|
||||
|
||||
POST {{ baseUrl }}/api/v1/agents/2/specs
|
||||
Content-Type: application/json
|
||||
|
||||
< ./uci-spec.payload.json
|
@ -1,163 +0,0 @@
|
||||
{
|
||||
"Name": "uci.emissary.cadoles.com",
|
||||
"Revision": 6,
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
61
misc/spec-samples/app.emissary.cadoles.com.json
Normal file
61
misc/spec-samples/app.emissary.cadoles.com.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"apps": {
|
||||
"edge.portal": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.9-41c100d.zip",
|
||||
"sha256sum": "b73a6741654f3e24281e354b3b506b109dac6ada8a9698452f52b03a53299a7d",
|
||||
"address": ":8082",
|
||||
"format": "zip"
|
||||
},
|
||||
"app.arcad.edge.hextris": {
|
||||
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.4.11-81fb4c4.zip",
|
||||
"sha256sum": "6d70f65971b3dd288da32d8d004ab8fbca030398b5c12e3c052ef98c53a6b81a",
|
||||
"address": ":8083",
|
||||
"format": "zip"
|
||||
},
|
||||
"edge.sdk.client.test": {
|
||||
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.11-f5283b8.zip",
|
||||
"sha256sum": "785d9f8d427900e1bb27ab85a33e8b1cbd1b6a1f8b2eab6366dc215a69655ade",
|
||||
"address": ":8084",
|
||||
"format": "zip"
|
||||
},
|
||||
"arcad.diffusion": {
|
||||
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.9-81046a2.zip",
|
||||
"sha256sum": "b8770adfaaf60e6d3e7776e0a090e6e7a0b31f3f9425b91168b42144d0346513",
|
||||
"address": ":8085",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"appUrlResolving": {
|
||||
"ifaceMappings": {
|
||||
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
"wlp4s0": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||
"enp0s31f6": "http://{{ .DeviceIP }}:{{ .AppPort }}"
|
||||
},
|
||||
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.localhost.arcad.lan:8080"
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
"acceptedHostPatterns": ["arcad.lan", "*.arcad.lan", "arcad-*.local", "*.*.*.*"],
|
||||
"hostTarget": "localhost.arcad.lan"
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": "absolutlynotsecret",
|
||||
"cookieDuration": "1h",
|
||||
"accounts": [
|
||||
{
|
||||
"username": "admin",
|
||||
"algo": "plain",
|
||||
"password": "admin",
|
||||
"claims": {
|
||||
"arcad_role": "admin",
|
||||
"arcad_tenant": "x86",
|
||||
"preferred_username": "Admin",
|
||||
"sub": "admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
29
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"services": {
|
||||
"arcad": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad"
|
||||
},
|
||||
"portal": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-portal"
|
||||
},
|
||||
"hextris": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-hextris"
|
||||
},
|
||||
"test": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-test"
|
||||
},
|
||||
"diffusion": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad-diffusion"
|
||||
}
|
||||
}
|
||||
}
|
45
misc/spec-samples/proxy.emissary.cadoles.com.json
Normal file
45
misc/spec-samples/proxy.emissary.cadoles.com.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"proxies": {
|
||||
"main": {
|
||||
"address": ":8080",
|
||||
"mappings": [
|
||||
{
|
||||
"hostPattern": "portal.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8082"
|
||||
},
|
||||
{
|
||||
"hostPattern": "hextris.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8083"
|
||||
},
|
||||
{
|
||||
"hostPattern": "test.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8084"
|
||||
},
|
||||
{
|
||||
"hostPattern": "diffusion.localhost.arcad.lan:*",
|
||||
"target": "http://localhost:8085"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-portal.local:*",
|
||||
"target": "http://localhost:8082"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-hextris.local:*",
|
||||
"target": "http://localhost:8083"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-test.local:*",
|
||||
"target": "http://localhost:8084"
|
||||
},
|
||||
{
|
||||
"hostPattern": "arcad-diffusion.local:*",
|
||||
"target": "http://localhost:8085"
|
||||
},
|
||||
{
|
||||
"hostPattern": "*",
|
||||
"target": "http://localhost:8082"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user