Compare commits

..

1 Commits

Author SHA1 Message Date
5fc5f5fd5d chore: add jenkins pipeline
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-03-31 17:00:08 +02:00
44 changed files with 242 additions and 1090 deletions

2
.gitignore vendored
View File

@ -4,7 +4,7 @@ dist/
/tools /tools
/tmp /tmp
/state.json /state.json
/emissary.sqlite* /emissary.sqlite
/.gitea-release /.gitea-release
/agent-key.json /agent-key.json
/apps /apps

40
Jenkinsfile vendored
View File

@ -10,30 +10,19 @@ pipeline {
} }
stages { 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') { stage('Run unit tests') {
steps { steps {
script { script {
withCredentials([ withCredentials([
usernamePassword([ usernamePassword([
credentialsId: 'forge-jenkins', credentialsId: 'forge-jenkins',
usernameVariable: 'GIT_USERNAME', usernameVariable: 'GIT_USERNAME',
passwordVariable: 'GIT_PASSWORD' passwordVariable: 'GIT_PASSWORD'
]) ])
]) { ]) {
sh ''' sh '''
git config --global credential.https://forge.cadoles.com.username "$GIT_USERNAME" 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' 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 export GOPRIVATE=forge.cadoles.com/arcad/edge
make test make test
''' '''
@ -53,25 +42,20 @@ pipeline {
script { script {
withCredentials([ withCredentials([
usernamePassword([ usernamePassword([
credentialsId: 'forge-jenkins', credentialsId: 'forge-jenkins',
usernameVariable: 'GITEA_RELEASE_USERNAME', usernameVariable: 'GITEA_RELEASE_USERNAME',
passwordVariable: 'GITEA_RELEASE_PASSWORD' passwordVariable: 'GITEA_RELEASE_PASSWORD'
]) ])
]) { ]) {
sh 'make gitea-release' sh 'make gitea-release'
} }
def currentVersion = sh(returnStdout: true, script: 'make full-version').trim() def currentVersion = sh(returnStdout: true, script: 'make full-version').trim()
if (currentVersion.endsWith('-dirty')) { build(
unstable('Could not trigger emissary-firmware build, dirty version !') job: "../emissary-firmware/${env.GIT_BRANCH}",
} else { parameters: [
build( [$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
job: "../emissary-firmware/${env.GIT_BRANCH}", ]
parameters: [ )
[$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
],
wait: false
)
}
} }
} }
} }

View File

@ -151,15 +151,6 @@ AGENT_ID ?= 1
load-sample-specs: 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/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/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: full-version:
@echo $(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
git add go.mod go.sum
git commit -m "feat: update arcad/edge dependency"
git push

View File

@ -7,7 +7,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/agent" "forge.cadoles.com/Cadoles/emissary/internal/command/agent"
"forge.cadoles.com/Cadoles/emissary/internal/command/api" "forge.cadoles.com/Cadoles/emissary/internal/command/api"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec" _ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
) )

3
go.mod
View File

@ -3,11 +3,10 @@ module forge.cadoles.com/Cadoles/emissary
go 1.19 go 1.19
require ( require (
forge.cadoles.com/arcad/edge v0.0.0-20230411091100-98ebd7a1689e forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/participle/v2 v2.0.0-beta.5 github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 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/btcsuite/btcd/btcutil v1.1.3
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/denisbrodbeck/machineid v1.0.1 github.com/denisbrodbeck/machineid v1.0.1

9
go.sum
View File

@ -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.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 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= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
forge.cadoles.com/arcad/edge v0.0.0-20230411091100-98ebd7a1689e h1:IPDTOO5u09ZjX6lmWuKNsrvgNMTaNY0/l7IwolXmUdE= forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/VBx/PzL2riC0zNuQ/cxxf5r3AmEvJE=
forge.cadoles.com/arcad/edge v0.0.0-20230411091100-98ebd7a1689e/go.mod h1:Vx4iq/oewXUOkGyi8QKc14clTLNO1sWpb0SjBYELlAs= forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= 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/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= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@ -201,8 +201,6 @@ 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/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/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/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/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.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
@ -980,7 +978,6 @@ 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 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.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.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 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= 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/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@ -1516,7 +1513,6 @@ 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-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-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-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-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-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -1807,7 +1803,6 @@ 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.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/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.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.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.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=

View File

@ -44,6 +44,8 @@ func (a *Agent) Run(ctx context.Context) error {
if err := a.registerAgent(ctx, client, state); err != nil { if err := a.registerAgent(ctx, client, state); err != nil {
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
return
} }
logger.Debug(ctx, "state before reconciliation", logger.F("state", state)) logger.Debug(ctx, "state before reconciliation", logger.F("state", state))
@ -79,7 +81,7 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
) )
if err := ctrl.Reconcile(ctrlCtx, state); err != nil { if err := ctrl.Reconcile(ctrlCtx, state); err != nil {
logger.Error(ctx, "could not reconcile", logger.E(errors.WithStack(err))) return errors.WithStack(err)
} }
} }

View File

@ -4,8 +4,8 @@ import (
"bytes" "bytes"
"context" "context"
"database/sql" "database/sql"
"net"
"path/filepath" "path/filepath"
"sync"
"text/template" "text/template"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
@ -20,18 +20,14 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/auth" "forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/blob" "forge.cadoles.com/arcad/edge/pkg/module/blob"
"forge.cadoles.com/arcad/edge/pkg/module/cast" "forge.cadoles.com/arcad/edge/pkg/module/cast"
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch" "forge.cadoles.com/arcad/edge/pkg/module/net"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/pkg/errors" "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) { func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
dataDir, err := c.ensureAppDataDir(ctx, appKey) dataDir, err := c.ensureAppDataDir(ctx, appKey)
if err != nil { if err != nil {
@ -39,7 +35,7 @@ func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs
} }
dbFile := filepath.Join(dataDir, appKey+".sqlite") dbFile := filepath.Join(dataDir, appKey+".sqlite")
db, err := sqlite.Open(dbFile + defaultSQLiteParams) db, err := sqlite.Open(dbFile)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile) return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
} }
@ -108,135 +104,46 @@ func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
return keySet, nil return keySet, nil
} }
func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) { func createGetAppURL(specs *spec.Spec) GetURLFunc {
rawIfaceMappings := make(map[string]string, 0) var (
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil { compileOnce sync.Once
rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings urlTemplate *template.Template
} err error
)
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings)) return func(ctx context.Context, manifest *app.Manifest) (string, error) {
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 { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
var appURLTemplate string
if specs.Config == nil || specs.Config.AppURLTemplate == "" {
appURLTemplate = `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.local`
} else {
appURLTemplate = specs.Config.AppURLTemplate
}
compileOnce.Do(func() {
urlTemplate, err = template.New("").Funcs(sprig.TxtFuncMap()).Parse(appURLTemplate)
})
var buf bytes.Buffer
data := struct { data := struct {
Manifest *app.Manifest Manifest *app.Manifest
Specs *spec.Spec Specs *spec.Spec
DeviceIP string
AppPort string
}{ }{
Manifest: manifest, Manifest: manifest,
Specs: specs, Specs: specs,
DeviceIP: deviceIP.String(),
AppPort: port,
} }
var buf bytes.Buffer
if err := urlTemplate.Execute(&buf, data); err != nil { if err := urlTemplate.Execute(&buf, data); err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
return buf.String(), nil return buf.String(), nil
}, nil }
} }
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory { func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
@ -248,7 +155,7 @@ func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec,
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
cast.CastModuleFactory(), cast.CastModuleFactory(),
module.LifecycleModuleFactory(), module.LifecycleModuleFactory(),
netModule.ModuleFactory(bus), net.ModuleFactory(bus),
module.RPCModuleFactory(bus), module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds), module.StoreModuleFactory(ds),
blob.ModuleFactory(bus, bs), blob.ModuleFactory(bus, bs),
@ -277,6 +184,5 @@ func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec,
}, },
), ),
appModule.ModuleFactory(c.appRepository), appModule.ModuleFactory(c.appRepository),
fetchModule.ModuleFactory(bus),
} }
} }

