package app import ( "context" "fmt" "os" "path/filepath" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" ) func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, sessionFlag string, sessionOpts config.SessionLoadOptions) (*config.Config, error) { resolvedPipelinePath, err := resolvePipelineConfigPath(pipelineFlag) if err != nil { return nil, err } resolvedCampaignPath, err := resolveCampaignConfigPath(campaignFlag) if err != nil { return nil, err } if explicitSession := strings.TrimSpace(sessionFlag); explicitSession != "" { return config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, explicitSession, sessionOpts) } discoveredSession, err := discoverSessionConfigPathWithCandidates(config.DefaultSessionConfigSearchPaths) if err != nil { return nil, err } if discoveredSession.Path != "" { return config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, discoveredSession.Path, sessionOpts) } pipelineCfg, err := config.LoadPipeline(resolvedPipelinePath) if err != nil { return nil, err } campaignCfg, err := config.LoadCampaign(resolvedCampaignPath) if err != nil { return nil, err } sessionID := strings.TrimSpace(sessionOpts.SessionID) if sessionID == "" { return nil, missingSessionConfigError(discoveredSession.Searched, "remote session loading requires --session-id") } sessionPrefix := artifacts.S3SessionPrefix(pipelineCfg.Storage.S3.RootPrefix, campaignCfg.Campaign, sessionID) remoteKey := artifacts.S3SessionConfigKey(sessionPrefix) partialCfg := &config.Config{ Pipeline: pipelineCfg, Campaign: campaignCfg, PipelinePath: resolvedPipelinePath, CampaignPath: resolvedCampaignPath, } store, err := newObjectStoreFromConfigFn(ctx, partialCfg) if err != nil { return nil, missingSessionConfigError(discoveredSession.Searched, fmt.Sprintf("remote session %q unavailable: %v", remoteKey, err)) } sessionInfo, err := findRemoteSessionConfig(ctx, store, sessionPrefix, remoteKey) if err != nil { return nil, missingSessionConfigError(discoveredSession.Searched, err.Error()) } sessionTempPath, err := downloadRemoteSessionConfig(ctx, store, remoteKey) if err != nil { return nil, missingSessionConfigError(discoveredSession.Searched, fmt.Sprintf("remote session %q download failed: %v", remoteKey, err)) } sessionBytes, err := os.ReadFile(sessionTempPath) if err != nil { return nil, fmt.Errorf("read downloaded remote session %q: %w", sessionTempPath, err) } sessionCfg, err := config.LoadSessionBytesWithOptions("s3://"+s3BucketName(pipelineCfg)+"/"+remoteKey, sessionBytes, sessionOpts) if err != nil { return nil, err } return config.Resolve( resolvedPipelinePath, pipelineCfg, resolvedCampaignPath, campaignCfg, sessionTempPath, sessionCfg, config.SessionSource{ Source: "session_config.s3", LocalPath: sessionTempPath, S3Bucket: s3BucketName(pipelineCfg), S3Key: remoteKey, S3Size: sessionInfo.Size, S3ETag: sessionInfo.ETag, SpoolPath: sessionTempPath, }, ) } func findRemoteSessionConfig(ctx context.Context, store storage.ObjectStore, sessionPrefix, remoteKey string) (storage.ObjectInfo, error) { objects, err := store.List(ctx, sessionPrefix) if err != nil { return storage.ObjectInfo{}, fmt.Errorf("remote session %q list failed: %w", remoteKey, err) } for _, obj := range objects { if obj.Key == remoteKey { return obj, nil } } return storage.ObjectInfo{}, fmt.Errorf("remote session %q not found", remoteKey) } func downloadRemoteSessionConfig(ctx context.Context, store storage.ObjectStore, remoteKey string) (string, error) { f, err := os.CreateTemp("", "narratio-session-*.yml") if err != nil { return "", fmt.Errorf("create temp file: %w", err) } path := f.Name() if err := f.Close(); err != nil { return "", fmt.Errorf("close temp file %q: %w", path, err) } if err := store.Download(ctx, remoteKey, path); err != nil { return "", err } return filepath.Clean(path), nil } func s3BucketName(cfg *config.PipelineConfig) string { if cfg == nil || cfg.Storage.S3 == nil { return "" } return strings.TrimSpace(cfg.Storage.S3.Bucket) }