feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
All checks were successful
arcad/edge/pipeline/head This commit looks good
This commit is contained in:
@ -4,9 +4,9 @@
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superadmin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"edge_entrypoint": "edge",
|
||||
"edge_role": "superadmin",
|
||||
"edge_tenant": "dev.cli",
|
||||
"preferred_username": "SuperAdmin",
|
||||
"sub": "superadmin"
|
||||
}
|
||||
@ -16,9 +16,9 @@
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "admin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"edge_entrypoint": "edge",
|
||||
"edge_role": "admin",
|
||||
"edge_tenant": "dev.cli",
|
||||
"preferred_username": "Admin",
|
||||
"sub": "admin"
|
||||
}
|
||||
@ -28,9 +28,9 @@
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superuser",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"edge_entrypoint": "edge",
|
||||
"edge_role": "superuser",
|
||||
"edge_tenant": "dev.cli",
|
||||
"preferred_username": "SuperUser",
|
||||
"sub": "superuser"
|
||||
}
|
||||
@ -40,9 +40,9 @@
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"edge_entrypoint": "edge",
|
||||
"edge_role": "user",
|
||||
"edge_tenant": "dev.cli",
|
||||
"preferred_username": "User",
|
||||
"sub": "user"
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
@ -18,7 +20,7 @@ import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
|
||||
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authModule "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/blob"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
@ -29,7 +31,6 @@ import (
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
@ -48,15 +49,15 @@ func RunCommand() *cli.Command {
|
||||
Name: "run",
|
||||
Usage: "Run the specified app bundle",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "path",
|
||||
Usage: "use `PATH` as app bundle (zipped bundle or directory)",
|
||||
Aliases: []string{"p"},
|
||||
Value: ".",
|
||||
Value: cli.NewStringSlice("."),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "use `ADDRESS` as http server listening address",
|
||||
Usage: "use `ADDRESS` as http server base listening address",
|
||||
Aliases: []string{"a"},
|
||||
Value: ":8080",
|
||||
},
|
||||
@ -83,96 +84,157 @@ func RunCommand() *cli.Command {
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
address := ctx.String("address")
|
||||
path := ctx.String("path")
|
||||
paths := ctx.StringSlice("path")
|
||||
|
||||
logFormat := ctx.String("log-format")
|
||||
logLevel := ctx.Int("log-level")
|
||||
storageFile := ctx.String("storage-file")
|
||||
accountsFile := ctx.String("accounts-file")
|
||||
|
||||
logger.SetFormat(logger.Format(logFormat))
|
||||
logger.SetLevel(logger.Level(logLevel))
|
||||
|
||||
cmdCtx := ctx.Context
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not resolve path '%s'", path)
|
||||
}
|
||||
|
||||
logger.Info(cmdCtx, "opening app bundle", logger.F("path", absPath))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load manifest from app bundle")
|
||||
}
|
||||
|
||||
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
|
||||
return errors.Wrap(err, "invalid app manifest")
|
||||
}
|
||||
|
||||
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
|
||||
|
||||
if err := ensureDir(storageFile); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
db, err := sqlite.Open(storageFile)
|
||||
host, portStr, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
bus := memory.NewBus()
|
||||
|
||||
handler := appHTTP.NewHandler(
|
||||
appHTTP.WithBus(bus),
|
||||
appHTTP.WithServerModules(getServerModules(bus, ds, bs, manifest, address)...),
|
||||
)
|
||||
if err := handler.Load(bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
|
||||
|
||||
accounts, err := loadLocalAccounts(accountsFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load local accounts")
|
||||
}
|
||||
|
||||
// Add auth handler
|
||||
key, err := dummyKey()
|
||||
port, err := strconv.ParseUint(portStr, 10, 32)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(accounts...),
|
||||
))
|
||||
|
||||
// Add app handler
|
||||
router.Handle("/*", handler)
|
||||
manifests := make([]*app.Manifest, len(paths))
|
||||
for idx, pth := range paths {
|
||||
bdl, err := bundle.FromPath(pth)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(cmdCtx, "listening", logger.F("address", address))
|
||||
manifest, err := app.LoadManifest(bdl)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
manifests[idx] = manifest
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for idx, p := range paths {
|
||||
wg.Add(1)
|
||||
|
||||
go func(path string, basePort uint64, appIndex int) {
|
||||
defer wg.Done()
|
||||
|
||||
port := basePort + uint64(appIndex)
|
||||
address := fmt.Sprintf("%s:%d", host, port)
|
||||
appsRepository := newAppRepository(host, basePort, manifests...)
|
||||
|
||||
appCtx := logger.With(cmdCtx, logger.F("address", address))
|
||||
|
||||
if err := runApp(appCtx, path, address, storageFile, accountsFile, appsRepository); err != nil {
|
||||
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}(p, port, idx)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, manifest *app.Manifest, address string) []app.ServerModuleFactory {
|
||||
func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not resolve path '%s'", path)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "opening app bundle", logger.F("path", absPath))
|
||||
|
||||
bundle, err := bundle.FromPath(path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
|
||||
}
|
||||
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load manifest from app bundle")
|
||||
}
|
||||
|
||||
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
|
||||
return errors.Wrap(err, "invalid app manifest")
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("appID", manifest.ID))
|
||||
|
||||
storageFile = injectAppID(storageFile, manifest.ID)
|
||||
|
||||
if err := ensureDir(storageFile); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
db, err := sqlite.Open(storageFile)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
accountsFile = injectAppID(accountsFile, manifest.ID)
|
||||
|
||||
accounts, err := loadLocalAccounts(accountsFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load local accounts")
|
||||
}
|
||||
|
||||
// Add auth handler
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
bus := memory.NewBus()
|
||||
|
||||
handler := appHTTP.NewHandler(
|
||||
appHTTP.WithBus(bus),
|
||||
appHTTP.WithServerModules(getServerModules(bus, ds, bs, appRepository)...),
|
||||
appHTTP.WithHTTPMounts(
|
||||
appModule.Mount(appRepository),
|
||||
authModule.Mount(
|
||||
authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(accounts...),
|
||||
),
|
||||
authModule.WithJWT(dummyKeySet),
|
||||
),
|
||||
),
|
||||
)
|
||||
if err := handler.Load(bundle); err != nil {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
// Add app handler
|
||||
router.Handle("/*", handler)
|
||||
|
||||
logger.Info(ctx, "listening", logger.F("address", address))
|
||||
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, appRepository appModule.Repository) []app.ServerModuleFactory {
|
||||
return []app.ServerModuleFactory{
|
||||
module.ContextModuleFactory(),
|
||||
module.ConsoleModuleFactory(),
|
||||
@ -182,49 +244,10 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
blob.ModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(dummyKeySet),
|
||||
),
|
||||
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"))
|
||||
}
|
||||
},
|
||||
authModule.ModuleFactory(
|
||||
authModule.WithJWT(dummyKeySet),
|
||||
),
|
||||
appModule.ModuleFactory(appModuleMemory.NewRepository(
|
||||
func(ctx context.Context, id app.ID, from string) (string, error) {
|
||||
addr := address
|
||||
if strings.HasPrefix(addr, ":") {
|
||||
addr = "0.0.0.0" + addr
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
addr, err = findMatchingDeviceAddress(ctx, from, host)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s:%s", addr, port), nil
|
||||
},
|
||||
manifest,
|
||||
)),
|
||||
appModule.ModuleFactory(appRepository),
|
||||
fetch.ModuleFactory(bus),
|
||||
}
|
||||
}
|
||||
@ -349,3 +372,25 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
|
||||
|
||||
return defaultAddr, nil
|
||||
}
|
||||
|
||||
func newAppRepository(host string, basePort uint64, manifests ...*app.Manifest) *appModuleMemory.Repository {
|
||||
return appModuleMemory.NewRepository(
|
||||
func(ctx context.Context, id app.ID, from string) (string, error) {
|
||||
appIndex := 0
|
||||
for i := 0; i < len(manifests); i++ {
|
||||
if manifests[i].ID == id {
|
||||
appIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
addr, err := findMatchingDeviceAddress(ctx, from, host)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s:%d", addr, int(basePort)+appIndex), nil
|
||||
},
|
||||
manifests...,
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user