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 Detail ComparisonDetail } type ComparisonDetailKind string const ( ComparisonDetailNone ComparisonDetailKind = "" ComparisonDetailInvalidState ComparisonDetailKind = "invalid_state" ComparisonDetailUnmanagedContent ComparisonDetailKind = "unmanaged_content" ComparisonDetailPipelineIDMismatch ComparisonDetailKind = "pipeline_id_mismatch" ComparisonDetailDestinationIDMismatch ComparisonDetailKind = "destination_id_mismatch" ComparisonDetailDifferentSourceID ComparisonDetailKind = "different_source_id" ComparisonDetailSameCreatedDigestConflict ComparisonDetailKind = "same_created_digest_conflict" ComparisonDetailDestinationNewer ComparisonDetailKind = "destination_newer" ComparisonDetailSharedRootOwnerAbsent ComparisonDetailKind = "shared_root_owner_absent" ComparisonDetailSharedRootOutputOwner ComparisonDetailKind = "shared_root_output_owner_conflict" ) type ComparisonDetail struct { Kind ComparisonDetailKind CurrentPipelineID string CurrentDestinationID string DestinationPipelineID string DestinationDestinationID string CurrentSourceID string DestinationSourceID string CurrentSourceDigest string DestinationSourceDigest string Path string CurrentOwner OwnerScope ConflictingOwner OwnerScope } func CompareSharedRootOwner(source bundle.Manifest, scope OwnerScope, status DestinationStatus) Comparison { if status.StateErr != nil { return Comparison{Outcome: OutcomeInvalidState, Reason: status.StateErr.Error(), Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } if status.SharedRoot != nil { if err := ValidateSharedRoot(*status.SharedRoot); err != nil { return Comparison{Outcome: OutcomeInvalidState, Reason: err.Error(), Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } 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), Detail: ComparisonDetail{ Kind: ComparisonDetailSharedRootOwnerAbsent, CurrentOwner: scope, }, } } 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(), Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } if destinationState.PipelineID != scope.PipelineID { return pipelineIDMismatchComparison(destinationState.PipelineID, scope.PipelineID) } if destinationState.DestinationID != scope.DestinationID { return destinationIDMismatchComparison(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", Detail: ComparisonDetail{Kind: ComparisonDetailUnmanagedContent}} } 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", Detail: ComparisonDetail{ Kind: ComparisonDetailDifferentSourceID, CurrentSourceID: source.ID, DestinationSourceID: destination.ID, }, } } 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", Detail: ComparisonDetail{ Kind: ComparisonDetailDestinationNewer, CurrentSourceID: source.ID, DestinationSourceID: destination.ID, }, } } if destination.Digest != source.Digest { return Comparison{ Outcome: OutcomeSameCreatedConflict, Reason: "destination source has same id and created time but different digest", Detail: ComparisonDetail{ Kind: ComparisonDetailSameCreatedDigestConflict, CurrentSourceID: source.ID, DestinationSourceID: destination.ID, CurrentSourceDigest: source.Digest, DestinationSourceDigest: destination.Digest, }, } } return Comparison{Outcome: OutcomeInvalidState, Reason: "destination source differs from source without a supported comparison outcome", Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } func Compare(source bundle.Manifest, pipelineID, destinationID string, status DestinationStatus) Comparison { if status.StateErr != nil { return Comparison{Outcome: OutcomeInvalidState, Reason: status.StateErr.Error(), Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } if status.State == nil { if status.HasContents { return Comparison{Outcome: OutcomeDestinationUnmanaged, Reason: "destination has content but no distributor state", Detail: ComparisonDetail{Kind: ComparisonDetailUnmanagedContent}} } 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(), Detail: ComparisonDetail{Kind: ComparisonDetailInvalidState}} } if destinationState.PipelineID != pipelineID { return pipelineIDMismatchComparison(destinationState.PipelineID, pipelineID) } if destinationState.DestinationID != destinationID { return destinationIDMismatchComparison(destinationState.DestinationID, destinationID) } return compareManifests(source, destinationState.Source.Manifest) } func pipelineIDMismatchComparison(destinationPipelineID, currentPipelineID string) Comparison { return Comparison{ Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("pipeline id %q does not match %q", destinationPipelineID, currentPipelineID), Detail: ComparisonDetail{ Kind: ComparisonDetailPipelineIDMismatch, CurrentPipelineID: currentPipelineID, DestinationPipelineID: destinationPipelineID, }, } } func destinationIDMismatchComparison(destinationDestinationID, currentDestinationID string) Comparison { return Comparison{ Outcome: OutcomeIdentityMismatch, Reason: fmt.Sprintf("destination id %q does not match %q", destinationDestinationID, currentDestinationID), Detail: ComparisonDetail{ Kind: ComparisonDetailDestinationIDMismatch, CurrentDestinationID: currentDestinationID, DestinationDestinationID: destinationDestinationID, }, } } 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 }