254 lines
11 KiB
Go
254 lines
11 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/server"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/fedwiki"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"go.temporal.io/sdk/client"
|
|
)
|
|
|
|
var startCmd = &cobra.Command{
|
|
Use: "start",
|
|
Short: "Start serving the member-console web application",
|
|
Long: `The start command starts an HTTP server that serves the member-console web
|
|
application from the components directory in the current directory.
|
|
|
|
The server listens on port 8080 by default, unless a different port is specified using the --port flag.`,
|
|
Args: cobra.NoArgs,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Create base context for the application
|
|
ctx := context.Background()
|
|
|
|
// Set up structured logging
|
|
env := viper.GetString("env")
|
|
logger := logging.SetupLogger(env)
|
|
|
|
// Store logger in context
|
|
ctx = logging.WithContext(ctx, logger)
|
|
|
|
// Database Setup
|
|
dbDSN := viper.GetString("db-dsn")
|
|
dbConfig := db.DefaultDBConfig(dbDSN)
|
|
database, err := db.ConnectAndMigrate(ctx, logger, dbConfig)
|
|
if err != nil {
|
|
logger.Error("failed to initialize database", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
defer database.Close()
|
|
|
|
// You'll pass 'database' (or your sqlc Querier) to your server/handlers.
|
|
// For example, by adding it to the server.Config
|
|
// Validate and load configurations from files if specified
|
|
configPairs := []struct {
|
|
value string
|
|
file string
|
|
configKey string
|
|
}{
|
|
{viper.GetString("oidc-sp-client-secret"), viper.GetString("oidc-sp-client-secret-file"), "oidc-sp-client-secret"},
|
|
{viper.GetString("session-secret"), viper.GetString("session-secret-file"), "session-secret"},
|
|
{viper.GetString("csrf-secret"), viper.GetString("csrf-secret-file"), "csrf-secret"},
|
|
{viper.GetString("fedwiki-admin-token"), viper.GetString("fedwiki-admin-token-file"), "fedwiki-admin-token"},
|
|
{viper.GetString("temporal-oauth-client-secret"), viper.GetString("temporal-oauth-client-secret-file"), "temporal-oauth-client-secret"},
|
|
}
|
|
|
|
// Check for conflicts between direct values and file paths
|
|
for _, pair := range configPairs {
|
|
// Check if both a direct value and a file path are provided
|
|
if pair.value != "" && pair.file != "" {
|
|
logger.Error("configuration error",
|
|
slog.String("config", pair.configKey),
|
|
slog.String("error", "both direct value and file path provided; use only one"))
|
|
return
|
|
}
|
|
|
|
// If a file path is provided, load the value from the file
|
|
if pair.file != "" {
|
|
value, err := loadFromFile(pair.file)
|
|
if err != nil {
|
|
logger.Error("failed to load configuration from file",
|
|
slog.String("config", pair.configKey),
|
|
slog.String("file", pair.file),
|
|
slog.Any("error", err))
|
|
return
|
|
}
|
|
viper.Set(pair.configKey, value)
|
|
}
|
|
}
|
|
|
|
// Retrieve the configuration values from Viper
|
|
port := viper.GetString("port")
|
|
csrfSecret := viper.GetString("csrf-secret")
|
|
|
|
// Create Temporal client if configured
|
|
var temporalClient client.Client
|
|
temporalHost := viper.GetString("temporal-host")
|
|
if temporalHost != "" {
|
|
temporalOAuthTokenURL := viper.GetString("temporal-oauth-token-url")
|
|
temporalOAuthClientID := viper.GetString("temporal-oauth-client-id")
|
|
temporalOAuthClientSecret := viper.GetString("temporal-oauth-client-secret")
|
|
temporalOAuthScopes := viper.GetStringSlice("temporal-oauth-scopes")
|
|
|
|
clientCfg := workflows.ClientConfig{
|
|
HostPort: temporalHost,
|
|
Namespace: viper.GetString("temporal-namespace"),
|
|
Logger: logger,
|
|
}
|
|
if temporalOAuthTokenURL != "" || temporalOAuthClientID != "" || temporalOAuthClientSecret != "" || len(temporalOAuthScopes) > 0 {
|
|
clientCfg.OAuthTokenProvider = &workflows.OAuthTokenProviderConfig{
|
|
TokenURL: temporalOAuthTokenURL,
|
|
ClientID: temporalOAuthClientID,
|
|
ClientSecret: temporalOAuthClientSecret,
|
|
Scopes: temporalOAuthScopes,
|
|
}
|
|
}
|
|
var err error
|
|
temporalClient, err = workflows.NewClient(ctx, clientCfg)
|
|
if err != nil {
|
|
logger.Error("failed to connect to Temporal", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
defer temporalClient.Close()
|
|
|
|
// Start Temporal worker
|
|
workerCfg := workflows.DefaultWorkerConfig(db.New(database), logger)
|
|
workerCfg.FedWikiFarmAPIURL = viper.GetString("fedwiki-farm-api-url")
|
|
workerCfg.FedWikiAllowedDomains = viper.GetStringSlice("fedwiki-allowed-domains")
|
|
workerCfg.FedWikiAdminToken = viper.GetString("fedwiki-admin-token")
|
|
workerCfg.SupportURL = viper.GetString("support-url")
|
|
|
|
worker, err := workflows.NewWorker(temporalClient, workerCfg)
|
|
if err != nil {
|
|
logger.Error("failed to create Temporal worker", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
if err := worker.Start(); err != nil {
|
|
logger.Error("failed to start Temporal worker", slog.Any("error", err))
|
|
os.Exit(1)
|
|
}
|
|
defer worker.Stop()
|
|
|
|
// Set up FedWiki site sync schedule if enabled
|
|
if viper.GetBool("fedwiki-sync-enabled") {
|
|
syncInterval := viper.GetDuration("fedwiki-sync-interval")
|
|
if syncInterval == 0 {
|
|
syncInterval = fedwiki.DefaultSyncInterval
|
|
}
|
|
scheduleManager := fedwiki.NewScheduleManager(temporalClient, logger)
|
|
err := scheduleManager.EnsureSyncSchedule(ctx, fedwiki.ScheduleConfig{
|
|
Interval: syncInterval,
|
|
DefaultUserID: viper.GetInt64("fedwiki-sync-default-user-id"),
|
|
TriggerImmediately: viper.GetBool("fedwiki-sync-trigger-immediately"),
|
|
})
|
|
if err != nil {
|
|
logger.Error("failed to set up FedWiki sync schedule", slog.Any("error", err))
|
|
// Non-fatal: continue without sync schedule
|
|
}
|
|
}
|
|
} else {
|
|
logger.Warn("Temporal not configured - FedWiki site provisioning will be unavailable")
|
|
}
|
|
|
|
// Create server config
|
|
serverConfig := server.Config{
|
|
Port: port,
|
|
Env: env,
|
|
CSRFSecret: csrfSecret,
|
|
Logger: logger,
|
|
DB: db.New(database), // Pass the sqlc Querier
|
|
FedWikiFarmAPIURL: viper.GetString("fedwiki-farm-api-url"),
|
|
FedWikiAllowedDomains: viper.GetStringSlice("fedwiki-allowed-domains"),
|
|
FedWikiSiteScheme: viper.GetString("fedwiki-site-scheme"),
|
|
FedWikiAdminToken: viper.GetString("fedwiki-admin-token"),
|
|
SupportURL: viper.GetString("support-url"),
|
|
TemporalClient: temporalClient,
|
|
}
|
|
|
|
// Start the server
|
|
if err := server.Start(ctx, serverConfig); err != nil {
|
|
logger.Error("server failed to start", slog.Any("error", err))
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
// Register flags with Cobra
|
|
// DO NOT SET DEFAULT VALUES HERE. Use viper.SetDefault() instead. https://github.com/spf13/viper/issues/671
|
|
|
|
// General configuration
|
|
startCmd.Flags().StringP("port", "p", "", "Port to listen on")
|
|
startCmd.Flags().String("base-url", "", "Address at which the server is exposed")
|
|
startCmd.Flags().String("env", "", "Environment (development/production)")
|
|
startCmd.Flags().String("db-dsn", "", "Database DSN (e.g., ./member_console.db or file:/path/to/data.db?_foreign_keys=on)")
|
|
startCmd.Flags().String("session-secret", "", "Secret key for session management (must be exactly 32 bytes)")
|
|
startCmd.Flags().String("session-secret-file", "", "Path to file containing session secret key")
|
|
startCmd.Flags().String("csrf-secret", "", "Secret key for CSRF protection (must be exactly 32 bytes)")
|
|
startCmd.Flags().String("csrf-secret-file", "", "Path to file containing CSRF secret key")
|
|
|
|
// OIDC configuration
|
|
startCmd.Flags().String("oidc-sp-client-id", "", "OIDC Client ID")
|
|
startCmd.Flags().String("oidc-idp-issuer-url", "", "OIDC Identity Provider Issuer URL")
|
|
startCmd.Flags().String("oidc-sp-client-secret", "", "OIDC Client Secret")
|
|
startCmd.Flags().String("oidc-sp-client-secret-file", "", "Path to file containing OIDC Client Secret")
|
|
|
|
// FedWiki configuration
|
|
startCmd.Flags().String("fedwiki-farm-api-url", "", "URL of the FedWiki farm API endpoint (e.g., https://admin.wiki.example.com)")
|
|
startCmd.Flags().StringSlice("fedwiki-allowed-domains", nil, "Domains where users can create FedWiki sites (e.g., wiki.example.com,wiki2.example.com)")
|
|
startCmd.Flags().String("fedwiki-site-scheme", "", "URL scheme for FedWiki sites (http or https)")
|
|
startCmd.Flags().String("fedwiki-admin-token", "", "FedWiki admin access token for farmmanager API")
|
|
startCmd.Flags().String("fedwiki-admin-token-file", "", "Path to file containing FedWiki admin access token")
|
|
|
|
// FedWiki sync configuration
|
|
startCmd.Flags().Bool("fedwiki-sync-enabled", false, "Enable periodic sync of sites from FedWiki farm")
|
|
startCmd.Flags().Duration("fedwiki-sync-interval", time.Hour, "Interval between FedWiki site syncs")
|
|
startCmd.Flags().Int64("fedwiki-sync-default-user-id", 1, "Default user ID for orphaned sites during sync")
|
|
startCmd.Flags().Bool("fedwiki-sync-trigger-immediately", true, "Run sync immediately on startup")
|
|
|
|
// Support configuration
|
|
startCmd.Flags().String("support-url", "", "URL for users to get support (shown in error messages)")
|
|
|
|
// Temporal configuration
|
|
startCmd.Flags().String("temporal-host", "", "Temporal server host:port (e.g., localhost:7233)")
|
|
startCmd.Flags().String("temporal-namespace", "", "Temporal namespace")
|
|
startCmd.Flags().String("temporal-oauth-token-url", "", "OAuth2 token endpoint for Temporal authentication")
|
|
startCmd.Flags().String("temporal-oauth-client-id", "", "OAuth2 client ID for Temporal authentication")
|
|
startCmd.Flags().String("temporal-oauth-client-secret", "", "OAuth2 client secret for Temporal authentication")
|
|
startCmd.Flags().String("temporal-oauth-client-secret-file", "", "Path to file containing Temporal OAuth2 client secret")
|
|
startCmd.Flags().StringSlice("temporal-oauth-scopes", nil, "OAuth2 scopes for Temporal authentication (comma-separated)")
|
|
|
|
// Bind all flags to Viper
|
|
viper.BindPFlags(startCmd.Flags())
|
|
|
|
// Set default values
|
|
viper.SetDefault("port", "8080")
|
|
viper.SetDefault("env", "development")
|
|
viper.SetDefault("db-dsn", "./member_console.db")
|
|
viper.SetDefault("temporal-namespace", "default")
|
|
viper.SetDefault("fedwiki-site-scheme", "https")
|
|
viper.SetDefault("fedwiki-sync-enabled", false)
|
|
viper.SetDefault("fedwiki-sync-interval", time.Hour)
|
|
viper.SetDefault("fedwiki-sync-default-user-id", 1)
|
|
viper.SetDefault("fedwiki-sync-trigger-immediately", true)
|
|
|
|
// Add the command to the root command
|
|
rootCmd.AddCommand(startCmd)
|
|
}
|
|
|
|
// loadFromFile reads a file and returns its contents as a trimmed string
|
|
func loadFromFile(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(data)), nil
|
|
}
|