-
Notifications
You must be signed in to change notification settings - Fork 1
fix(server): implement multi-workflow mode API wiring #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0d9ee9c
4873f17
da06770
9dd269c
d6a5b8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "context" | ||
| "database/sql" | ||
| "encoding/json" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "log" | ||
|
|
@@ -14,13 +15,15 @@ import ( | |
| "path/filepath" | ||
| "strings" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/CrisisTextLine/modular" | ||
| "github.com/GoCodeAlone/workflow" | ||
| "github.com/GoCodeAlone/workflow/admin" | ||
| "github.com/GoCodeAlone/workflow/ai" | ||
| copilotai "github.com/GoCodeAlone/workflow/ai/copilot" | ||
| "github.com/GoCodeAlone/workflow/ai/llm" | ||
| apihandler "github.com/GoCodeAlone/workflow/api" | ||
| "github.com/GoCodeAlone/workflow/audit" | ||
| "github.com/GoCodeAlone/workflow/billing" | ||
| "github.com/GoCodeAlone/workflow/bundle" | ||
|
|
@@ -62,6 +65,7 @@ import ( | |
| "github.com/GoCodeAlone/workflow/schema" | ||
| evstore "github.com/GoCodeAlone/workflow/store" | ||
| "github.com/google/uuid" | ||
| "golang.org/x/crypto/bcrypt" | ||
| _ "modernc.org/sqlite" | ||
| ) | ||
|
|
||
|
|
@@ -74,10 +78,11 @@ var ( | |
| anthropicModel = flag.String("anthropic-model", "", "Anthropic model name") | ||
|
|
||
| // Multi-workflow mode flags | ||
| databaseDSN = flag.String("database-dsn", "", "PostgreSQL connection string for multi-workflow mode") | ||
| jwtSecret = flag.String("jwt-secret", "", "JWT signing secret for API authentication") | ||
| adminEmail = flag.String("admin-email", "", "Initial admin user email (first-run bootstrap)") | ||
| adminPassword = flag.String("admin-password", "", "Initial admin user password (first-run bootstrap)") | ||
| databaseDSN = flag.String("database-dsn", "", "PostgreSQL connection string for multi-workflow mode") | ||
| jwtSecret = flag.String("jwt-secret", "", "JWT signing secret for API authentication") | ||
| adminEmail = flag.String("admin-email", "", "Initial admin user email (first-run bootstrap)") | ||
| adminPassword = flag.String("admin-password", "", "Initial admin user password (first-run bootstrap)") | ||
| multiWorkflowAddr = flag.String("multi-workflow-addr", ":8090", "HTTP listen address for multi-workflow REST API") | ||
|
|
||
| // License flags | ||
| licenseKey = flag.String("license-key", "", "License key for the workflow engine (or set WORKFLOW_LICENSE_KEY env var)") | ||
|
|
@@ -1184,43 +1189,121 @@ func main() { | |
| })) | ||
|
|
||
| if *databaseDSN != "" { | ||
| // Multi-workflow mode | ||
| logger.Info("Starting in multi-workflow mode") | ||
|
|
||
| // TODO: Once the api package is implemented, this section will: | ||
| // 1. Connect to PostgreSQL using *databaseDSN | ||
| // 2. Run database migrations | ||
| // 3. Create store instances (UserStore, CompanyStore, ProjectStore, WorkflowStore, etc.) | ||
| // 4. Bootstrap admin user if *adminEmail and *adminPassword are set (first-run) | ||
| // 5. Create WorkflowEngineManager with stores | ||
| // 6. Create api.NewRouter() with stores, *jwtSecret, and engine manager | ||
| // 7. Mount API router at /api/v1/ alongside existing routes | ||
|
|
||
| // For now, log the configuration and fall through to single-config mode | ||
| logger.Info("Multi-workflow mode configured", | ||
| "database_dsn_set", *databaseDSN != "", | ||
| // Multi-workflow mode: connect to PostgreSQL, run migrations, start the | ||
| // REST API router on a dedicated port alongside the single-config engine. | ||
| logger.Info("Starting in multi-workflow mode", | ||
| "database_dsn_set", true, | ||
| "jwt_secret_set", *jwtSecret != "", | ||
| "admin_email_set", *adminEmail != "", | ||
| "api_addr", *multiWorkflowAddr, | ||
| ) | ||
|
|
||
| // Suppress unused variable warnings until api package is ready | ||
| _ = databaseDSN | ||
| _ = jwtSecret | ||
| _ = adminEmail | ||
| _ = adminPassword | ||
| // Validate JWT secret meets minimum security requirements. | ||
| if len(*jwtSecret) < 32 { | ||
| log.Fatalf("multi-workflow mode: --jwt-secret must be at least 32 bytes (got %d)", len(*jwtSecret)) | ||
| } | ||
|
|
||
| pgStore, pgErr := evstore.NewPGStore(context.Background(), evstore.PGConfig{URL: *databaseDSN}) | ||
| if pgErr != nil { | ||
| log.Fatalf("multi-workflow mode: failed to connect to PostgreSQL: %v", pgErr) | ||
| } | ||
| migrator := evstore.NewMigrator(pgStore.Pool()) | ||
| if mErr := migrator.Migrate(context.Background()); mErr != nil { | ||
| log.Fatalf("multi-workflow mode: database migration failed: %v", mErr) | ||
| } | ||
| logger.Info("multi-workflow mode: database migrations applied") | ||
|
|
||
| // Bootstrap admin user on first run. | ||
| if *adminEmail != "" && *adminPassword != "" { | ||
| _, lookupErr := pgStore.Users().GetByEmail(context.Background(), *adminEmail) | ||
| switch { | ||
| case errors.Is(lookupErr, evstore.ErrNotFound): | ||
| hash, hashErr := bcrypt.GenerateFromPassword([]byte(*adminPassword), bcrypt.DefaultCost) | ||
| if hashErr != nil { | ||
| log.Fatalf("multi-workflow mode: failed to hash admin password: %v", hashErr) | ||
| } | ||
| now := time.Now() | ||
| adminUser := &evstore.User{ | ||
| ID: uuid.New(), | ||
| Email: *adminEmail, | ||
| PasswordHash: string(hash), | ||
| DisplayName: "Admin", | ||
| Active: true, | ||
| CreatedAt: now, | ||
| UpdatedAt: now, | ||
| } | ||
| if createErr := pgStore.Users().Create(context.Background(), adminUser); createErr != nil { | ||
| logger.Warn("multi-workflow mode: failed to create admin user (may already exist)", "error", createErr) | ||
| } else { | ||
| logger.Info("multi-workflow mode: created bootstrap admin user", "email", *adminEmail) | ||
| } | ||
| case lookupErr != nil: | ||
| log.Fatalf("multi-workflow mode: failed to look up admin user: %v", lookupErr) | ||
| default: | ||
| logger.Info("multi-workflow mode: admin user already exists, skipping bootstrap", "email", *adminEmail) | ||
| } | ||
| } | ||
|
|
||
| logger.Warn("Multi-workflow mode requires the api package (not yet available); falling back to single-config mode") | ||
| engineBuilder := func(cfg *config.WorkflowConfig, lg *slog.Logger) (*workflow.StdEngine, modular.Application, error) { | ||
| eng, _, _, _, buildErr := buildEngine(cfg, lg) | ||
| if buildErr != nil { | ||
| return nil, nil, buildErr | ||
| } | ||
| app := eng.GetApp() | ||
| return eng, app, nil | ||
| } | ||
| engineMgr := workflow.NewWorkflowEngineManager(pgStore.Workflows(), pgStore.CrossWorkflowLinks(), logger, engineBuilder) | ||
|
|
||
| apiRouter := apihandler.NewRouter(apihandler.Stores{ | ||
| Users: pgStore.Users(), | ||
| Sessions: pgStore.Sessions(), | ||
| Companies: pgStore.Companies(), | ||
| Projects: pgStore.Projects(), | ||
| Workflows: pgStore.Workflows(), | ||
| Memberships: pgStore.Memberships(), | ||
| Links: pgStore.CrossWorkflowLinks(), | ||
| Executions: pgStore.Executions(), | ||
| Logs: pgStore.Logs(), | ||
| Audit: pgStore.Audit(), | ||
| IAM: pgStore.IAM(), | ||
| }, apihandler.Config{ | ||
| JWTSecret: *jwtSecret, | ||
| Orchestrator: engineMgr, | ||
| }) | ||
|
|
||
| apiServer := &http.Server{ //nolint:gosec // ReadHeaderTimeout set below | ||
| Addr: *multiWorkflowAddr, | ||
| Handler: apiRouter, | ||
| ReadHeaderTimeout: 10 * time.Second, | ||
| } | ||
| go func() { | ||
| logger.Info("multi-workflow API listening", "addr", *multiWorkflowAddr) | ||
| if sErr := apiServer.ListenAndServe(); sErr != nil && sErr != http.ErrServerClosed { | ||
| logger.Error("multi-workflow API server error", "error", sErr) | ||
| } | ||
| }() | ||
| defer func() { | ||
| shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer shutdownCancel() | ||
| if sErr := apiServer.Shutdown(shutdownCtx); sErr != nil { | ||
| logger.Warn("multi-workflow API server shutdown error", "error", sErr) | ||
| } | ||
| if sErr := engineMgr.StopAll(shutdownCtx); sErr != nil { | ||
| logger.Warn("multi-workflow engine manager shutdown error", "error", sErr) | ||
| } | ||
| pgStore.Close() | ||
| }() | ||
| } | ||
|
|
||
| // Existing single-config behavior | ||
| // Single-config mode always runs alongside multi-workflow mode (if enabled). | ||
| cfg, err := loadConfig(logger) | ||
| if err != nil { | ||
| log.Fatalf("Configuration error: %v", err) | ||
| log.Fatalf("Configuration error: %v", err) //nolint:gocritic // exitAfterDefer: intentional, cleanup is best-effort | ||
| } | ||
|
|
||
| app, err := setup(logger, cfg) | ||
| if err != nil { | ||
| log.Fatalf("Setup error: %v", err) | ||
| log.Fatalf("Setup error: %v", err) //nolint:gocritic // exitAfterDefer: intentional, cleanup is best-effort | ||
| } | ||
|
Comment on lines
+1285
to
1307
|
||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The admin user bootstrap logic treats any GetByEmail error as "user doesn't exist" and attempts to create the admin user. This means database connection errors, permission errors, or other transient failures will trigger an admin user creation attempt. Check specifically for store.ErrNotFound instead of treating all errors as "user not found".