Compare commits
5 Commits
v2023.4.1-
...
v2023.4.4-
Author | SHA1 | Date | |
---|---|---|---|
242a247222 | |||
562d698066 | |||
909549f056 | |||
7d551a8312 | |||
d02eb91b11 |
1
Makefile
1
Makefile
@ -151,6 +151,7 @@ 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)
|
3
go.mod
3
go.mod
@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6
|
||||
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
|
||||
@ -33,6 +33,7 @@ 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/brutella/dnssd v1.2.6 // indirect
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230320130059-dcf93ba651dd // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
|
7
go.sum
7
go.sum
@ -56,6 +56,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/VBx/PzL2riC0zNuQ/cxxf5r3AmEvJE=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6 h1:MxMEBSEvwagUrFORUJ9snZekFIKkaV3OB0EplXra+LU=
|
||||
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||
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=
|
||||
@ -201,6 +203,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=
|
||||
@ -978,6 +982,7 @@ 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=
|
||||
@ -1513,6 +1518,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=
|
||||
@ -1803,6 +1809,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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
"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"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
@ -186,5 +187,6 @@ func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec,
|
||||
},
|
||||
),
|
||||
appModule.ModuleFactory(c.appRepository),
|
||||
fetchModule.ModuleFactory(bus),
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
3
internal/auth/thirdparty/jwt.go
vendored
3
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)
|
||||
|
@ -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"
|
||||
@ -66,6 +67,10 @@ func RunCommand() *cli.Command {
|
||||
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 {
|
||||
|
@ -10,7 +10,7 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
||||
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{
|
||||
{
|
||||
|
@ -20,14 +20,15 @@ const (
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
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"`
|
||||
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,11 +69,12 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
|
||||
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
|
||||
|
||||
type AgentUpdateOptions struct {
|
||||
Label *string
|
||||
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 {
|
||||
@ -104,3 +106,9 @@ func WithAgentUpdateLabel(label string) AgentUpdateOptionFunc {
|
||||
opts.Label = &label
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateContactedAt(contactedAt time.Time) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.ContactedAt = &contactedAt
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +127,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, label, 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 {
|
||||
@ -194,12 +194,16 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
agent := &datastore.Agent{}
|
||||
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
|
||||
if err := rows.Scan(&agent.ID, &agent.Label, &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)
|
||||
}
|
||||
@ -315,7 +319,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
SELECT "id", "label", "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
|
||||
`
|
||||
@ -323,9 +327,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.Label, &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
|
||||
}
|
||||
@ -334,6 +339,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 {
|
||||
@ -362,15 +370,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)
|
||||
@ -401,23 +405,45 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
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", "label", "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.Label, &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
|
||||
}
|
||||
@ -426,6 +452,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 {
|
||||
|
@ -2,6 +2,7 @@ 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/proxy"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
|
||||
|
@ -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
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;
|
38
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
38
misc/spec-samples/mdns.emissary.cadoles.com.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"services": {
|
||||
"arcad": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "arcad",
|
||||
"ifaces": ["wlp4s0"]
|
||||
},
|
||||
"portal": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "portal",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
},
|
||||
"hextris": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "hextris",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
},
|
||||
"test": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "test",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
},
|
||||
"diffusion": {
|
||||
"type": "_http._tcp",
|
||||
"port": 8080,
|
||||
"host": "diffusion",
|
||||
"domain": "arcad.local",
|
||||
"ifaces": ["wlp4s0"]
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,22 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"hostPattern": "*",
|
||||
"target": "http://localhost:8082"
|
||||
|
Reference in New Issue
Block a user