2026-05-31 02:13:58 +00:00
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 {
2026-06-19 15:28:03 +00:00
State * DistributorState
SharedRoot * SharedRootState
Catalog * CatalogState
SupersededLegacy * SupersededLegacyState
StateErr error
HasContents bool
2026-05-31 02:13:58 +00:00
}
type Comparison struct {
Outcome Outcome
Reason string
2026-06-18 15:09:18 +00:00
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
2026-05-31 02:13:58 +00:00
}
2026-06-08 18:53:22 +00:00
func CompareSharedRootOwner ( source bundle . Manifest , scope OwnerScope , status DestinationStatus ) Comparison {
if status . StateErr != nil {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : status . StateErr . Error ( ) , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-06-08 18:53:22 +00:00
}
if status . SharedRoot != nil {
if err := ValidateSharedRoot ( * status . SharedRoot ) ; err != nil {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : err . Error ( ) , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-06-08 18:53:22 +00:00
}
owner , ok := status . SharedRoot . Owner ( scope )
if ! ok {
2026-06-18 15:09:18 +00:00
return Comparison {
Outcome : OutcomeDestinationAbsent ,
Reason : fmt . Sprintf ( "destination owner %s/%s is absent" , scope . PipelineID , scope . DestinationID ) ,
Detail : ComparisonDetail {
Kind : ComparisonDetailSharedRootOwnerAbsent ,
CurrentOwner : scope ,
} ,
}
2026-06-08 18:53:22 +00:00
}
return compareManifests ( source , owner . Source . Manifest )
}
if status . State != nil {
destinationState := * status . State
if err := Validate ( destinationState ) ; err != nil {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : err . Error ( ) , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-06-08 18:53:22 +00:00
}
if destinationState . PipelineID != scope . PipelineID {
2026-06-18 15:09:18 +00:00
return pipelineIDMismatchComparison ( destinationState . PipelineID , scope . PipelineID )
2026-06-08 18:53:22 +00:00
}
if destinationState . DestinationID != scope . DestinationID {
2026-06-18 15:09:18 +00:00
return destinationIDMismatchComparison ( destinationState . DestinationID , scope . DestinationID )
2026-06-08 18:53:22 +00:00
}
return compareManifests ( source , destinationState . Source . Manifest )
}
if status . HasContents {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeDestinationUnmanaged , Reason : "destination has content but no distributor state" , Detail : ComparisonDetail { Kind : ComparisonDetailUnmanagedContent } }
2026-06-08 18:53:22 +00:00
}
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 {
2026-06-18 15:09:18 +00:00
return Comparison {
Outcome : OutcomeDifferentSourceConflict ,
Reason : "destination source id differs from source" ,
Detail : ComparisonDetail {
Kind : ComparisonDetailDifferentSourceID ,
CurrentSourceID : source . ID ,
DestinationSourceID : destination . ID ,
} ,
}
2026-06-08 18:53:22 +00:00
}
if destination . Created . Before ( source . Created ) {
return Comparison { Outcome : OutcomeDestinationOlder , Reason : "destination source is older than source" }
}
if destination . Created . After ( source . Created ) {
2026-06-18 15:09:18 +00:00
return Comparison {
Outcome : OutcomeDestinationNewer ,
Reason : "destination source is newer than source" ,
Detail : ComparisonDetail {
Kind : ComparisonDetailDestinationNewer ,
CurrentSourceID : source . ID ,
DestinationSourceID : destination . ID ,
} ,
}
2026-06-08 18:53:22 +00:00
}
if destination . Digest != source . Digest {
2026-06-18 15:09:18 +00:00
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 ,
} ,
}
2026-06-08 18:53:22 +00:00
}
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : "destination source differs from source without a supported comparison outcome" , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-06-08 18:53:22 +00:00
}
2026-05-31 02:13:58 +00:00
func Compare ( source bundle . Manifest , pipelineID , destinationID string , status DestinationStatus ) Comparison {
if status . StateErr != nil {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : status . StateErr . Error ( ) , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-05-31 02:13:58 +00:00
}
if status . State == nil {
if status . HasContents {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeDestinationUnmanaged , Reason : "destination has content but no distributor state" , Detail : ComparisonDetail { Kind : ComparisonDetailUnmanagedContent } }
2026-05-31 02:13:58 +00:00
}
return Comparison { Outcome : OutcomeDestinationAbsent , Reason : "destination state is absent" }
}
destinationState := * status . State
if err := Validate ( destinationState ) ; err != nil {
2026-06-18 15:09:18 +00:00
return Comparison { Outcome : OutcomeInvalidState , Reason : err . Error ( ) , Detail : ComparisonDetail { Kind : ComparisonDetailInvalidState } }
2026-05-31 02:13:58 +00:00
}
if destinationState . PipelineID != pipelineID {
2026-06-18 15:09:18 +00:00
return pipelineIDMismatchComparison ( destinationState . PipelineID , pipelineID )
2026-05-31 02:13:58 +00:00
}
if destinationState . DestinationID != destinationID {
2026-06-18 15:09:18 +00:00
return destinationIDMismatchComparison ( destinationState . DestinationID , destinationID )
2026-05-31 02:13:58 +00:00
}
2026-06-08 18:53:22 +00:00
return compareManifests ( source , destinationState . Source . Manifest )
2026-05-31 02:13:58 +00:00
}
2026-06-18 15:09:18 +00:00
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 ,
} ,
}
}
2026-05-31 02:13:58 +00:00
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
}