package app import ( "context" "errors" "flag" "fmt" "io" "strings" "time" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" ) // LocksList lists independent publish locks. func LocksList(ctx context.Context, args []string, out io.Writer) error { return locksListWithDependencies(ctx, args, out, commandDependencies{}) } func locksListWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { fs := flag.NewFlagSet("locks", flag.ContinueOnError) fs.SetOutput(io.Discard) var flags commonConfigFlags addCommonConfigFlags(fs, &flags) if err := parseSessionAwareFlags("locks", fs, args, &flags.sessionID); err != nil { return err } if strings.TrimSpace(flags.sessionID) == "" { return fmt.Errorf("locks: session_id is required") } cfg, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps) if err != nil { return fmt.Errorf("locks: %w", err) } defer cleanup() locks, err := artifacts.ListRemoteLocks(ctx, store, sessionPrefix) if err != nil { return fmt.Errorf("locks: %w", err) } writeLocks(out, cfg, locks) return nil } // LocksAdd adds or updates one remote lock. func LocksAdd(ctx context.Context, args []string, out io.Writer) error { return locksAddWithDependencies(ctx, args, out, commandDependencies{}) } func locksAddWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { fs := flag.NewFlagSet("locks add", flag.ContinueOnError) fs.SetOutput(io.Discard) var flags commonConfigFlags var reason string var force bool addCommonConfigFlags(fs, &flags) fs.StringVar(&reason, "reason", "", "lock reason") fs.BoolVar(&force, "force", false, "update existing remote lock") source, err := parseSessionIDAndOnePositionalArg("locks add", "source id", fs, args, &flags.sessionID) if err != nil { return err } if strings.TrimSpace(flags.sessionID) == "" { return fmt.Errorf("locks add: session_id is required") } _, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps) if err != nil { return fmt.Errorf("locks add: %w", err) } defer cleanup() lock, err := artifacts.NewRemoteLock(source, strings.TrimSpace(reason), nowUTC()) if err != nil { return fmt.Errorf("locks add: %w", err) } if !force { current, err := artifacts.LookupRemoteLock(ctx, store, sessionPrefix, source) if err != nil { return fmt.Errorf("locks add: %w", err) } if current.Present { return fmt.Errorf("locks add: remote lock for %q already exists; pass --force to update", source) } } if err := artifacts.AddRemoteLock(ctx, store, sessionPrefix, lock, force); err != nil { if errors.Is(err, storage.ErrConditionNotMet) { return fmt.Errorf("locks add: remote lock for %q changed concurrently; retry: %w", source, err) } return fmt.Errorf("locks add: %w", err) } _, err = fmt.Fprintf(out, "narratio session locks add: locked %s\n", source) return err } // LocksRemove removes one remote lock. func LocksRemove(ctx context.Context, args []string, out io.Writer) error { return locksRemoveWithDependencies(ctx, args, out, commandDependencies{}) } func locksRemoveWithDependencies(ctx context.Context, args []string, out io.Writer, deps commandDependencies) error { fs := flag.NewFlagSet("locks remove", flag.ContinueOnError) fs.SetOutput(io.Discard) var flags commonConfigFlags addCommonConfigFlags(fs, &flags) source, err := parseSessionIDAndOnePositionalArg("locks remove", "source id", fs, args, &flags.sessionID) if err != nil { return err } if strings.TrimSpace(flags.sessionID) == "" { return fmt.Errorf("locks remove: session_id is required") } _, store, sessionPrefix, cleanup, err := loadLockCommandContextWithDependencies(ctx, flags, deps) if err != nil { return fmt.Errorf("locks remove: %w", err) } defer cleanup() current, err := artifacts.LookupRemoteLock(ctx, store, sessionPrefix, source) if err != nil { return fmt.Errorf("locks remove: %w", err) } if err := artifacts.RemoveRemoteLock(ctx, store, sessionPrefix, source); err != nil { return fmt.Errorf("locks remove: %w", err) } if !current.Present { _, err = fmt.Fprintf(out, "narratio session locks remove: %s was not locked\n", source) return err } _, err = fmt.Fprintf(out, "narratio session locks remove: unlocked %s\n", source) return err } func loadLockCommandContext(ctx context.Context, flags commonConfigFlags) (*config.Config, storage.ObjectStore, string, func(), error) { return loadLockCommandContextWithDependencies(ctx, flags, commandDependencies{}) } func loadLockCommandContextWithDependencies(ctx context.Context, flags commonConfigFlags, deps commandDependencies) (*config.Config, storage.ObjectStore, string, func(), error) { deps = normalizeCommandDependencies(deps) loaded, err := loadCommandConfigWithDependencies(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions(), deps) if err != nil { return 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, err } store, err := newCommandObjectStoreWithDependencies(ctx, cfg, nil, deps) if err != nil { return nil, nil, "", nil, err } sessionPrefix, err := artifacts.ResolvePublishSessionPrefix(cfg, nil) if err != nil { return nil, nil, "", nil, err } release = false return cfg, store, sessionPrefix, func() { _ = loaded.Close() }, nil } func writeLocks(out io.Writer, cfg *config.Config, locks []artifacts.RemoteLock) { if len(locks) == 0 { fmt.Fprintln(out, "Publish locks: none") return } fmt.Fprintln(out, "Publish locks:") published := map[string]config.PublishOutputRule{} if cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Publish != nil { for _, rule := range cfg.Pipeline.Publish.Outputs { published[strings.TrimSpace(rule.Source)] = rule } } for _, lock := range locks { promo := "not-published" if _, ok := published[lock.Source]; ok { promo = "published" } reason := strings.TrimSpace(lock.Reason) if reason == "" { reason = "(no reason)" } fmt.Fprintf(out, "- %s %s reason=%s created_at=%s\n", lock.Source, promo, reason, lock.CreatedAt.UTC().Format(time.RFC3339Nano)) } }