package app import ( "context" "flag" "fmt" "io" "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" "gitea.maximumdirect.net/eric/narratio/internal/fileops" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) type commonConfigFlags struct { pipelinePath string campaignPath string campaignFilePath string sessionPath string sessionID string previousSessionID string profile singletonStringFlag secretsDir singletonStringFlag } func addCommonConfigFlags(fs *flag.FlagSet, flags *commonConfigFlags) { fs.StringVar(&flags.pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)") fs.StringVar(&flags.campaignPath, "campaign", "", "campaign ID") fs.StringVar(&flags.campaignFilePath, "campaign-file", "", "path to campaign.yml") fs.StringVar(&flags.sessionPath, "session", "", "path to session.yml") fs.StringVar(&flags.sessionID, "session-id", "", "session identifier") fs.StringVar(&flags.previousSessionID, "previous-session-id", "", "expected previous session identifier") flags.profile.name = "profile" fs.Var(&flags.profile, "profile", "named pipeline profile") flags.secretsDir.name = "secrets-dir" fs.Var(&flags.secretsDir, "secrets-dir", "secrets directory (optional; defaults beside pipeline.yml)") } func (f commonConfigFlags) sessionOptions() config.SessionLoadOptions { return config.SessionLoadOptions{ SessionID: f.sessionID, PreviousSessionID: f.previousSessionID, Profile: f.profile.pointer(), } } // Session dispatches session helper subcommands. func Session(ctx context.Context, args []string, out io.Writer) error { return sessionWithDependencies(ctx, args, out, commandDependencies{}) } func sessionWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { deps = normalizeCommandDependencies(deps) if len(args) == 0 { return fmt.Errorf("session: expected subcommand: init|validate|status|plan|restore|artifacts|locks") } switch args[0] { case "init": return sessionInitWithDependencies(ctx, args[1:], out, deps) case "validate": return sessionValidateWithDependencies(ctx, args[1:], out, deps) case "status": return statusWithDependencies(ctx, args[1:], out, deps) case "plan": return planWithDependencies(ctx, args[1:], out, deps) case "restore": return restoreWithDependencies(ctx, args[1:], out, deps) case "artifacts": return artifactsListWithDependencies(ctx, args[1:], out, deps) case "locks": return sessionLocksWithDependencies(ctx, args[1:], out, deps) default: return fmt.Errorf("session: unknown subcommand %q", args[0]) } } // SessionLocks dispatches session-oriented publish lock list and mutation // helpers. func SessionLocks(ctx context.Context, args []string, out io.Writer) error { return sessionLocksWithDependencies(ctx, args, out, commandDependencies{}) } func sessionLocksWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { if len(args) > 0 && !isCLIFlagToken(args[0]) { switch args[0] { case "add": return locksAddWithDependencies(ctx, args[1:], out, deps) case "remove": return locksRemoveWithDependencies(ctx, args[1:], out, deps) } } return locksListWithDependencies(ctx, args, out, deps) } // Artifacts dispatches artifact helper subcommands. func Artifacts(ctx context.Context, args []string, out io.Writer) error { return artifactsWithDependencies(ctx, args, out, commandDependencies{}) } func artifactsWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { if len(args) == 0 { return fmt.Errorf("artifacts: expected subcommand: list") } switch args[0] { case "list": return artifactsListWithDependencies(ctx, args[1:], out, deps) default: return fmt.Errorf("artifacts: unknown subcommand %q", args[0]) } } func loadHelperContextWithDependencies(ctx context.Context, flags commonConfigFlags, needStore bool, deps commandDependencies) (*config.Config, storage.ObjectStore, []artifacts.RemoteLock, *manifest.Manifest, func(), error) { deps = normalizeCommandDependencies(deps) loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), flags.secretsDir.pointer(), deps) if err != nil { return nil, nil, nil, nil, nil, err } release := true defer func() { if release { _ = loaded.Close() } }() cfg := loaded.Config if err := config.Validate(cfg); err != nil { return nil, nil, nil, nil, nil, err } var store storage.ObjectStore if needStore { store, err = newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps) if err != nil { return nil, nil, nil, nil, nil, err } } else { store, _ = objectStoreIfConfiguredWithDependencies(ctx, cfg, deps) } var locks []artifacts.RemoteLock if store != nil { sessionPrefix, prefixErr := artifacts.ResolvePublishSessionPrefix(cfg, nil) if prefixErr != nil { return nil, nil, nil, nil, nil, prefixErr } locks, err = artifacts.ListRemoteLocks(ctx, store, sessionPrefix) if err != nil { return nil, nil, nil, nil, nil, err } } paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID) m, err := loadLocalManifest(ctx, paths.ManifestPath) if err != nil { return nil, nil, nil, nil, nil, err } release = false return cfg, store, locks, m, func() { _ = loaded.Close() }, nil } func objectStoreIfConfiguredWithDependencies(ctx context.Context, cfg *config.Config, deps commandDependencies) (storage.ObjectStore, error) { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Storage.S3 == nil || strings.TrimSpace(cfg.Pipeline.Storage.S3.Bucket) == "" { return nil, nil } store, err := newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps) if err != nil { return nil, err } return store, nil } func writeLocalFile(path string, data []byte, force bool) error { cleaned := filepath.Clean(strings.TrimSpace(path)) if cleaned == "" || cleaned == "." { return fmt.Errorf("output path is required") } if !force { if _, err := os.Stat(cleaned); err == nil { return fmt.Errorf("output file %q already exists; pass --force to overwrite", cleaned) } else if err != nil && !os.IsNotExist(err) { return fmt.Errorf("check output file %q: %w", cleaned, err) } } if err := fileops.EnsureWorkspaceDirectory(filepath.Dir(cleaned)); err != nil { return fmt.Errorf("create output directory: %w", err) } if err := os.WriteFile(cleaned, data, fileops.WorkspaceFileMode); err != nil { return err } return os.Chmod(cleaned, fileops.WorkspaceFileMode) }