Files
distributor/internal/state/compare.go

127 lines
4.9 KiB
Go

package state
import (
"fmt"
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
)
type Outcome string
const (
OutcomeDestinationAbsent Outcome = "destination_absent"
OutcomeDestinationUnmanaged Outcome = "destination_unmanaged"
OutcomeInvalidState Outcome = "invalid_destination_state"
OutcomeIdentityMismatch Outcome = "destination_identity_mismatch"
OutcomeSameSource Outcome = "same_source_manifest"
OutcomeDestinationOlder Outcome = "destination_older"
OutcomeDestinationNewer Outcome = "destination_newer"
OutcomeSameCreatedConflict Outcome = "same_created_digest_conflict"
OutcomeDifferentSourceConflict Outcome = "different_source_conflict"
)
type DestinationStatus struct {
State *DistributorState
SharedRoot *SharedRootState
StateErr error
HasContents bool
}
type Comparison struct {
Outcome Outcome
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()}
}
if status.State == nil {
if status.HasContents {
return Comparison{Outcome: OutcomeDestinationUnmanaged, Reason: "destination has content but no distributor state"}
}
return Comparison{Outcome: OutcomeDestinationAbsent, Reason: "destination state is absent"}
}
destinationState := *status.State
if err := Validate(destinationState); err != nil {
return Comparison{Outcome: OutcomeInvalidState, Reason: err.Error()}
}
if destinationState.PipelineID != pipelineID {
return Comparison{Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("pipeline id %q does not match %q", destinationState.PipelineID, pipelineID)}
}
if destinationState.DestinationID != destinationID {
return Comparison{Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("destination id %q does not match %q", destinationState.DestinationID, destinationID)}
}
return compareManifests(source, destinationState.Source.Manifest)
}
func manifestsEqual(a, b bundle.Manifest) bool {
if a.SchemaVersion != b.SchemaVersion ||
a.ID != b.ID ||
a.Digest != b.Digest ||
!a.Created.Equal(b.Created) ||
len(a.Files) != len(b.Files) {
return false
}
for index := range a.Files {
if a.Files[index] != b.Files[index] {
return false
}
}
return true
}