158 lines
4.8 KiB
Go
158 lines
4.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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"
|
|
)
|
|
|
|
type effectiveLocks struct {
|
|
Static []config.ArchiveLockRule
|
|
Remote []config.ArchiveLockRule
|
|
All []config.ArchiveLockRule
|
|
Key string
|
|
}
|
|
|
|
func remoteLocksKey(cfg *config.Config) (string, error) {
|
|
if cfg == nil || cfg.Pipeline == nil || cfg.Session == nil {
|
|
return "", fmt.Errorf("resolved config is required")
|
|
}
|
|
if cfg.Pipeline.Storage.S3 == nil {
|
|
return "", fmt.Errorf("pipeline.storage.s3 configuration is required")
|
|
}
|
|
sessionPrefix := artifacts.S3SessionPrefix(
|
|
cfg.Pipeline.Storage.S3.RootPrefix,
|
|
cfg.Session.Campaign,
|
|
cfg.Session.SessionID,
|
|
)
|
|
return artifacts.S3SessionLocksKey(sessionPrefix), nil
|
|
}
|
|
|
|
func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage.ObjectStore) (*config.ArchiveLockStore, string, error) {
|
|
key, err := remoteLocksKey(cfg)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
exists, err := store.Exists(ctx, key)
|
|
if err != nil {
|
|
return nil, key, fmt.Errorf("check remote locks %q: %w", key, err)
|
|
}
|
|
if !exists {
|
|
return &config.ArchiveLockStore{}, key, nil
|
|
}
|
|
tmp, err := downloadObjectToTemp(ctx, store, key, "narratio-locks-*.yml")
|
|
if err != nil {
|
|
return nil, key, fmt.Errorf("download remote locks %q: %w", key, err)
|
|
}
|
|
defer func() { _ = os.Remove(tmp) }()
|
|
data, err := os.ReadFile(tmp)
|
|
if err != nil {
|
|
return nil, key, fmt.Errorf("read remote locks %q: %w", key, err)
|
|
}
|
|
lockStore, err := config.LoadArchiveLockStoreBytes("s3://"+s3BucketName(cfg.Pipeline)+"/"+key, data, cfg.Pipeline.Scriptorium)
|
|
if err != nil {
|
|
return nil, key, err
|
|
}
|
|
return lockStore, key, nil
|
|
}
|
|
|
|
func loadEffectiveLocks(ctx context.Context, cfg *config.Config, store storage.ObjectStore) (*effectiveLocks, error) {
|
|
staticLocks := staticArchiveLocks(cfg)
|
|
if store == nil {
|
|
return &effectiveLocks{
|
|
Static: staticLocks,
|
|
All: append([]config.ArchiveLockRule(nil), staticLocks...),
|
|
}, nil
|
|
}
|
|
lockStore, key, err := loadRemoteLockStore(ctx, cfg, store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
remoteLocks := append([]config.ArchiveLockRule(nil), lockStore.Locks...)
|
|
return &effectiveLocks{
|
|
Static: staticLocks,
|
|
Remote: remoteLocks,
|
|
All: config.MergeArchiveLockRules(staticLocks, remoteLocks),
|
|
Key: key,
|
|
}, nil
|
|
}
|
|
|
|
func staticArchiveLocks(cfg *config.Config) []config.ArchiveLockRule {
|
|
if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Archive == nil {
|
|
return nil
|
|
}
|
|
return append([]config.ArchiveLockRule(nil), cfg.Pipeline.Archive.Locks...)
|
|
}
|
|
|
|
func applyEffectiveLocks(cfg *config.Config, locks []config.ArchiveLockRule) {
|
|
if cfg == nil || cfg.Pipeline == nil {
|
|
return
|
|
}
|
|
if cfg.Pipeline.Archive == nil {
|
|
cfg.Pipeline.Archive = &config.ArchiveConfig{}
|
|
}
|
|
cfg.Pipeline.Archive.Locks = append([]config.ArchiveLockRule(nil), locks...)
|
|
}
|
|
|
|
func uploadRemoteLockStore(ctx context.Context, store storage.ObjectStore, key string, lockStore *config.ArchiveLockStore) error {
|
|
data, err := config.MarshalArchiveLockStore(lockStore)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp("", "narratio-locks-upload-*.yml")
|
|
if err != nil {
|
|
return fmt.Errorf("create lock store temp file: %w", err)
|
|
}
|
|
tmpPath := tmp.Name()
|
|
defer func() { _ = os.Remove(tmpPath) }()
|
|
if _, err := tmp.Write(data); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("write lock store temp file: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("close lock store temp file: %w", err)
|
|
}
|
|
if _, err := store.Upload(ctx, tmpPath, key, storage.UploadOptions{ContentType: "application/x-yaml; charset=utf-8"}); err != nil {
|
|
return fmt.Errorf("upload remote locks %q: %w", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lockSourceSet(locks []config.ArchiveLockRule) map[string]config.ArchiveLockRule {
|
|
out := make(map[string]config.ArchiveLockRule, len(locks))
|
|
for _, lock := range locks {
|
|
source := strings.TrimSpace(lock.Source)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
lock.Source = source
|
|
lock.Reason = strings.TrimSpace(lock.Reason)
|
|
out[source] = lock
|
|
}
|
|
return out
|
|
}
|
|
|
|
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 := os.MkdirAll(filepath.Dir(cleaned), 0o755); err != nil {
|
|
return fmt.Errorf("create output directory: %w", err)
|
|
}
|
|
return os.WriteFile(cleaned, data, 0o644)
|
|
}
|