API / cmd /server /bootstrap.go
sshinmen's picture
Refactor config and management API handlers
13d6015
Raw
History Blame Contribute Delete
9.57 kB
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/store"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
// BootstrapResult holds the outcome of the application bootstrap process.
type BootstrapResult struct {
Config *config.Config
ConfigFilePath string
TokenStore coreauth.Store
}
// Bootstrap initializes the environment, determines the storage backend,
// loads the configuration, and sets up the token store.
func Bootstrap(ctx context.Context, configPathOverride string, isCloudDeploy bool) (*BootstrapResult, error) {
var (
err error
cfg *config.Config
configFilePath string
tokenStore coreauth.Store
usePostgresStore bool
pgStoreDSN string
pgStoreSchema string
pgStoreLocalPath string
useGitStore bool
gitStoreRemoteURL string
gitStoreUser string
gitStorePassword string
gitStoreLocalPath string
useObjectStore bool
objectStoreEndpoint string
objectStoreAccess string
objectStoreSecret string
objectStoreBucket string
objectStoreLocalPath string
)
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err)
}
// Load environment variables from .env if present.
if errLoad := godotenv.Load(filepath.Join(wd, ".env")); errLoad != nil {
if !errors.Is(errLoad, os.ErrNotExist) {
log.WithError(errLoad).Warn("failed to load .env file")
}
}
lookupEnv := func(keys ...string) (string, bool) {
for _, key := range keys {
if value, ok := os.LookupEnv(key); ok {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed, true
}
}
}
return "", false
}
writableBase := util.WritablePath()
if value, ok := lookupEnv("PGSTORE_DSN", "pgstore_dsn"); ok {
usePostgresStore = true
pgStoreDSN = value
}
if usePostgresStore {
if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok {
pgStoreSchema = value
}
if value, ok := lookupEnv("PGSTORE_LOCAL_PATH", "pgstore_local_path"); ok {
pgStoreLocalPath = value
}
if pgStoreLocalPath == "" {
if writableBase != "" {
pgStoreLocalPath = writableBase
} else {
pgStoreLocalPath = wd
}
}
useGitStore = false
}
if value, ok := lookupEnv("GITSTORE_GIT_URL", "gitstore_git_url"); ok {
useGitStore = true
gitStoreRemoteURL = value
}
if value, ok := lookupEnv("GITSTORE_GIT_USERNAME", "gitstore_git_username"); ok {
gitStoreUser = value
}
if value, ok := lookupEnv("GITSTORE_GIT_TOKEN", "gitstore_git_token"); ok {
gitStorePassword = value
}
if value, ok := lookupEnv("GITSTORE_LOCAL_PATH", "gitstore_local_path"); ok {
gitStoreLocalPath = value
}
if value, ok := lookupEnv("OBJECTSTORE_ENDPOINT", "objectstore_endpoint"); ok {
useObjectStore = true
objectStoreEndpoint = value
}
if value, ok := lookupEnv("OBJECTSTORE_ACCESS_KEY", "objectstore_access_key"); ok {
objectStoreAccess = value
}
if value, ok := lookupEnv("OBJECTSTORE_SECRET_KEY", "objectstore_secret_key"); ok {
objectStoreSecret = value
}
if value, ok := lookupEnv("OBJECTSTORE_BUCKET", "objectstore_bucket"); ok {
objectStoreBucket = value
}
if value, ok := lookupEnv("OBJECTSTORE_LOCAL_PATH", "objectstore_local_path"); ok {
objectStoreLocalPath = value
}
// Determine and load the configuration file.
// Prefer the Postgres store when configured, otherwise fallback to git or local files.
if usePostgresStore {
if pgStoreLocalPath == "" {
pgStoreLocalPath = wd
}
pgStoreLocalPath = filepath.Join(pgStoreLocalPath, "pgstore")
pgStoreInst, err := store.NewPostgresStore(ctx, store.PostgresStoreConfig{
DSN: pgStoreDSN,
Schema: pgStoreSchema,
SpoolDir: pgStoreLocalPath,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize postgres token store: %w", err)
}
tokenStore = pgStoreInst
examplePath := filepath.Join(wd, "config.example.yaml")
if errBootstrap := pgStoreInst.Bootstrap(ctx, examplePath); errBootstrap != nil {
return nil, fmt.Errorf("failed to bootstrap postgres-backed config: %w", errBootstrap)
}
configFilePath = pgStoreInst.ConfigPath()
cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy)
if err == nil {
cfg.AuthDir = pgStoreInst.AuthDir()
log.Infof("postgres-backed token store enabled, workspace path: %s", pgStoreInst.WorkDir())
}
} else if useObjectStore {
if objectStoreLocalPath == "" {
if writableBase != "" {
objectStoreLocalPath = writableBase
} else {
objectStoreLocalPath = wd
}
}
objectStoreRoot := filepath.Join(objectStoreLocalPath, "objectstore")
resolvedEndpoint := strings.TrimSpace(objectStoreEndpoint)
useSSL := true
if strings.Contains(resolvedEndpoint, "://") {
parsed, errParse := url.Parse(resolvedEndpoint)
if errParse != nil {
return nil, fmt.Errorf("failed to parse object store endpoint %q: %w", objectStoreEndpoint, errParse)
}
switch strings.ToLower(parsed.Scheme) {
case "http":
useSSL = false
case "https":
useSSL = true
default:
return nil, fmt.Errorf("unsupported object store scheme %q (only http and https are allowed)", parsed.Scheme)
}
if parsed.Host == "" {
return nil, fmt.Errorf("object store endpoint %q is missing host information", objectStoreEndpoint)
}
resolvedEndpoint = parsed.Host
if parsed.Path != "" && parsed.Path != "/" {
resolvedEndpoint = strings.TrimSuffix(parsed.Host+parsed.Path, "/")
}
}
resolvedEndpoint = strings.TrimRight(resolvedEndpoint, "/")
objCfg := store.ObjectStoreConfig{
Endpoint: resolvedEndpoint,
Bucket: objectStoreBucket,
AccessKey: objectStoreAccess,
SecretKey: objectStoreSecret,
LocalRoot: objectStoreRoot,
UseSSL: useSSL,
PathStyle: true,
}
objectStoreInst, err := store.NewObjectTokenStore(objCfg)
if err != nil {
return nil, fmt.Errorf("failed to initialize object token store: %w", err)
}
tokenStore = objectStoreInst
examplePath := filepath.Join(wd, "config.example.yaml")
if errBootstrap := objectStoreInst.Bootstrap(ctx, examplePath); errBootstrap != nil {
return nil, fmt.Errorf("failed to bootstrap object-backed config: %w", errBootstrap)
}
configFilePath = objectStoreInst.ConfigPath()
cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy)
if err == nil {
if cfg == nil {
cfg = &config.Config{}
}
cfg.AuthDir = objectStoreInst.AuthDir()
log.Infof("object-backed token store enabled, bucket: %s", objectStoreBucket)
}
} else if useGitStore {
if gitStoreLocalPath == "" {
if writableBase != "" {
gitStoreLocalPath = writableBase
} else {
gitStoreLocalPath = wd
}
}
gitStoreRoot := filepath.Join(gitStoreLocalPath, "gitstore")
authDir := filepath.Join(gitStoreRoot, "auths")
gitStoreInst := store.NewGitTokenStore(gitStoreRemoteURL, gitStoreUser, gitStorePassword)
gitStoreInst.SetBaseDir(authDir)
tokenStore = gitStoreInst
if errRepo := gitStoreInst.EnsureRepository(); errRepo != nil {
return nil, fmt.Errorf("failed to prepare git token store: %w", errRepo)
}
configFilePath = gitStoreInst.ConfigPath()
if configFilePath == "" {
configFilePath = filepath.Join(gitStoreRoot, "config", "config.yaml")
}
if _, statErr := os.Stat(configFilePath); errors.Is(statErr, fs.ErrNotExist) {
examplePath := filepath.Join(wd, "config.example.yaml")
if _, errExample := os.Stat(examplePath); errExample != nil {
return nil, fmt.Errorf("failed to find template config file: %w", errExample)
}
if errCopy := misc.CopyConfigTemplate(examplePath, configFilePath); errCopy != nil {
return nil, fmt.Errorf("failed to bootstrap git-backed config: %w", errCopy)
}
if errCommit := gitStoreInst.PersistConfig(context.Background()); errCommit != nil {
return nil, fmt.Errorf("failed to commit initial git-backed config: %w", errCommit)
}
log.Infof("git-backed config initialized from template: %s", configFilePath)
} else if statErr != nil {
return nil, fmt.Errorf("failed to inspect git-backed config: %w", statErr)
}
cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy)
if err == nil {
cfg.AuthDir = gitStoreInst.AuthDir()
log.Infof("git-backed token store enabled, repository path: %s", gitStoreRoot)
}
} else if configPathOverride != "" {
configFilePath = configPathOverride
cfg, err = config.LoadConfigOptional(configPathOverride, isCloudDeploy)
} else {
wd, err = os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get working directory: %w", err)
}
configFilePath = filepath.Join(wd, "config.yaml")
cfg, err = config.LoadConfigOptional(configFilePath, isCloudDeploy)
}
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
if cfg == nil {
cfg = &config.Config{}
}
// If tokenStore is still nil (local file mode), use the file token store
if tokenStore == nil {
tokenStore = sdkAuth.NewFileTokenStore()
}
return &BootstrapResult{
Config: cfg,
ConfigFilePath: configFilePath,
TokenStore: tokenStore,
}, nil
}