| 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" |
| ) |
|
|
| |
| type BootstrapResult struct { |
| Config *config.Config |
| ConfigFilePath string |
| TokenStore coreauth.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) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| 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 == nil { |
| tokenStore = sdkAuth.NewFileTokenStore() |
| } |
|
|
| return &BootstrapResult{ |
| Config: cfg, |
| ConfigFilePath: configFilePath, |
| TokenStore: tokenStore, |
| }, nil |
| } |
|
|