182 lines
5.9 KiB
Go
182 lines
5.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
)
|
|
|
|
// Locks dispatches publish lock list and mutation helpers.
|
|
func Locks(ctx context.Context, args []string, out io.Writer) error {
|
|
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
|
|
switch args[0] {
|
|
case "add":
|
|
return LocksAdd(ctx, args[1:], out)
|
|
case "remove":
|
|
return LocksRemove(ctx, args[1:], out)
|
|
default:
|
|
return fmt.Errorf("locks: unknown subcommand %q", args[0])
|
|
}
|
|
}
|
|
return LocksList(ctx, args, out)
|
|
}
|
|
|
|
// LocksList lists effective publish locks.
|
|
func LocksList(ctx context.Context, args []string, out io.Writer) 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, _, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
|
|
if err != nil {
|
|
return fmt.Errorf("locks: %w", err)
|
|
}
|
|
defer cleanup()
|
|
writeLocks(out, cfg, locks)
|
|
return nil
|
|
}
|
|
|
|
// LocksAdd adds or updates one remote lock.
|
|
func LocksAdd(ctx context.Context, args []string, out io.Writer) 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")
|
|
}
|
|
cfg, store, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
|
|
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 {
|
|
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 err != nil {
|
|
return 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)
|
|
return err
|
|
}
|
|
|
|
// LocksRemove removes one remote lock.
|
|
func LocksRemove(ctx context.Context, args []string, out io.Writer) 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")
|
|
}
|
|
cfg, store, locks, _, cleanup, err := loadHelperContext(ctx, flags, true)
|
|
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 {
|
|
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 {
|
|
return fmt.Errorf("locks remove: %w", 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 {
|
|
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
|
|
}
|
|
}
|
|
staticSet := lockSourceSet(locks.Static)
|
|
for _, lock := range locks.All {
|
|
origin := "remote"
|
|
if _, ok := staticSet[lock.Source]; ok {
|
|
origin = "pipeline"
|
|
}
|
|
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 origin=%s %s reason=%s\n", lock.Source, origin, promo, reason)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|