feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good

This commit is contained in:
2023-04-18 17:57:16 +02:00
parent 9e3fc427bb
commit b5b4042cc7
59 changed files with 22276 additions and 486 deletions

View File

@ -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"
}

View File

@ -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...,
)
}