feat(module,app): iface-based app url resolving
arcad/emissary/pipeline/head There was a failure building this commit
Details
arcad/emissary/pipeline/head There was a failure building this commit
Details
This commit is contained in:
parent
242a247222
commit
2e1ee44e6a
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6
|
forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88
|
||||||
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
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -58,6 +58,8 @@ forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab h1:xOtzLAYOUcKd/
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230328183829-d8ce2901d2ab/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
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 h1:MxMEBSEvwagUrFORUJ9snZekFIKkaV3OB0EplXra+LU=
|
||||||
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
forge.cadoles.com/arcad/edge v0.0.0-20230402160147-f08f645432c6/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
|
||||||
|
forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88 h1:d+AXw/Kx8X7M4GvtPC/mkZWK3tRMT051XORAiQAnClA=
|
||||||
|
forge.cadoles.com/arcad/edge v0.0.0-20230405131922-006f13bc7b88/go.mod h1:Vx4iq/oewXUOkGyi8QKc14clTLNO1sWpb0SjBYELlAs=
|
||||||
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=
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -21,12 +21,13 @@ import (
|
||||||
"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"
|
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=journal_mode(WAL)&_txlock=immediate"
|
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_txlock=immediate"
|
||||||
|
@ -107,46 +108,132 @@ func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
|
||||||
return keySet, nil
|
return keySet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGetAppURL(specs *spec.Spec) GetURLFunc {
|
func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) {
|
||||||
|
rawIfaceMappings := make(map[string]string, 0)
|
||||||
|
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil {
|
||||||
|
rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings))
|
||||||
|
for iface, rawTemplate := range rawIfaceMappings {
|
||||||
|
tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not parse iface '%s' template", iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaceMappings[iface] = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRawTemplate := `http://{{ .DeviceIP }}:{{ .AppPort }}`
|
||||||
|
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.DefaultURLTemplate != "" {
|
||||||
|
defaultRawTemplate = specs.Config.AppURLResolving.DefaultURLTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(defaultRawTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx context.Context, manifest *app.Manifest, from string) (string, error) {
|
||||||
var (
|
var (
|
||||||
compileOnce sync.Once
|
|
||||||
urlTemplate *template.Template
|
urlTemplate *template.Template
|
||||||
err error
|
deviceIP net.IP
|
||||||
)
|
)
|
||||||
|
|
||||||
return func(ctx context.Context, manifest *app.Manifest) (string, error) {
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@ -158,7 +245,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(),
|
||||||
net.ModuleFactory(bus),
|
netModule.ModuleFactory(bus),
|
||||||
module.RPCModuleFactory(bus),
|
module.RPCModuleFactory(bus),
|
||||||
module.StoreModuleFactory(ds),
|
module.StoreModuleFactory(ds),
|
||||||
blob.ModuleFactory(bus, bs),
|
blob.ModuleFactory(bus, bs),
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &spec.Config{
|
||||||
|
AppURLResolving: &spec.AppURLResolving{
|
||||||
|
IfaceMappings: map[string]string{
|
||||||
|
"lo": "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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,10 @@ import (
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetURLFunc func(context.Context, *app.Manifest) (string, error)
|
type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error)
|
||||||
|
|
||||||
type AppRepository struct {
|
type AppRepository struct {
|
||||||
getURL GetURLFunc
|
resolveAppURL ResolveAppURLFunc
|
||||||
bundles []string
|
bundles []string
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
@ -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) (string, error) {
|
func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (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) (string, error) {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := r.getURL(ctx, manifest)
|
url, err := r.resolveAppURL(ctx, manifest, from)
|
||||||
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(getURL GetURLFunc, bundles []string) {
|
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
defer r.mutex.Unlock()
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
r.getURL = getURL
|
r.resolveAppURL = resolveAppURL
|
||||||
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{
|
||||||
getURL: func(ctx context.Context, m *app.Manifest) (string, error) {
|
resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) {
|
||||||
return "", errors.New("unavailable")
|
return "", errors.New("unavailable")
|
||||||
},
|
},
|
||||||
bundles: []string{},
|
bundles: []string{},
|
||||||
|
|
|
@ -96,7 +96,14 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.updateAppRepository(ctx, specs)
|
if err := c.updateAppRepository(ctx, specs); err != nil {
|
||||||
|
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 {
|
||||||
|
@ -109,32 +116,32 @@ func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) {
|
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
getURL := createGetAppURL(specs)
|
resolveAppURL, err := createResolveAppURL(specs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
c.appRepository.Update(getURL, bundles)
|
c.appRepository.Update(resolveAppURL, 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
|
||||||
Auth *spec.Auth
|
Config *spec.Config
|
||||||
}{
|
}{
|
||||||
App: appEntry,
|
App: appEntry,
|
||||||
Auth: auth,
|
Config: specs.Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
|
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
|
||||||
|
@ -164,27 +171,30 @@ func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey str
|
||||||
server = nil
|
server = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if server == nil {
|
newServerEntry := func() (*serverEntry, error) {
|
||||||
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not create handler options")
|
return nil, 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, auth, options...),
|
Server: NewServer(bundle, specs.Config, options...),
|
||||||
AppDefHash: 0,
|
AppDefHash: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.servers[appKey] = server
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
@ -194,6 +204,17 @@ 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",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -31,7 +32,7 @@ type Server struct {
|
||||||
handlerOptions []edgeHTTP.HandlerOptionFunc
|
handlerOptions []edgeHTTP.HandlerOptionFunc
|
||||||
server *http.Server
|
server *http.Server
|
||||||
serverMutex sync.RWMutex
|
serverMutex sync.RWMutex
|
||||||
auth *appSpec.Auth
|
config *appSpec.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||||
|
@ -53,9 +54,20 @@ 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 err := s.configureAuth(router, s.auth); err != nil {
|
if s.config != nil {
|
||||||
|
if s.config.UnexpectedHostRedirect != nil {
|
||||||
|
router.Use(unexpectedHostRedirect(
|
||||||
|
s.config.UnexpectedHostRedirect.HostTarget,
|
||||||
|
s.config.UnexpectedHostRedirect.AcceptedHostPatterns...,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Auth != nil {
|
||||||
|
if err := s.configureAuth(router, s.config.Auth); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.Handle("/*", handler)
|
router.Handle("/*", handler)
|
||||||
|
|
||||||
|
@ -124,13 +136,9 @@ func (s *Server) Stop() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = s.auth.Local.Key
|
var rawKey any = auth.Local.Key
|
||||||
if strKey, ok := rawKey.(string); ok {
|
if strKey, ok := rawKey.(string); ok {
|
||||||
rawKey = []byte(strKey)
|
rawKey = []byte(strKey)
|
||||||
}
|
}
|
||||||
|
@ -141,53 +149,74 @@ func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieDuration := defaultCookieDuration
|
cookieDuration := defaultCookieDuration
|
||||||
if s.auth.Local.CookieDuration != "" {
|
if auth.Local.CookieDuration != "" {
|
||||||
cookieDuration, err = time.ParseDuration(s.auth.Local.CookieDuration)
|
cookieDuration, err = time.ParseDuration(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(s.auth.Local.Accounts...),
|
authHTTP.WithAccounts(auth.Local.Accounts...),
|
||||||
authHTTP.WithCookieOptions(s.auth.Local.CookieDomain, cookieDuration),
|
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(bundle bundle.Bundle, auth *appSpec.Auth, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
|
func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
bundle: bundle,
|
bundle: bundle,
|
||||||
auth: auth,
|
config: config,
|
||||||
handlerOptions: handlerOptions,
|
handlerOptions: handlerOptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidCookieDomainRedirect(cookieDomain string) func(http.Handler) http.Handler {
|
func getCookieDomain(r *http.Request) (string, error) {
|
||||||
domain := strings.TrimPrefix(cookieDomain, ".")
|
host, _, err := net.SplitHostPort(r.Host)
|
||||||
hostPattern := "*" + domain
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
return func(h http.Handler) http.Handler {
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
hostParts := strings.SplitN(r.Host, ":", 2)
|
|
||||||
|
|
||||||
if !wildcard.Match(hostParts[0], hostPattern) {
|
|
||||||
url := r.URL
|
|
||||||
|
|
||||||
newHost := domain
|
|
||||||
if len(hostParts) > 1 {
|
|
||||||
newHost += ":" + hostParts[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url.Host = newHost
|
// If host is an IP address
|
||||||
|
if wildcard.Match(host, "*.*.*.*") {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If host is an domain, return top level domain
|
||||||
|
domainParts := strings.Split(host, ".")
|
||||||
|
if len(domainParts) >= 2 {
|
||||||
|
topLevelDomain := strings.Join(domainParts[len(domainParts)-2:], ".")
|
||||||
|
return topLevelDomain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, return host
|
||||||
|
return host, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unexpectedHostRedirect(hostTarget string, acceptedHostPatterns ...string) func(http.Handler) http.Handler {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
host, port, err := net.SplitHostPort(r.Host)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(r.Context(), "could not split host/port", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
url := r.URL
|
||||||
|
|
||||||
|
url.Host = hostTarget
|
||||||
|
if port != "" {
|
||||||
|
url.Host += ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"appUrlResolving": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ifaceMappings": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
".*": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultUrlTemplate": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["defaultUrlTemplate"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"unexpectedHostRedirect": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"acceptedHostPatterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hostTarget": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["acceptedHostPatterns", "hostTarget"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -81,14 +118,14 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"key"
|
"key"
|
||||||
]
|
],
|
||||||
}
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"additionalProperties": false
|
||||||
"appUrlTemplate": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
@ -33,7 +33,18 @@ type LocalAuth struct {
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Auth *Auth `json:"auth"`
|
Auth *Auth `json:"auth"`
|
||||||
AppURLTemplate string `json:"appUrlTemplate"`
|
UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"`
|
||||||
|
AppURLResolving *AppURLResolving `json:"appUrlResolving"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnexpectedHostRedirect struct {
|
||||||
|
AcceptedHostPatterns []string `json:"acceptedHostPatterns"`
|
||||||
|
HostTarget string `json:"hostTarget"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppURLResolving struct {
|
||||||
|
IfaceMappings map[string]string `json:"ifaceMappings"`
|
||||||
|
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecName() spec.Name {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"local": {
|
"local": {
|
||||||
"key": {
|
"key": {
|
||||||
|
@ -36,6 +37,17 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"unexpectedHostRedirect": {
|
||||||
|
"acceptedHostPatterns": ["arcad.local", "*.arcad.local", "arcad-*.local", "*.*.*.*"],
|
||||||
|
"hostTarget": "arcad.local"
|
||||||
|
},
|
||||||
|
"appUrlResolving": {
|
||||||
|
"ifaceMappings": {
|
||||||
|
"eth0": "http://{{ .DeviceIP }}:{{ .AppHost }}"
|
||||||
|
},
|
||||||
|
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"revision": 0
|
"revision": 0
|
||||||
|
|
|
@ -1,36 +1,46 @@
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"portal": {
|
"edge.portal": {
|
||||||
"url": "https://emissary.cadol.es/files/apps/arcad.portal_v2023.3.28-3feda80.zip",
|
"url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.5-45546c4.zip",
|
||||||
"sha256sum": "921402c44a5fa554d5b630d1284957b05416aa6872b402314cf52e964e06fac5",
|
"sha256sum": "c83e7e4b3785f5f4d3fcae7cad334819626015b11b446520aa79f42176a2744d",
|
||||||
"address": "127.0.0.1:8082",
|
"address": ":8082",
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
},
|
},
|
||||||
"hextris": {
|
"app.arcad.edge.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": "127.0.0.1:8083",
|
"address": ":8083",
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
},
|
},
|
||||||
"test": {
|
"edge.sdk.client.test": {
|
||||||
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.3.24-ed535b6.zip",
|
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.2-f08f645.zip",
|
||||||
"sha256sum": "e97b7b79159bb5d6a13b05644c091272b02a1a3cbb1b613dd5eda37e1eb84623",
|
"sha256sum": "8b48388c817802ebeb38907b3a42f1189dc0759f94c5f33de4546c1a7ebfc784",
|
||||||
"address": "127.0.0.1:8084",
|
"address": ":8084",
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
},
|
},
|
||||||
"diffusion": {
|
"arcad.diffusion": {
|
||||||
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.3.29-5b3fab4.zip",
|
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.5-ffcd1c7.zip",
|
||||||
"sha256sum": "1282e75719beedbc7c7e67879389d0f3e11c86d3d2c37cf13da624a66faaeb58",
|
"sha256sum": "a51a961212470ce1de4527aaaec9e8e0286a978ec675ff9df29b2029daf05a55",
|
||||||
"address": "127.0.0.1:8085",
|
"address": ":8085",
|
||||||
"format": "zip"
|
"format": "zip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"appUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local:8080",
|
"appUrlResolving": {
|
||||||
|
"ifaceMappings": {
|
||||||
|
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||||
|
"wlp4s0": "http://{{ .DeviceIP }}:{{ .AppPort }}",
|
||||||
|
"enp0s31f6": "http://{{ .DeviceIP }}:{{ .AppPort }}"
|
||||||
|
},
|
||||||
|
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.localhost.arcad.lan:8080"
|
||||||
|
},
|
||||||
|
"unexpectedHostRedirect": {
|
||||||
|
"acceptedHostPatterns": ["arcad.lan", "*.arcad.lan", "arcad-*.local", "*.*.*.*"],
|
||||||
|
"hostTarget": "localhost.arcad.lan"
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"local": {
|
"local": {
|
||||||
"key": "absolutlynotsecret",
|
"key": "absolutlynotsecret",
|
||||||
"cookieDomain": ".arcad.local",
|
|
||||||
"cookieDuration": "1h",
|
"cookieDuration": "1h",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,36 +3,27 @@
|
||||||
"arcad": {
|
"arcad": {
|
||||||
"type": "_http._tcp",
|
"type": "_http._tcp",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "arcad",
|
"host": "arcad"
|
||||||
"ifaces": ["wlp4s0"]
|
|
||||||
},
|
},
|
||||||
"portal": {
|
"portal": {
|
||||||
"type": "_http._tcp",
|
"type": "_http._tcp",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "portal",
|
"host": "arcad-portal"
|
||||||
"domain": "arcad.local",
|
|
||||||
"ifaces": ["wlp4s0"]
|
|
||||||
},
|
},
|
||||||
"hextris": {
|
"hextris": {
|
||||||
"type": "_http._tcp",
|
"type": "_http._tcp",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "hextris",
|
"host": "arcad-hextris"
|
||||||
"domain": "arcad.local",
|
|
||||||
"ifaces": ["wlp4s0"]
|
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"type": "_http._tcp",
|
"type": "_http._tcp",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "test",
|
"host": "arcad-test"
|
||||||
"domain": "arcad.local",
|
|
||||||
"ifaces": ["wlp4s0"]
|
|
||||||
},
|
},
|
||||||
"diffusion": {
|
"diffusion": {
|
||||||
"type": "_http._tcp",
|
"type": "_http._tcp",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "diffusion",
|
"host": "arcad-diffusion"
|
||||||
"domain": "arcad.local",
|
|
||||||
"ifaces": ["wlp4s0"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,19 +4,19 @@
|
||||||
"address": ":8080",
|
"address": ":8080",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"hostPattern": "portal.arcad.local:*",
|
"hostPattern": "portal.localhost.arcad.lan:*",
|
||||||
"target": "http://localhost:8082"
|
"target": "http://localhost:8082"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostPattern": "hextris.arcad.local:*",
|
"hostPattern": "hextris.localhost.arcad.lan:*",
|
||||||
"target": "http://localhost:8083"
|
"target": "http://localhost:8083"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostPattern": "test.arcad.local:*",
|
"hostPattern": "test.localhost.arcad.lan:*",
|
||||||
"target": "http://localhost:8084"
|
"target": "http://localhost:8084"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostPattern": "diffusion.arcad.local:*",
|
"hostPattern": "diffusion.localhost.arcad.lan:*",
|
||||||
"target": "http://localhost:8085"
|
"target": "http://localhost:8085"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue