feat(controller,app): add app module
This commit is contained in:
190
internal/agent/controller/app/app_handler.go
Normal file
190
internal/agent/controller/app/app_handler.go
Normal file
@ -0,0 +1,190 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
dbFile := filepath.Join(dataDir, appKey+".sqlite")
|
||||
db, err := sqlite.Open(dbFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
keySet, err := getAuthKeySet(specs.Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve auth key set")
|
||||
}
|
||||
|
||||
bundles := make([]string, 0, len(specs.Apps))
|
||||
for appKey, app := range specs.Apps {
|
||||
path := c.getAppBundlePath(appKey, app.Format)
|
||||
bundles = append(bundles, path)
|
||||
}
|
||||
|
||||
getAppURL := createGetAppURL(specs)
|
||||
|
||||
bus := memory.NewBus()
|
||||
modules := getAppModules(bus, db, specs, keySet, getAppURL, bundles)
|
||||
|
||||
options := []edgeHTTP.HandlerOptionFunc{
|
||||
edgeHTTP.WithBus(bus),
|
||||
edgeHTTP.WithServerModules(modules...),
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
|
||||
keySet := jwk.NewSet()
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
auth := config.Auth
|
||||
|
||||
if auth == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var (
|
||||
key jwk.Key
|
||||
err error
|
||||
)
|
||||
|
||||
switch typedKey := auth.Local.Key.(type) {
|
||||
case string:
|
||||
key, err = jwk.FromRaw([]byte(typedKey))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse local auth key")
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected key type '%T'", auth.Local.Key)
|
||||
}
|
||||
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return keySet, nil
|
||||
}
|
||||
|
||||
func createGetAppURL(specs *spec.Spec) GetURLFunc {
|
||||
var (
|
||||
compileOnce sync.Once
|
||||
urlTemplate *template.Template
|
||||
err error
|
||||
)
|
||||
|
||||
return func(ctx context.Context, manifest *app.Manifest) (string, error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
Manifest *app.Manifest
|
||||
Specs *spec.Spec
|
||||
}{
|
||||
Manifest: manifest,
|
||||
Specs: specs,
|
||||
}
|
||||
|
||||
if err := urlTemplate.Execute(&buf, data); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set, getAppURL GetURLFunc, bundles []string) []app.ServerModuleFactory {
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
blob.ModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(func() (jwk.Set, error) {
|
||||
return keySet, nil
|
||||
}),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
},
|
||||
),
|
||||
appModule.ModuleFactory(NewAppRepository(getAppURL, bundles...)),
|
||||
}
|
||||
}
|
104
internal/agent/controller/app/app_repository.go
Normal file
104
internal/agent/controller/app/app_repository.go
Normal file
@ -0,0 +1,104 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type GetURLFunc func(context.Context, *app.Manifest) (string, error)
|
||||
|
||||
type AppRepository struct {
|
||||
getURL GetURLFunc
|
||||
bundles []string
|
||||
}
|
||||
|
||||
// Get implements app.Repository
|
||||
func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetURL implements app.Repository
|
||||
func (r *AppRepository) GetURL(ctx context.Context, id app.ID) (string, error) {
|
||||
manifest, err := r.findManifest(ctx, id)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
url, err := r.getURL(ctx, manifest)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// List implements app.Repository
|
||||
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||
manifests := make([]*app.Manifest, 0)
|
||||
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
|
||||
for _, path := range r.bundles {
|
||||
bundleCtx := logger.With(ctx, logger.F("path", path))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if manifest.ID != id {
|
||||
continue
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(appModule.ErrNotFound)
|
||||
}
|
||||
|
||||
func NewAppRepository(getURL GetURLFunc, bundles ...string) *AppRepository {
|
||||
return &AppRepository{getURL, bundles}
|
||||
}
|
||||
|
||||
var _ appModule.Repository = &AppRepository{}
|
@ -8,9 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -35,9 +34,9 @@ func (c *Controller) Name() string {
|
||||
|
||||
// Reconcile implements node.Controller.
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
appSpec := app.NewSpec()
|
||||
appSpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(app.NameApp, appSpec); err != nil {
|
||||
if err := state.GetSpec(spec.Name, appSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find app spec")
|
||||
|
||||
@ -56,7 +55,7 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *spec.Spec) {
|
||||
if len(c.servers) > 0 {
|
||||
logger.Info(ctx, "stopping all apps")
|
||||
}
|
||||
@ -76,122 +75,121 @@ func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
|
||||
// Stop and remove obsolete apps
|
||||
for appID, entry := range c.servers {
|
||||
if _, exists := spec.Apps[appID]; exists {
|
||||
for appKey, server := range c.servers {
|
||||
if _, exists := specs.Apps[appKey]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info(ctx, "stopping app", logger.F("appID", appID))
|
||||
logger.Info(ctx, "stopping app", logger.F("appKey", appKey))
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "error while stopping app",
|
||||
logger.F("gatewayID", appID),
|
||||
logger.F("appKey", appKey),
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
delete(c.servers, appID)
|
||||
delete(c.servers, appKey)
|
||||
}
|
||||
}
|
||||
|
||||
// (Re)start apps
|
||||
for appID, appSpec := range spec.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appID", appID))
|
||||
for appKey := range specs.Apps {
|
||||
appCtx := logger.With(ctx, logger.F("appKey", appKey))
|
||||
|
||||
if err := c.updateApp(ctx, appID, appSpec, spec.Auth); err != nil {
|
||||
if err := c.updateApp(ctx, specs, appKey); err != nil {
|
||||
logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err)))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry, auth *app.Auth) (err error) {
|
||||
newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil)
|
||||
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
|
||||
appEntry := specs.Apps[appKey]
|
||||
|
||||
newAppSpecHash, err := hashstructure.Hash(appEntry, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec)
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appKey, appEntry)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
}
|
||||
|
||||
dataDir, err := c.ensureAppDataDir(ctx, appID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
var entry *serverEntry
|
||||
|
||||
entry, exists := c.servers[appID]
|
||||
server, exists := c.servers[appKey]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appSpec.SHA256Sum {
|
||||
} else if sha256sum != appEntry.SHA256Sum {
|
||||
logger.Info(
|
||||
ctx, "bundle hash mismatch, stopping app",
|
||||
logger.F("currentHash", sha256sum),
|
||||
logger.F("specHash", appSpec.SHA256Sum),
|
||||
logger.F("specHash", appEntry.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
if err := server.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
entry = nil
|
||||
server = nil
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
dbFile := filepath.Join(dataDir, appID+".sqlite")
|
||||
db, err := sqlite.Open(dbFile)
|
||||
if server == nil {
|
||||
options, err := c.getHandlerOptions(ctx, appKey, specs)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not opend database file '%s'", dbFile)
|
||||
return errors.Wrap(err, "could not create handler options")
|
||||
}
|
||||
|
||||
entry = &serverEntry{
|
||||
Server: NewServer(bundle, db, auth),
|
||||
var auth *spec.Auth
|
||||
if specs.Config != nil {
|
||||
auth = specs.Config.Auth
|
||||
}
|
||||
|
||||
server = &serverEntry{
|
||||
Server: NewServer(bundle, auth, options...),
|
||||
SpecHash: 0,
|
||||
}
|
||||
|
||||
c.servers[appID] = entry
|
||||
c.servers[appKey] = server
|
||||
}
|
||||
|
||||
specChanged := newAppSpecHash != entry.SpecHash
|
||||
specChanged := newAppSpecHash != server.SpecHash
|
||||
|
||||
if entry.Server.Running() && !specChanged {
|
||||
if server.Server.Running() && !specChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if specChanged && entry.SpecHash != 0 {
|
||||
if specChanged && server.SpecHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
logger.F("address", appEntry.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := entry.Server.Start(ctx, appSpec.Address); err != nil {
|
||||
delete(c.servers, appID)
|
||||
if err := server.Server.Start(ctx, appEntry.Address); err != nil {
|
||||
delete(c.servers, appKey)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
entry.SpecHash = newAppSpecHash
|
||||
server.SpecHash = newAppSpecHash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app.AppEntry) (bundle.Bundle, string, error) {
|
||||
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec spec.AppEntry) (bundle.Bundle, string, error) {
|
||||
if err := os.MkdirAll(c.downloadDir, os.ModePerm); err != nil {
|
||||
return nil, "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundlePath := filepath.Join(c.downloadDir, appID+"."+spec.Format)
|
||||
bundlePath := c.getAppBundlePath(appID, spec.Format)
|
||||
|
||||
_, err := os.Stat(bundlePath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
@ -285,6 +283,10 @@ func (c *Controller) ensureAppDataDir(ctx context.Context, appID string) (string
|
||||
return dataDir, nil
|
||||
}
|
||||
|
||||
func (c *Controller) getAppBundlePath(appKey string, format string) string {
|
||||
return filepath.Join(c.downloadDir, appKey+"."+format)
|
||||
}
|
||||
|
||||
func NewController(funcs ...OptionFunc) *Controller {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
|
@ -2,26 +2,17 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
@ -31,13 +22,14 @@ import (
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/passwd"
|
||||
)
|
||||
|
||||
const defaultCookieDuration time.Duration = 24 * time.Hour
|
||||
|
||||
type Server struct {
|
||||
bundle bundle.Bundle
|
||||
db *sql.DB
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
auth *appSpec.Auth
|
||||
keySet jwk.Set
|
||||
bundle bundle.Bundle
|
||||
handlerOptions []edgeHTTP.HandlerOptionFunc
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
auth *appSpec.Auth
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
@ -51,47 +43,13 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
bus := memory.NewBus()
|
||||
ds := sqlite.NewDocumentStoreWithDB(s.db)
|
||||
bs := sqlite.NewBlobStoreWithDB(s.db)
|
||||
|
||||
handler := edgeHTTP.NewHandler(
|
||||
edgeHTTP.WithBus(bus),
|
||||
edgeHTTP.WithServerModules(s.getAppModules(bus, ds, bs)...),
|
||||
)
|
||||
handler := edgeHTTP.NewHandler(s.handlerOptions...)
|
||||
if err := handler.Load(s.bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
if s.auth != nil {
|
||||
if s.auth.Local != nil {
|
||||
var rawKey any = s.auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keySet := jwk.NewSet()
|
||||
if err := keySet.AddKey(key); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.keySet = keySet
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(s.auth.Local.Accounts...),
|
||||
))
|
||||
}
|
||||
if err := s.configureAuth(router, s.auth); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
router.Handle("/*", handler)
|
||||
@ -157,49 +115,46 @@ func (s *Server) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
cast.CastModuleFactory(),
|
||||
module.LifecycleModuleFactory(),
|
||||
net.ModuleFactory(bus),
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(s.getJWTKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
},
|
||||
),
|
||||
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case auth.Local != nil:
|
||||
var rawKey any = s.auth.Local.Key
|
||||
if strKey, ok := rawKey.(string); ok {
|
||||
rawKey = []byte(strKey)
|
||||
}
|
||||
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
cookieDuration := defaultCookieDuration
|
||||
if s.auth.Local.CookieDuration != "" {
|
||||
cookieDuration, err = time.ParseDuration(s.auth.Local.CookieDuration)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(s.auth.Local.Accounts...),
|
||||
authHTTP.WithCookieOptions(s.auth.Local.CookieDomain, cookieDuration),
|
||||
))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getJWTKeySet() (jwk.Set, error) {
|
||||
return s.keySet, nil
|
||||
}
|
||||
|
||||
func NewServer(bundle bundle.Bundle, db *sql.DB, auth *appSpec.Auth) *Server {
|
||||
func NewServer(bundle bundle.Bundle, auth *appSpec.Auth, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
|
||||
return &Server{
|
||||
bundle: bundle,
|
||||
db: db,
|
||||
auth: auth,
|
||||
bundle: bundle,
|
||||
auth: auth,
|
||||
handlerOptions: handlerOptions,
|
||||
}
|
||||
}
|
||||
|
17
internal/agent/controller/app/spec/init.go
Normal file
17
internal/agent/controller/app/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))
|
||||
}
|
||||
}
|
98
internal/agent/controller/app/spec/schema.json
Normal file
98
internal/agent/controller/app/spec/schema.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256sum": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"zip",
|
||||
"tar.gz"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"address",
|
||||
"format"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
},
|
||||
"accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"claims": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"password",
|
||||
"algo"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cookieDomain": {
|
||||
"type": "string"
|
||||
},
|
||||
"cookieDuration": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"appUrlTemplate": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apps"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
60
internal/agent/controller/app/spec/spec.go
Normal file
60
internal/agent/controller/app/spec/spec.go
Normal file
@ -0,0 +1,60 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
)
|
||||
|
||||
const Name spec.Name = "app.emissary.cadoles.com"
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
Apps map[string]AppEntry `json:"apps"`
|
||||
Config *Config `json:"config"`
|
||||
}
|
||||
|
||||
type AppEntry struct {
|
||||
URL string `json:"url"`
|
||||
SHA256Sum string `json:"sha256sum"`
|
||||
Address string `json:"address"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Local *LocalAuth `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
type LocalAuth struct {
|
||||
Key any `json:"key"`
|
||||
Accounts []edgeAuth.LocalAccount `json:"accounts"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
CookieDuration string `json:"cookieDuration"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth *Auth `json:"auth"`
|
||||
AppURLTemplate string `json:"appUrlTemplate"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
||||
func (s *Spec) SpecData() map[string]any {
|
||||
return map[string]any{
|
||||
"apps": s.Apps,
|
||||
"config": s.Config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
42
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
42
internal/agent/controller/app/spec/testdata/spec-ok.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "app.emissary.cadoles.com",
|
||||
"data": {
|
||||
"apps": {
|
||||
"edge.sdk.client.test": {
|
||||
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
|
||||
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
|
||||
"address": ":8081",
|
||||
"format": "zip"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"local": {
|
||||
"key": {
|
||||
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
|
||||
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
|
||||
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
|
||||
"e": "AQAB",
|
||||
"kty": "RSA",
|
||||
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
|
||||
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
|
||||
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
|
||||
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"username": "foo",
|
||||
"algo": "plain",
|
||||
"password": "bar",
|
||||
"claims": {
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Foo",
|
||||
"sub": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": 0
|
||||
}
|
65
internal/agent/controller/app/spec/validator_test.go
Normal file
65
internal/agent/controller/app/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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user