View File

@ -1,73 +0,0 @@
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)
}
}

View File

@ -11,12 +11,12 @@ import (
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error) type GetURLFunc func(context.Context, *app.Manifest) (string, error)
type AppRepository struct { type AppRepository struct {
resolveAppURL ResolveAppURLFunc getURL GetURLFunc
bundles []string bundles []string
mutex sync.RWMutex mutex sync.RWMutex
} }
// Get implements app.Repository // Get implements app.Repository
@ -33,7 +33,7 @@ func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, erro
} }
// GetURL implements app.Repository // GetURL implements app.Repository
func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (string, error) { func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()
@ -42,7 +42,7 @@ func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (str
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
url, err := r.resolveAppURL(ctx, manifest, from) url, err := r.getURL(ctx, manifest)
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStack(err)
} }
@ -80,11 +80,11 @@ func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
return manifests, nil return manifests, nil
} }
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) { func (r *AppRepository) Update(getURL GetURLFunc, bundles []string) {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
r.resolveAppURL = resolveAppURL r.getURL = getURL
r.bundles = bundles r.bundles = bundles
} }
@ -118,7 +118,7 @@ func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manif
func NewAppRepository() *AppRepository { func NewAppRepository() *AppRepository {
return &AppRepository{ return &AppRepository{
resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) { getURL: func(ctx context.Context, m *app.Manifest) (string, error) {
return "", errors.New("unavailable") return "", errors.New("unavailable")
}, },
bundles: []string{}, bundles: []string{},

View File

@ -96,14 +96,7 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
} }
} }
if err := c.updateAppRepository(ctx, specs); err != nil { c.updateAppRepository(ctx, specs)
logger.Error(
ctx, "could not update app repository",
logger.E(errors.WithStack(err)),
)
return
}
// (Re)start apps if necessary // (Re)start apps if necessary
for appKey := range specs.Apps { for appKey := range specs.Apps {
@ -116,32 +109,32 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
} }
} }
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error { func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) {
bundles := make([]string, 0, len(specs.Apps)) bundles := make([]string, 0, len(specs.Apps))
for appKey, app := range specs.Apps { for appKey, app := range specs.Apps {
path := c.getAppBundlePath(appKey, app.Format) path := c.getAppBundlePath(appKey, app.Format)
bundles = append(bundles, path) bundles = append(bundles, path)
} }
resolveAppURL, err := createResolveAppURL(specs) getURL := createGetAppURL(specs)
if err != nil {
return errors.WithStack(err)
}
c.appRepository.Update(resolveAppURL, bundles) c.appRepository.Update(getURL, bundles)
return nil
} }
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) { func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
appEntry := specs.Apps[appKey] appEntry := specs.Apps[appKey]
var auth *spec.Auth
if specs.Config != nil {
auth = specs.Config.Auth
}
appDef := struct { appDef := struct {
App spec.AppEntry App spec.AppEntry
Config *spec.Config Auth *spec.Auth
}{ }{
App: appEntry, App: appEntry,
Config: specs.Config, Auth: auth,
} }
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil) newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
@ -171,30 +164,27 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
server = nil server = nil
} }
newServerEntry := func() (*serverEntry, error) { if server == nil {
options, err := c.getHandlerOptions(ctx, appKey, specs) options, err := c.getHandlerOptions(ctx, appKey, specs)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not create handler options") return errors.Wrap(err, "could not create handler options")
}
var auth *spec.Auth
if specs.Config != nil {
auth = specs.Config.Auth
} }
server = &serverEntry{ server = &serverEntry{
Server: NewServer(bundle, specs.Config, options...), Server: NewServer(bundle, auth, options...),
AppDefHash: 0, AppDefHash: 0,
} }
return server, nil c.servers[appKey] = server
}
if server == nil {
serverEntry, err := newServerEntry()
if err != nil {
return errors.WithStack(err)
}
c.servers[appKey] = serverEntry
} }
defChanged := newAppDefHash != server.AppDefHash defChanged := newAppDefHash != server.AppDefHash
if server.Server.Running() && !defChanged { if server.Server.Running() && !defChanged {
return nil return nil
} }
@ -204,17 +194,6 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
ctx, "restarting app", ctx, "restarting app",
logger.F("address", appEntry.Address), 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 { } else {
logger.Info( logger.Info(
ctx, "starting app", ctx, "starting app",

View File

@ -2,7 +2,6 @@ package app
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@ -32,19 +31,16 @@ type Server struct {
handlerOptions []edgeHTTP.HandlerOptionFunc handlerOptions []edgeHTTP.HandlerOptionFunc
server *http.Server server *http.Server
serverMutex sync.RWMutex serverMutex sync.RWMutex
config *appSpec.Config auth *appSpec.Auth
} }
func (s *Server) Start(ctx context.Context, addr string) (err error) { func (s *Server) Start(ctx context.Context, addr string) (err error) {
if s.Running() { if s.server != nil {
if err := s.Stop(); err != nil { if err := s.Stop(); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
} }
s.serverMutex.Lock()
defer s.serverMutex.Unlock()
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.Logger) router.Use(middleware.Logger)
@ -54,19 +50,8 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
return errors.Wrap(err, "could not load app bundle") return errors.Wrap(err, "could not load app bundle")
} }
if s.config != nil { if err := s.configureAuth(router, s.auth); err != nil {
if s.config.UnexpectedHostRedirect != nil { return errors.WithStack(err)
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) router.Handle("/*", handler)
@ -100,7 +85,9 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
} }
}() }()
s.serverMutex.Lock()
s.server = server s.server = server
s.serverMutex.Unlock()
return nil return nil
} }
@ -113,32 +100,31 @@ func (s *Server) Running() bool {
} }
func (s *Server) Stop() error { func (s *Server) Stop() error {
if !s.Running() {
return nil
}
s.serverMutex.Lock()
defer s.serverMutex.Unlock()
if s.server == nil { if s.server == nil {
return nil return nil
} }
if err := s.server.Close(); err != nil { defer func() {
s.serverMutex.Lock()
s.server = nil s.server = nil
s.serverMutex.Unlock()
}()
if err := s.server.Close(); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
s.server = nil
return nil return nil
} }
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error { func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
if auth == nil {
return nil
}
switch { switch {
case auth.Local != nil: case auth.Local != nil:
var rawKey any = auth.Local.Key var rawKey any = s.auth.Local.Key
if strKey, ok := rawKey.(string); ok { if strKey, ok := rawKey.(string); ok {
rawKey = []byte(strKey) rawKey = []byte(strKey)
} }
@ -149,72 +135,54 @@ func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
} }
cookieDuration := defaultCookieDuration cookieDuration := defaultCookieDuration
if auth.Local.CookieDuration != "" { if s.auth.Local.CookieDuration != "" {
cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration) cookieDuration, err = time.ParseDuration(s.auth.Local.CookieDuration)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
} }
if s.auth.Local.CookieDomain != "" {
router.Use(invalidCookieDomainRedirect(s.auth.Local.CookieDomain))
}
router.Handle("/auth/*", authHTTP.NewLocalHandler( router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key, jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"), authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(auth.Local.Accounts...), authHTTP.WithAccounts(s.auth.Local.Accounts...),
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration), authHTTP.WithCookieOptions(s.auth.Local.CookieDomain, cookieDuration),
)) ))
} }
return nil return nil
} }
func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server { func NewServer(bundle bundle.Bundle, auth *appSpec.Auth, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
return &Server{ return &Server{
bundle: bundle, bundle: bundle,
config: config, auth: auth,
handlerOptions: handlerOptions, handlerOptions: handlerOptions,
} }
} }
func getCookieDomain(r *http.Request) (string, error) { func invalidCookieDomainRedirect(cookieDomain string) func(http.Handler) http.Handler {
host, _, err := net.SplitHostPort(r.Host) domain := strings.TrimPrefix(cookieDomain, ".")
if err != nil { hostPattern := "*" + domain
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 { return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
host, port, err := net.SplitHostPort(r.Host) hostParts := strings.SplitN(r.Host, ":", 2)
if err != nil {
host = r.Host
}
matched := wildcard.MatchAny(host, acceptedHostPatterns...) if !wildcard.Match(hostParts[0], hostPattern) {
if !matched {
url := r.URL url := r.URL
url.Host = hostTarget newHost := domain
if port != "" { if len(hostParts) > 1 {
url.Host += ":" + port newHost += ":" + hostParts[1]
} }
url.Host = newHost
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect) http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
return return

View File

@ -38,94 +38,57 @@
} }
} }
}, },
"config": { "auth": {
"type": "object", "type": "object",
"properties": { "properties": {
"appUrlResolving": { "local": {
"type": "object", "type": "object",
"properties": { "properties": {
"ifaceMappings": { "key": {
"type": "object", "type": ["object", "string"]
"patternProperties": {
".*": {
"type": "string"
}
}
}, },
"defaultUrlTemplate": { "accounts": {
"type": "string"
}
},
"required": ["defaultUrlTemplate"],
"additionalProperties": false
},
"unexpectedHostRedirect": {
"type": "object",
"properties": {
"acceptedHostPatterns": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"algo": {
"type": "string"
},
"claims": {
"type": "object"
}
},
"required": [
"username",
"password",
"algo"
]
} }
}, },
"hostTarget": { "cookieDomain": {
"type": "string"
},
"cookieDuration": {
"type": "string" "type": "string"
} }
}, },
"required": ["acceptedHostPatterns", "hostTarget"], "required": [
"additionalProperties": false "key"
}, ]
"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 },
"config": {
"appUrlTemplate": {
"type": "string"
}
} }
}, },
"required": [ "required": [

View File

@ -32,19 +32,8 @@ type LocalAuth struct {
} }
type Config struct { type Config struct {
Auth *Auth `json:"auth"` Auth *Auth `json:"auth"`
UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"` AppURLTemplate string `json:"appUrlTemplate"`
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 { func (s *Spec) SpecName() spec.Name {

View File

@ -9,44 +9,32 @@
"format": "zip" "format": "zip"
} }
}, },
"config": { "auth": {
"auth": { "local": {
"local": { "key": {
"key": { "d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ", "dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE", "dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc", "e": "AQAB",
"e": "AQAB", "kty": "RSA",
"kty": "RSA", "n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w", "p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE", "q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8", "qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
"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" "accounts": [
{
"username": "foo",
"algo": "plain",
"password": "bar",
"claims": {
"arcad_role": "user",
"arcad_tenant": "dev.cli",
"preferred_username": "Foo",
"sub": "foo"
}
}
]
} }
} }
}, },

View File

@ -1,181 +0,0 @@
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{}

View File

@ -1,17 +0,0 @@
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))
}
}

View File

@ -1,47 +0,0 @@
{
"$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
}

View File

@ -1,42 +0,0 @@
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{}

View File

@ -1,15 +0,0 @@
{
"name": "mdns.emissary.cadoles.com",
"data": {
"services": {
"My Website": {
"type": "_http._tcp",
"domain": "local",
"host": "mywebsite",
"ifaces": ["lo", "eth0"],
"port": 80
}
}
},
"revision": 0
}

View File

@ -1,65 +0,0 @@
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)
}
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"net/http" "net/http"
"strings" "strings"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/auth" "forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/datastore" "forge.cadoles.com/Cadoles/emissary/internal/datastore"
@ -14,11 +13,8 @@ import (
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
const DefaultAcceptableSkew = 5 * time.Minute
type Authenticator struct { type Authenticator struct {
repo datastore.AgentRepository repo datastore.AgentRepository
acceptableSkew time.Duration
} }
// Authenticate implements auth.Authenticator. // Authenticate implements auth.Authenticator.
@ -75,19 +71,11 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
[]byte(rawToken), []byte(rawToken),
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)), jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
jwt.WithValidate(true), jwt.WithValidate(true),
jwt.WithAcceptableSkew(a.acceptableSkew),
) )
if err != nil { if err != nil {
return nil, errors.WithStack(err) 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{ user := &User{
agent: agent, agent: agent,
} }
@ -95,10 +83,9 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return user, nil return user, nil
} }
func NewAuthenticator(repo datastore.AgentRepository, acceptableSkew time.Duration) *Authenticator { func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
return &Authenticator{ return &Authenticator{
repo: repo, repo: repo,
acceptableSkew: acceptableSkew,
} }
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"net/http" "net/http"
"strings" "strings"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/auth" "forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/jwk" "forge.cadoles.com/Cadoles/emissary/internal/jwk"
@ -12,12 +11,9 @@ import (
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
const DefaultAcceptableSkew = 5 * time.Minute
type Authenticator struct { type Authenticator struct {
keys jwk.Set keys jwk.Set
issuer string issuer string
acceptableSkew time.Duration
} }
// Authenticate implements auth.Authenticator. // Authenticate implements auth.Authenticator.
@ -34,7 +30,7 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return nil, errors.WithStack(auth.ErrUnauthenticated) return nil, errors.WithStack(auth.ErrUnauthenticated)
} }
token, err := parseToken(ctx, a.keys, a.issuer, rawToken, a.acceptableSkew) token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -61,11 +57,10 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return user, nil return user, nil
} }
func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator { func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
return &Authenticator{ return &Authenticator{
keys: keys, keys: keys,
issuer: issuer, issuer: issuer,
acceptableSkew: acceptableSkew,
} }
} }

