Add shared-root destination state model

This commit is contained in:
2026-06-08 18:45:16 +00:00
parent ef0b6c1056
commit eb86cf9ab6
13 changed files with 1165 additions and 10 deletions

View File

@@ -55,6 +55,7 @@ type Destination struct {
Transform Transform `yaml:"transform"`
PathMap PathMapping `yaml:"path_mapping"`
Links *Links `yaml:"links"`
State StatePolicy `yaml:"state"`
Reconciliation ReconciliationPolicy `yaml:"reconciliation"`
Transfer TransferPolicy `yaml:"transfer"`
}
@@ -123,6 +124,10 @@ type ReconciliationPolicy struct {
Mode string `yaml:"mode"`
}
type StatePolicy struct {
Mode string `yaml:"mode"`
}
type TransferPolicy struct {
OnDestinationSame string `yaml:"on_destination_same"`
OnDestinationOlder string `yaml:"on_destination_older"`

View File

@@ -47,6 +47,11 @@ const (
ReconciliationModeMerge = "merge"
)
const (
StateModeSingleOwner = "single_owner"
StateModeSharedRoot = "shared_root"
)
const DefaultS3Region = "us-east-1"
const (
@@ -84,6 +89,9 @@ func ApplyDefaults(cfg *Config) {
if destination.Links != nil && destination.Links.Primary == "" {
destination.Links.Primary = LinkPrimaryAuto
}
if destination.State.Mode == "" {
destination.State.Mode = StateModeSingleOwner
}
if destination.Reconciliation.Mode == "" {
destination.Reconciliation.Mode = ReconciliationModeReplace
}

View File

@@ -36,6 +36,9 @@ pipelines:
if got, want := destination.Reconciliation.Mode, ReconciliationModeReplace; got != want {
t.Fatalf("reconciliation mode default = %q, want %q", got, want)
}
if got, want := destination.State.Mode, StateModeSingleOwner; got != want {
t.Fatalf("state mode default = %q, want %q", got, want)
}
if cfg.Secrets.Directory != "" {
t.Fatalf("secrets.directory = %q, want empty", cfg.Secrets.Directory)
}
@@ -196,6 +199,35 @@ pipelines:
}
}
func TestLoadFileAcceptsExplicitStateModes(t *testing.T) {
cfg := loadConfig(t, `
pipelines:
- id: reports
source:
backend: local
path: /source
destinations:
- id: archive
backend: local
path: /archive
state:
mode: single_owner
- id: web
backend: local
path: /web
state:
mode: shared_root
`)
destinations := cfg.Pipelines[0].Destinations
if got, want := destinations[0].State.Mode, StateModeSingleOwner; got != want {
t.Fatalf("archive state mode = %q, want %q", got, want)
}
if got, want := destinations[1].State.Mode, StateModeSharedRoot; got != want {
t.Fatalf("web state mode = %q, want %q", got, want)
}
}
func TestLoadFileAcceptsFixedPathMapping(t *testing.T) {
cfg := loadConfig(t, `
pipelines:

View File

@@ -72,6 +72,7 @@ func Validate(cfg Config) error {
errs = validatePublishTransformPolicy(errs, destinationContext, destination.Publish, destination.Transform)
errs = validatePathMapping(errs, destinationContext+".path_mapping", destination.PathMap)
errs = validateLinks(errs, destinationContext+".links", destination.Links)
errs = validateStatePolicy(errs, destinationContext+".state", destination.State)
errs = validateReconciliationPolicy(errs, destinationContext+".reconciliation", destination.Reconciliation)
errs = validateTransferPolicy(errs, destinationContext+".transfer", destination.Transfer)
}
@@ -322,6 +323,13 @@ func validateReconciliationPolicy(errs ValidationErrors, context string, policy
return errs
}
func validateStatePolicy(errs ValidationErrors, context string, policy StatePolicy) ValidationErrors {
if policy.Mode != StateModeSingleOwner && policy.Mode != StateModeSharedRoot {
errs = append(errs, context+".mode must be "+StateModeSingleOwner+" or "+StateModeSharedRoot)
}
return errs
}
func validateTransferPolicy(errs ValidationErrors, context string, policy TransferPolicy) ValidationErrors {
if policy.OnDestinationSame != TransferActionSkip && policy.OnDestinationSame != TransferActionFail {
errs = append(errs, context+".on_destination_same must be skip or fail")

View File

@@ -141,6 +141,40 @@ func TestValidateReconciliationPolicy(t *testing.T) {
}
}
func TestValidateStatePolicy(t *testing.T) {
tests := []struct {
name string
mode string
wantErr bool
}{
{name: "single owner", mode: StateModeSingleOwner},
{name: "shared root", mode: StateModeSharedRoot},
{name: "invalid", mode: "shared", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{Pipelines: []Pipeline{{
ID: "reports",
Source: Backend{Backend: BackendLocal, Path: "/source"},
Destinations: []Destination{{
ID: "archive",
Backend: BackendLocal,
Path: "/destination",
State: StatePolicy{Mode: tt.mode},
}},
}}}
ApplyDefaults(&cfg)
err := Validate(cfg)
if tt.wantErr && err == nil {
t.Fatal("Validate() error = nil, want error")
}
if !tt.wantErr && err != nil {
t.Fatalf("Validate() error = %v", err)
}
})
}
}
func TestValidateLinks(t *testing.T) {
tests := []struct {
name string