Migrate lock commands to independent objects

This commit is contained in:
2026-09-05 13:11:11 +00:00
parent 4bc33773fa
commit 610eaefb27
13 changed files with 368 additions and 156 deletions

View File

@@ -2,12 +2,15 @@ package app
import (
"context"
"errors"
"flag"
"fmt"
"io"
"sort"
"strings"
"time"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
@@ -26,7 +29,7 @@ func Locks(ctx context.Context, args []string, out io.Writer) error {
return LocksList(ctx, args, out)
}
// LocksList lists effective publish locks.
// LocksList lists independent publish locks.
func LocksList(ctx context.Context, args []string, out io.Writer) error {
fs := flag.NewFlagSet("locks", flag.ContinueOnError)
fs.SetOutput(io.Discard)
@@ -38,11 +41,15 @@ func LocksList(ctx context.Context, args []string, out io.Writer) error {
if strings.TrimSpace(flags.sessionID) == "" {
return fmt.Errorf("locks: session_id is required")
}
cfg, _, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
cfg, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
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
}
@@ -64,31 +71,28 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
if strings.TrimSpace(flags.sessionID) == "" {
return fmt.Errorf("locks add: session_id is required")
}
cfg, store, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
_, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
if err != nil {
return fmt.Errorf("locks add: %w", err)
}
defer cleanup()
if _, err := config.ValidatePublishLockRules([]config.PublishLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, cfg.Pipeline.Notarius, "locks add"); err != nil {
lock, err := artifacts.NewRemoteLock(source, strings.TrimSpace(reason), nowUTC())
if err != nil {
return fmt.Errorf("locks add: %w", err)
}
if _, ok := lockSourceSet(locks.Static)[source]; ok {
return fmt.Errorf("locks add: source %q is locked by pipeline config and cannot be modified remotely", source)
}
if err := mutateRemoteLockStore(ctx, cfg, store, func(lockStore *config.PublishLockStore) error {
remoteSet := lockSourceSet(lockStore.Locks)
if _, exists := remoteSet[source]; exists && !force {
return fmt.Errorf("remote lock for %q already exists; pass --force to update", source)
}
remoteSet[source] = config.PublishLockRule{Source: source, Reason: strings.TrimSpace(reason)}
lockStore.Locks = lockMapValues(remoteSet)
normalized, err := config.ValidatePublishLockRules(lockStore.Locks, cfg.Pipeline.Scriptorium, cfg.Pipeline.Notarius, "locks")
if !force {
current, err := artifacts.LookupRemoteLock(ctx, store, sessionPrefix, source)
if err != nil {
return err
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)
}
lockStore.Locks = normalized
return nil
}); err != nil {
return fmt.Errorf("locks add: %w", err)
}
_, err = fmt.Fprintf(out, "narratio session locks add: locked %s\n", source)
@@ -108,34 +112,55 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
if strings.TrimSpace(flags.sessionID) == "" {
return fmt.Errorf("locks remove: session_id is required")
}
cfg, store, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
_, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
if err != nil {
return fmt.Errorf("locks remove: %w", err)
}
defer cleanup()
if _, err := config.ValidatePublishLockRules([]config.PublishLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, cfg.Pipeline.Notarius, "locks remove"); err != nil {
current, err := artifacts.LookupRemoteLock(ctx, store, sessionPrefix, source)
if err != nil {
return fmt.Errorf("locks remove: %w", err)
}
if err := mutateRemoteLockStore(ctx, cfg, store, func(lockStore *config.PublishLockStore) error {
remoteSet := lockSourceSet(lockStore.Locks)
if _, ok := remoteSet[source]; !ok {
if _, static := lockSourceSet(locks.Static)[source]; static {
return fmt.Errorf("source %q is locked by pipeline config and cannot be unlocked remotely", source)
}
return fmt.Errorf("remote lock for %q does not exist", source)
}
delete(remoteSet, source)
lockStore.Locks = lockMapValues(remoteSet)
return nil
}); err != nil {
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 writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
if locks == nil || len(locks.All) == 0 {
func loadLockCommandContext(ctx context.Context, flags commonConfigFlags) (*config.Config, storage.ObjectStore, string, func(), error) {
loaded, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
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 := newCommandObjectStore(ctx, cfg, nil)
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
}
@@ -146,12 +171,7 @@ func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
published[strings.TrimSpace(rule.Source)] = rule
}
}
staticSet := lockSourceSet(locks.Static)
for _, lock := range locks.All {
origin := "remote"
if _, ok := staticSet[lock.Source]; ok {
origin = "pipeline"
}
for _, lock := range locks {
promo := "not-published"
if _, ok := published[lock.Source]; ok {
promo = "published"
@@ -160,22 +180,6 @@ func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
if reason == "" {
reason = "(no reason)"
}
fmt.Fprintf(out, "- %s origin=%s %s reason=%s\n", lock.Source, origin, promo, reason)
fmt.Fprintf(out, "- %s %s reason=%s created_at=%s\n", lock.Source, promo, reason, lock.CreatedAt.UTC().Format(time.RFC3339Nano))
}
}
func lockMapValues(in map[string]config.PublishLockRule) []config.PublishLockRule {
keys := make([]string, 0, len(in))
for key := range in {
keys = append(keys, key)
}
sort.Strings(keys)
out := make([]config.PublishLockRule, 0, len(keys))
for _, key := range keys {
item := in[key]
item.Source = key
item.Reason = strings.TrimSpace(item.Reason)
out = append(out, item)
}
return out
}