Unify application configuration loading
This commit is contained in:
@@ -88,11 +88,7 @@ func cleanAllLocal(flags commonConfigFlags, dryRun, clearCache bool, out io.Writ
|
||||
strings.TrimSpace(flags.previousSessionID) != "" {
|
||||
return fmt.Errorf("clean: --all cannot be combined with --campaign, --campaign-file, --session, a session_id, or --previous-session-id")
|
||||
}
|
||||
resolvedPipelinePath, err := resolvePipelineConfigPath(flags.pipelinePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
pipelineCfg, err := config.LoadPipeline(resolvedPipelinePath)
|
||||
_, pipelineCfg, err := loadPipelineConfig(flags.pipelinePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean: %w", err)
|
||||
}
|
||||
|
||||
@@ -14,14 +14,10 @@ import (
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
)
|
||||
|
||||
type pipelineCampaignConfig struct {
|
||||
PipelinePath string
|
||||
CampaignPath string
|
||||
Pipeline *config.PipelineConfig
|
||||
Campaign *config.CampaignConfig
|
||||
}
|
||||
type pipelineCampaignConfig = config.LoadedPipelineCampaign
|
||||
|
||||
var downloadObjectToTempFn = storage.DownloadObjectToTemp
|
||||
var loadPipelineConfigFn = config.LoadPipeline
|
||||
|
||||
type commandConfig struct {
|
||||
Config *config.Config
|
||||
@@ -58,7 +54,7 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
|
||||
}
|
||||
|
||||
if explicitSession := strings.TrimSpace(sessionFlag); explicitSession != "" {
|
||||
cfg, err := config.LoadWithSessionOptions(base.PipelinePath, base.CampaignPath, explicitSession, sessionOpts)
|
||||
cfg, err := config.LoadSessionWithPipelineCampaignOptions(*base, explicitSession, sessionOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,7 +66,7 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
|
||||
return nil, err
|
||||
}
|
||||
if discoveredSession.Path != "" {
|
||||
cfg, err := config.LoadWithSessionOptions(base.PipelinePath, base.CampaignPath, discoveredSession.Path, sessionOpts)
|
||||
cfg, err := config.LoadSessionWithPipelineCampaignOptions(*base, discoveredSession.Path, sessionOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,11 +84,9 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
|
||||
}
|
||||
sessionPrefix := artifacts.S3SessionPrefix(rootPrefix, config.CampaignID(base.Campaign), sessionID)
|
||||
remoteKey := artifacts.S3SessionConfigKey(sessionPrefix)
|
||||
partialCfg := &config.Config{
|
||||
Pipeline: base.Pipeline,
|
||||
Campaign: base.Campaign,
|
||||
PipelinePath: base.PipelinePath,
|
||||
CampaignPath: base.CampaignPath,
|
||||
partialCfg, err := config.ResolveLoadedPipelineCampaign(*base, "", nil, config.SessionSource{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store, err := newCommandObjectStore(ctx, partialCfg, nil)
|
||||
if err != nil {
|
||||
@@ -122,11 +116,8 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := config.Resolve(
|
||||
base.PipelinePath,
|
||||
base.Pipeline,
|
||||
base.CampaignPath,
|
||||
base.Campaign,
|
||||
cfg, err := config.ResolveLoadedPipelineCampaign(
|
||||
*base,
|
||||
sessionTempPath,
|
||||
sessionCfg,
|
||||
config.SessionSource{
|
||||
@@ -147,11 +138,7 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
|
||||
}
|
||||
|
||||
func loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag string) (*pipelineCampaignConfig, error) {
|
||||
resolvedPipelinePath, err := resolvePipelineConfigPath(pipelineFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pipelineCfg, err := config.LoadPipeline(resolvedPipelinePath)
|
||||
loadedPipelinePath, pipelineCfg, err := loadPipelineConfig(pipelineFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,13 +156,25 @@ func loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag str
|
||||
}
|
||||
}
|
||||
return &pipelineCampaignConfig{
|
||||
PipelinePath: resolvedPipelinePath,
|
||||
PipelinePath: loadedPipelinePath,
|
||||
CampaignPath: resolvedCampaignPath,
|
||||
Pipeline: pipelineCfg,
|
||||
Campaign: campaignCfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadPipelineConfig(pipelineFlag string) (string, *config.PipelineConfig, error) {
|
||||
resolvedPipelinePath, err := resolvePipelineConfigPath(pipelineFlag)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
pipelineCfg, err := loadPipelineConfigFn(resolvedPipelinePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return resolvedPipelinePath, pipelineCfg, nil
|
||||
}
|
||||
|
||||
func findRemoteSessionConfig(ctx context.Context, store storage.ObjectStore, sessionPrefix, remoteKey string) (storage.ObjectInfo, error) {
|
||||
objects, err := store.List(ctx, sessionPrefix)
|
||||
if err != nil {
|
||||
|
||||
50
internal/app/config_loader_test.go
Normal file
50
internal/app/config_loader_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
func TestLoadCommandConfigRetainsInitiallyLoadedPipeline(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
|
||||
originalLoader := loadPipelineConfigFn
|
||||
loadCalls := 0
|
||||
loadPipelineConfigFn = func(path string) (*config.PipelineConfig, error) {
|
||||
loaded, err := originalLoader(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loadCalls++
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changedRoot := workspaceRoot + "-changed"
|
||||
updated := strings.ReplaceAll(string(data), workspaceRoot, changedRoot)
|
||||
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loaded, nil
|
||||
}
|
||||
t.Cleanup(func() { loadPipelineConfigFn = originalLoader })
|
||||
|
||||
loaded, err := loadCommandConfig(context.Background(), pipelinePath, "", campaignPath, sessionPath, config.SessionLoadOptions{SessionID: "2026-05-03"})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCommandConfig() error = %v", err)
|
||||
}
|
||||
defer func() { _ = loaded.Close() }()
|
||||
|
||||
if loadCalls != 1 {
|
||||
t.Fatalf("pipeline load calls = %d, want 1", loadCalls)
|
||||
}
|
||||
if got := loaded.Config.Pipeline.Workspace.Root; got != workspaceRoot {
|
||||
t.Fatalf("resolved workspace root = %q, want originally loaded %q", got, workspaceRoot)
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("session init: %w", err)
|
||||
}
|
||||
cfg, err := config.Resolve(base.PipelinePath, base.Pipeline, base.CampaignPath, base.Campaign, label, sessionCfg, config.SessionSource{Source: "session_config", LocalPath: label})
|
||||
cfg, err := config.ResolveLoadedPipelineCampaign(*base, label, sessionCfg, config.SessionSource{Source: "session_config", LocalPath: label})
|
||||
if err != nil {
|
||||
return fmt.Errorf("session init: %w", err)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,9 @@ inputs:
|
||||
if err := loaded.Close(); err != nil {
|
||||
t.Fatalf("second Close() error = %v", err)
|
||||
}
|
||||
if loaded.Config == nil || loaded.Config.Pipeline == nil || loaded.Config.Campaign == nil || loaded.Config.Session == nil {
|
||||
t.Fatal("closing remote session cleanup discarded retained configuration")
|
||||
}
|
||||
if _, err := os.Stat(downloadedPath); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("downloaded remote session path still exists or could not be inspected: %q, err=%v", downloadedPath, err)
|
||||
}
|
||||
|
||||
@@ -167,42 +167,81 @@ func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sess
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadSessionWithPipelineCampaignOptions(LoadedPipelineCampaign{
|
||||
PipelinePath: pipelinePath,
|
||||
Pipeline: pipelineCfg,
|
||||
CampaignPath: campaignPath,
|
||||
Campaign: campaignCfg,
|
||||
}, sessionPath, sessionOpts)
|
||||
}
|
||||
|
||||
// LoadedPipelineCampaign retains one already loaded pipeline and campaign for
|
||||
// subsequent local or remote session resolution. It prevents a command from
|
||||
// reloading the root pipeline after campaign selection.
|
||||
type LoadedPipelineCampaign struct {
|
||||
PipelinePath string
|
||||
Pipeline *PipelineConfig
|
||||
CampaignPath string
|
||||
Campaign *CampaignConfig
|
||||
}
|
||||
|
||||
// LoadSessionWithPipelineCampaignOptions loads one local session and combines
|
||||
// it with an already loaded pipeline and campaign.
|
||||
func LoadSessionWithPipelineCampaignOptions(loaded LoadedPipelineCampaign, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
||||
sessionCfg, err := LoadSessionWithOptions(sessionPath, sessionOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Resolve(pipelinePath, pipelineCfg, campaignPath, campaignCfg, sessionPath, sessionCfg, SessionSource{
|
||||
return ResolveLoadedPipelineCampaign(loaded, sessionPath, sessionCfg, SessionSource{
|
||||
Source: "session_config",
|
||||
LocalPath: sessionPath,
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve builds final stage-facing configuration from already loaded
|
||||
// pipeline, campaign, and session documents.
|
||||
func Resolve(pipelinePath string, pipelineCfg *PipelineConfig, campaignPath string, campaignCfg *CampaignConfig, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
|
||||
stableInputs, err := mergeCampaignSession(campaignCfg, sessionCfg, campaignPath, sessionPath)
|
||||
// ResolveLoadedPipelineCampaign combines an already loaded pipeline and
|
||||
// campaign with optional already loaded session data. A nil session preserves
|
||||
// the resolved pipeline/campaign context for callers that need to locate or
|
||||
// retrieve a session without rereading the root pipeline.
|
||||
func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Pipeline: loaded.Pipeline,
|
||||
Campaign: loaded.Campaign,
|
||||
Session: sessionCfg,
|
||||
PipelinePath: loaded.PipelinePath,
|
||||
CampaignPath: loaded.CampaignPath,
|
||||
SessionPath: sessionPath,
|
||||
SessionSource: sessionSource,
|
||||
}
|
||||
if sessionCfg == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
stableInputs, err := mergeCampaignSession(loaded.Campaign, sessionCfg, loaded.CampaignPath, sessionPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(sessionSource.Source) == "" {
|
||||
sessionSource.Source = "session_config"
|
||||
if strings.TrimSpace(cfg.SessionSource.Source) == "" {
|
||||
cfg.SessionSource.Source = "session_config"
|
||||
}
|
||||
if strings.TrimSpace(sessionSource.LocalPath) == "" {
|
||||
sessionSource.LocalPath = sessionPath
|
||||
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
|
||||
cfg.SessionSource.LocalPath = sessionPath
|
||||
}
|
||||
cfg.StableInputs = stableInputs
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Pipeline: pipelineCfg,
|
||||
Campaign: campaignCfg,
|
||||
Session: sessionCfg,
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
SessionPath: sessionPath,
|
||||
StableInputs: stableInputs,
|
||||
SessionSource: sessionSource,
|
||||
}, nil
|
||||
// Resolve builds final stage-facing configuration from already loaded
|
||||
// pipeline, campaign, and session documents.
|
||||
func Resolve(pipelinePath string, pipelineCfg *PipelineConfig, campaignPath string, campaignCfg *CampaignConfig, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
|
||||
if sessionCfg == nil {
|
||||
return nil, fmt.Errorf("session config is required")
|
||||
}
|
||||
return ResolveLoadedPipelineCampaign(LoadedPipelineCampaign{
|
||||
PipelinePath: pipelinePath,
|
||||
Pipeline: pipelineCfg,
|
||||
CampaignPath: campaignPath,
|
||||
Campaign: campaignCfg,
|
||||
}, sessionPath, sessionCfg, sessionSource)
|
||||
}
|
||||
|
||||
func campaignSessionPaths(paths ...string) (campaignPath, sessionPath string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user