View File

@ -13,13 +13,12 @@ import (
const keyRole = "role" const keyRole = "role"
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) { func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
token, err := jwt.Parse( token, err := jwt.Parse(
[]byte(rawToken), []byte(rawToken),
jwt.WithKeySet(keys, jws.WithRequireKid(false)), jwt.WithKeySet(keys, jws.WithRequireKid(false)),
jwt.WithIssuer(issuer), jwt.WithIssuer(issuer),
jwt.WithValidate(true), jwt.WithValidate(true),
jwt.WithAcceptableSkew(acceptableSkew),
) )
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)

View File

@ -10,7 +10,6 @@ import (
type UpdateAgentOptions struct { type UpdateAgentOptions struct {
Status *int Status *int
Label *string
Options []OptionFunc Options []OptionFunc
} }
@ -22,12 +21,6 @@ func WithAgentStatus(status int) UpdateAgentOptionFunc {
} }
} }
func WithAgentLabel(label string) UpdateAgentOptionFunc {
return func(opts *UpdateAgentOptions) {
opts.Label = &label
}
}
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc { func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
return func(opts *UpdateAgentOptions) { return func(opts *UpdateAgentOptions) {
opts.Options = funcs opts.Options = funcs
@ -46,10 +39,6 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
payload["status"] = *opts.Status payload["status"] = *opts.Status
} }
if opts.Label != nil {
payload["label"] = *opts.Label
}
response := withResponse[struct { response := withResponse[struct {
Agent *datastore.Agent `json:"agent"` Agent *datastore.Agent `json:"agent"`
}]() }]()

View File

@ -5,7 +5,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/agent" "forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app" "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/openwrt"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy"
@ -50,6 +49,10 @@ func RunCommand() *cli.Command {
controllers = append(controllers, spec.NewController()) controllers = append(controllers, spec.NewController())
} }
if ctrlConf.Proxy.Enabled {
controllers = append(controllers, proxy.NewController())
}
if ctrlConf.UCI.Enabled { if ctrlConf.UCI.Enabled {
controllers = append(controllers, openwrt.NewUCIController( controllers = append(controllers, openwrt.NewUCIController(
string(ctrlConf.UCI.BinPath), string(ctrlConf.UCI.BinPath),
@ -63,14 +66,6 @@ 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 { if ctrlConf.SysUpgrade.Enabled {
sysUpgradeArgs := make([]string, 0) sysUpgradeArgs := make([]string, 0)
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 { if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {

View File

@ -22,11 +22,6 @@ func UpdateCommand() *cli.Command {
Usage: "Set `STATUS` to selected agent", Usage: "Set `STATUS` to selected agent",
Value: -1, Value: -1,
}, },
&cli.StringFlag{
Name: "label",
Usage: "Set `LABEL` to selected agent",
Value: "",
},
), ),
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx) baseFlags := clientFlag.GetBaseFlags(ctx)
@ -48,11 +43,6 @@ func UpdateCommand() *cli.Command {
options = append(options, client.WithAgentStatus(status)) 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)) client := client.New(baseFlags.ServerURL, client.WithToken(token))
agent, err := client.UpdateAgent(ctx.Context, agentID, options...) agent, err := client.UpdateAgent(ctx.Context, agentID, options...)

View File

@ -7,10 +7,9 @@ func agentHints(outputMode format.OutputMode) format.Hints {
OutputMode: outputMode, OutputMode: outputMode,
Props: []format.Prop{ Props: []format.Prop{
format.NewProp("ID", "ID"), format.NewProp("ID", "ID"),
format.NewProp("Label", "Label"),
format.NewProp("Thumbprint", "Thumbprint"), format.NewProp("Thumbprint", "Thumbprint"),
format.NewProp("Status", "Status"), format.NewProp("Status", "Status"),
format.NewProp("ContactedAt", "ContactedAt"), format.NewProp("CreatedAt", "CreatedAt"),
format.NewProp("UpdatedAt", "UpdatedAt"), format.NewProp("UpdatedAt", "UpdatedAt"),
}, },
} }

View File

@ -23,7 +23,6 @@ type ControllersConfig struct {
UCI UCIControllerConfig `yaml:"uci"` UCI UCIControllerConfig `yaml:"uci"`
App AppControllerConfig `yaml:"app"` App AppControllerConfig `yaml:"app"`
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"` SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
MDNS MDNSControllerConfig `yaml:"mdns"`
} }
type PersistenceControllerConfig struct { type PersistenceControllerConfig struct {
@ -56,10 +55,6 @@ type SysUpgradeControllerConfig struct {
FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"` FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"`
} }
type MDNSControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
}
func NewDefaultAgentConfig() AgentConfig { func NewDefaultAgentConfig() AgentConfig {
return AgentConfig{ return AgentConfig{
ServerURL: "http://127.0.0.1:3000", ServerURL: "http://127.0.0.1:3000",
@ -91,9 +86,6 @@ func NewDefaultAgentConfig() AgentConfig {
SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate}, SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate},
FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`}, FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`},
}, },
MDNS: MDNSControllerConfig{
Enabled: true,
},
}, },
Collectors: []ShellCollectorConfig{ Collectors: []ShellCollectorConfig{
{ {

View File

@ -15,6 +15,6 @@ type DatabaseConfig struct {
func NewDefaultDatabaseConfig() DatabaseConfig { func NewDefaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{ return DatabaseConfig{
Driver: "sqlite", Driver: "sqlite",
DSN: "sqlite://emissary.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000", DSN: "sqlite://emissary.sqlite?_fk=true&_journal=WAL",
} }
} }

View File

@ -20,15 +20,13 @@ const (
) )
type Agent struct { type Agent struct {
ID AgentID `json:"id"` ID AgentID `json:"id"`
Label string `json:"label"` Thumbprint string `json:"thumbprint"`
Thumbprint string `json:"thumbprint"` KeySet *SerializableKeySet `json:"keyset,omitempty"`
KeySet *SerializableKeySet `json:"keyset,omitempty"` Metadata map[string]any `json:"metadata,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"` Status AgentStatus `json:"status"`
Status AgentStatus `json:"status"` CreatedAt time.Time `json:"createdAt"`
CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"`
UpdatedAt time.Time `json:"updatedAt"`
ContactedAt *time.Time `json:"contactedAt,omitempty"`
} }
type SerializableKeySet struct { type SerializableKeySet struct {

View File

@ -2,7 +2,6 @@ package datastore
import ( import (
"context" "context"
"time"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
) )
@ -69,12 +68,10 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
type AgentUpdateOptionFunc func(*AgentUpdateOptions) type AgentUpdateOptionFunc func(*AgentUpdateOptions)
type AgentUpdateOptions struct { type AgentUpdateOptions struct {
Label *string Status *AgentStatus
Status *AgentStatus Metadata *map[string]any
ContactedAt *time.Time KeySet *jwk.Set
Metadata *map[string]any Thumbprint *string
KeySet *jwk.Set
Thumbprint *string
} }
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc { func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
@ -100,15 +97,3 @@ func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
opts.Thumbprint = &thumbprint 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
}
}

View File

@ -127,7 +127,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
count := 0 count := 0
err := r.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents` query := `SELECT id, thumbprint, status, created_at, updated_at FROM agents`
limit := 10 limit := 10
if options.Limit != nil { if options.Limit != nil {
@ -194,16 +194,12 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
agent := &datastore.Agent{} agent := &datastore.Agent{}
metadata := JSONMap{} metadata := JSONMap{}
contactedAt := sql.NullTime{}
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil { if err := rows.Scan(&agent.ID, &agent.Thumbprint, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
agent.Metadata = metadata agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
agents = append(agents, agent) agents = append(agents, agent)
} }
@ -319,7 +315,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
err := r.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at" SELECT "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
FROM agents FROM agents
WHERE id = $1 WHERE id = $1
` `
@ -327,10 +323,9 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
row := r.db.QueryRowContext(ctx, query, id) row := r.db.QueryRowContext(ctx, query, id)
metadata := JSONMap{} metadata := JSONMap{}
contactedAt := sql.NullTime{}
var rawKeySet []byte var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil { if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound return datastore.ErrNotFound
} }
@ -339,9 +334,6 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
} }
agent.Metadata = metadata agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
keySet := jwk.NewSet() keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil { if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
@ -370,11 +362,15 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
err := r.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
UPDATE agents SET id = $1 UPDATE agents SET updated_at = $2
` `
args := []any{id} now := time.Now().UTC()
index := 2
args := []any{
id, now,
}
index := 3
if options.Status != nil { if options.Status != nil {
query += fmt.Sprintf(`, status = $%d`, index) query += fmt.Sprintf(`, status = $%d`, index)
@ -399,51 +395,23 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
index++ 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 { if options.Metadata != nil {
query += fmt.Sprintf(`, metadata = $%d`, index) query += fmt.Sprintf(`, metadata = $%d`, index)
args = append(args, JSONMap(*options.Metadata)) args = append(args, JSONMap(*options.Metadata))
index++ 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 += ` query += `
WHERE id = $1 WHERE id = $1
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at" RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
` `
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
row := tx.QueryRowContext(ctx, query, args...) row := tx.QueryRowContext(ctx, query, args...)
metadata := JSONMap{} metadata := JSONMap{}
contactedAt := sql.NullTime{}
var rawKeySet []byte var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil { if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound return datastore.ErrNotFound
} }
@ -452,9 +420,6 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
} }
agent.Metadata = metadata agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
keySet := jwk.NewSet() keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil { if err := json.Unmarshal(rawKeySet, &keySet); err != nil {

View File

@ -2,7 +2,6 @@ package spec
import ( import (
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec" _ "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/agent/controller/openwrt/spec/sysupgrade"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy" _ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci" _ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"

View File

@ -145,7 +145,6 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
type updateAgentRequest struct { type updateAgentRequest struct {
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"` 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) { func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
@ -167,10 +166,6 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status)) options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
} }
if updateAgentReq.Label != nil {
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
}
agent, err := s.agentRepo.Update( agent, err := s.agentRepo.Update(
ctx, ctx,
datastore.AgentID(agentID), datastore.AgentID(agentID),

View File

@ -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.Group(func(r chi.Router) {
r.Use(auth.Middleware( r.Use(auth.Middleware(
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer), thirdparty.DefaultAcceptableSkew), thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew), agent.NewAuthenticator(s.agentRepo),
)) ))
r.Route("/agents", func(r chi.Router) { r.Route("/agents", func(r chi.Router) {

View File

@ -1 +0,0 @@
ALTER TABLE agents DROP COLUMN label;

View File

@ -1 +0,0 @@
ALTER TABLE agents ADD COLUMN label TEXT DEFAULT "";

View File

@ -1 +0,0 @@
ALTER TABLE agents DROP COLUMN contacted_at;

View File

@ -1 +0,0 @@
ALTER TABLE agents ADD COLUMN contacted_at datetime;

View File

@ -9,7 +9,7 @@ server:
port: 3000 port: 3000
database: database:
driver: sqlite driver: sqlite
dsn: sqlite:///var/lib/emissary/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000 dsn: sqlite:///var/lib/emissary/data.sqlite?_fk=true&_journal=WAL
cors: cors:
allowedOrigins: [] allowedOrigins: []
allowCredentials: true allowCredentials: true

View File

@ -1,46 +1,36 @@
{ {
"apps": { "apps": {
"edge.portal": { "portal": {
"url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.5-45546c4.zip", "url": "https://emissary.cadol.es/files/apps/arcad.portal_v2023.3.28-3feda80.zip",
"sha256sum": "c83e7e4b3785f5f4d3fcae7cad334819626015b11b446520aa79f42176a2744d", "sha256sum": "921402c44a5fa554d5b630d1284957b05416aa6872b402314cf52e964e06fac5",
"address": ":8082", "address": "127.0.0.1:8082",
"format": "zip" "format": "zip"
}, },
"app.arcad.edge.hextris": { "hextris": {
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.3.22-33ece28.zip", "url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.3.22-33ece28.zip",
"sha256sum": "5f9f3c8d6f22796beb051d747d7ff12efa17af9d1552c0ab08baef13703a2aba", "sha256sum": "5f9f3c8d6f22796beb051d747d7ff12efa17af9d1552c0ab08baef13703a2aba",
"address": ":8083", "address": "127.0.0.1:8083",
"format": "zip" "format": "zip"
}, },
"edge.sdk.client.test": { "test": {
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.2-f08f645.zip", "url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.3.24-ed535b6.zip",
"sha256sum": "8b48388c817802ebeb38907b3a42f1189dc0759f94c5f33de4546c1a7ebfc784", "sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623",
"address": ":8084", "address": "127.0.0.1:8084",
"format": "zip" "format": "zip"
}, },
"arcad.diffusion": { "diffusion": {
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.5-ffcd1c7.zip", "url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.3.29-5b3fab4.zip",
"sha256sum": "a51a961212470ce1de4527aaaec9e8e0286a978ec675ff9df29b2029daf05a55", "sha256sum": "1282e75719beedbc7c7e67879389d0f3e11c86d3d2c37cf13da624a66faaeb58",
"address": ":8085", "address": "127.0.0.1:8085",
"format": "zip" "format": "zip"
} }
}, },
"config": { "config": {
"appUrlResolving": { "appUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local:8080",
"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": { "auth": {
"local": { "local": {
"key": "absolutlynotsecret", "key": "absolutlynotsecret",
"cookieDomain": ".arcad.local",
"cookieDuration": "1h", "cookieDuration": "1h",
"accounts": [ "accounts": [
{ {

View File

@ -1,29 +0,0 @@
{
"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"
}
}
}

View File

@ -4,35 +4,19 @@
"address": ":8080", "address": ":8080",
"mappings": [ "mappings": [
{ {
"hostPattern": "portal.localhost.arcad.lan:*", "hostPattern": "portal.arcad.local:*",
"target": "http://localhost:8082" "target": "http://localhost:8082"
}, },
{ {
"hostPattern": "hextris.localhost.arcad.lan:*", "hostPattern": "hextris.arcad.local:*",
"target": "http://localhost:8083" "target": "http://localhost:8083"
}, },
{ {
"hostPattern": "test.localhost.arcad.lan:*", "hostPattern": "test.arcad.local:*",
"target": "http://localhost:8084" "target": "http://localhost:8084"
}, },
{ {
"hostPattern": "diffusion.localhost.arcad.lan:*", "hostPattern": "diffusion.arcad.local:*",
"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" "target": "http://localhost:8085"
}, },
{ {