171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
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 {
|
|
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 := 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
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
_, store, sessionPrefix, cleanup, err := loadLockCommandContext(ctx, flags)
|
|
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 {
|
|
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 := loadLockCommandContext(ctx, flags)
|
|
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) {
|
|
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
|
|
}
|
|
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))
|
|
}
|
|
}
|