Add shared-root publish planning

This commit is contained in:
2026-06-08 18:53:22 +00:00
parent eb86cf9ab6
commit 89169f810f
10 changed files with 629 additions and 23 deletions

View File

@@ -22,6 +22,7 @@ const (
type DestinationStatus struct {
State *DistributorState
SharedRoot *SharedRootState
StateErr error
HasContents bool
}
@@ -31,6 +32,58 @@ type Comparison struct {
Reason string
}
func CompareSharedRootOwner(source bundle.Manifest, scope OwnerScope, status DestinationStatus) Comparison {
if status.StateErr != nil {
return Comparison{Outcome: OutcomeInvalidState, Reason: status.StateErr.Error()}
}
if status.SharedRoot != nil {
if err := ValidateSharedRoot(*status.SharedRoot); err != nil {
return Comparison{Outcome: OutcomeInvalidState, Reason: err.Error()}
}
owner, ok := status.SharedRoot.Owner(scope)
if !ok {
return Comparison{Outcome: OutcomeDestinationAbsent, Reason: fmt.Sprintf("destination owner %s/%s is absent", scope.PipelineID, scope.DestinationID)}
}
return compareManifests(source, owner.Source.Manifest)
}
if status.State != nil {
destinationState := *status.State
if err := Validate(destinationState); err != nil {
return Comparison{Outcome: OutcomeInvalidState, Reason: err.Error()}
}
if destinationState.PipelineID != scope.PipelineID {
return Comparison{Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("pipeline id %q does not match %q", destinationState.PipelineID, scope.PipelineID)}
}
if destinationState.DestinationID != scope.DestinationID {
return Comparison{Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("destination id %q does not match %q", destinationState.DestinationID, scope.DestinationID)}
}
return compareManifests(source, destinationState.Source.Manifest)
}
if status.HasContents {
return Comparison{Outcome: OutcomeDestinationUnmanaged, Reason: "destination has content but no distributor state"}
}
return Comparison{Outcome: OutcomeDestinationAbsent, Reason: "destination state is absent"}
}
func compareManifests(source, destination bundle.Manifest) Comparison {
if manifestsEqual(source, destination) {
return Comparison{Outcome: OutcomeSameSource, Reason: "destination source manifest matches source"}
}
if destination.ID != source.ID {
return Comparison{Outcome: OutcomeDifferentSourceConflict, Reason: "destination source id differs from source"}
}
if destination.Created.Before(source.Created) {
return Comparison{Outcome: OutcomeDestinationOlder, Reason: "destination source is older than source"}
}
if destination.Created.After(source.Created) {
return Comparison{Outcome: OutcomeDestinationNewer, Reason: "destination source is newer than source"}
}
if destination.Digest != source.Digest {
return Comparison{Outcome: OutcomeSameCreatedConflict, Reason: "destination source has same id and created time but different digest"}
}
return Comparison{Outcome: OutcomeInvalidState, Reason: "destination source differs from source without a supported comparison outcome"}
}
func Compare(source bundle.Manifest, pipelineID, destinationID string, status DestinationStatus) Comparison {
if status.StateErr != nil {
return Comparison{Outcome: OutcomeInvalidState, Reason: status.StateErr.Error()}
@@ -53,23 +106,7 @@ func Compare(source bundle.Manifest, pipelineID, destinationID string, status De
return Comparison{Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("destination id %q does not match %q", destinationState.DestinationID, destinationID)}
}
destinationManifest := destinationState.Source.Manifest
if manifestsEqual(source, destinationManifest) {
return Comparison{Outcome: OutcomeSameSource, Reason: "destination source manifest matches source"}
}
if destinationManifest.ID != source.ID {
return Comparison{Outcome: OutcomeDifferentSourceConflict, Reason: "destination source id differs from source"}
}
if destinationManifest.Created.Before(source.Created) {
return Comparison{Outcome: OutcomeDestinationOlder, Reason: "destination source is older than source"}
}
if destinationManifest.Created.After(source.Created) {
return Comparison{Outcome: OutcomeDestinationNewer, Reason: "destination source is newer than source"}
}
if destinationManifest.Digest != source.Digest {
return Comparison{Outcome: OutcomeSameCreatedConflict, Reason: "destination source has same id and created time but different digest"}
}
return Comparison{Outcome: OutcomeInvalidState, Reason: "destination source differs from source without a supported comparison outcome"}
return compareManifests(source, destinationState.Source.Manifest)
}
func manifestsEqual(a, b bundle.Manifest) bool {

View File

@@ -200,6 +200,38 @@ func TestSharedRootOwnerOutputHelpersRejectConflicts(t *testing.T) {
}
}
func TestCompareSharedRootOwnerScopesCurrentOwner(t *testing.T) {
state := validSharedRootState(t)
source := validManifest(t)
source.Created = source.Created.Add(time.Hour)
scope := CurrentOwnerScope("reports", "archive")
comparison := CompareSharedRootOwner(source, scope, DestinationStatus{SharedRoot: &state, HasContents: true})
if comparison.Outcome != OutcomeDestinationOlder {
t.Fatalf("comparison = %#v, want destination older for current owner", comparison)
}
missing := CompareSharedRootOwner(source, CurrentOwnerScope("missing", "archive"), DestinationStatus{SharedRoot: &state, HasContents: true})
if missing.Outcome != OutcomeDestinationAbsent {
t.Fatalf("missing owner comparison = %#v, want destination absent", missing)
}
}
func TestCompareSharedRootOwnerAcceptsMatchingSingleOwnerState(t *testing.T) {
singleOwner, err := Parse([]byte(validStateJSON(t)))
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
source := singleOwner.Source.Manifest
source.Created = source.Created.Add(time.Hour)
scope := CurrentOwnerScope(singleOwner.PipelineID, singleOwner.DestinationID)
comparison := CompareSharedRootOwner(source, scope, DestinationStatus{State: &singleOwner, HasContents: true})
if comparison.Outcome != OutcomeDestinationOlder {
t.Fatalf("comparison = %#v, want destination older", comparison)
}
}
func validSharedRootStateJSON(t *testing.T) string {
t.Helper()
data, err := json.MarshalIndent(validSharedRootState(t), "", " ")