406 lines
11 KiB
Go
406 lines
11 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
|
)
|
|
|
|
type OutputProjection struct {
|
|
Path string
|
|
Kind string
|
|
SourcePath string
|
|
Transform string
|
|
URL string
|
|
SHA256 string
|
|
Size int64
|
|
}
|
|
|
|
func FindOutputByPath(outputs []OutputFile, path string) (OutputFile, bool) {
|
|
for _, output := range outputs {
|
|
if output.Path == path {
|
|
return output, true
|
|
}
|
|
}
|
|
return OutputFile{}, false
|
|
}
|
|
|
|
func FindCatalogOutputByPath(outputs []CatalogOutputFile, path string) (CatalogOutputFile, bool) {
|
|
for _, output := range outputs {
|
|
if output.Path == path {
|
|
return output, true
|
|
}
|
|
}
|
|
return CatalogOutputFile{}, false
|
|
}
|
|
|
|
func MergeOutputFiles(retained, planned []OutputFile) ([]OutputFile, error) {
|
|
outputs := make([]OutputFile, 0, len(retained)+len(planned))
|
|
indexByPath := make(map[string]int, len(retained)+len(planned))
|
|
for _, output := range retained {
|
|
if _, exists := indexByPath[output.Path]; exists {
|
|
return nil, fmt.Errorf("state output path %q is duplicated", output.Path)
|
|
}
|
|
indexByPath[output.Path] = len(outputs)
|
|
outputs = append(outputs, output)
|
|
}
|
|
seenPlanned := make(map[string]struct{}, len(planned))
|
|
for _, output := range planned {
|
|
if _, exists := seenPlanned[output.Path]; exists {
|
|
return nil, fmt.Errorf("state output path %q is duplicated", output.Path)
|
|
}
|
|
seenPlanned[output.Path] = struct{}{}
|
|
if index, exists := indexByPath[output.Path]; exists {
|
|
outputs[index] = output
|
|
continue
|
|
}
|
|
indexByPath[output.Path] = len(outputs)
|
|
outputs = append(outputs, output)
|
|
}
|
|
return outputs, nil
|
|
}
|
|
|
|
func ManagedOutputPaths(s DistributorState) []string {
|
|
paths := make([]string, 0, len(s.Outputs))
|
|
for _, output := range s.Outputs {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func RemoveMissingOutputs(s DistributorState, missingPaths []string) (DistributorState, bool) {
|
|
if len(missingPaths) == 0 {
|
|
return s, false
|
|
}
|
|
missing := pathSet(missingPaths)
|
|
next := s
|
|
next.Outputs = make([]OutputFile, 0, len(s.Outputs))
|
|
changed := false
|
|
for _, output := range s.Outputs {
|
|
if _, remove := missing[output.Path]; remove {
|
|
changed = true
|
|
continue
|
|
}
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
return next, changed
|
|
}
|
|
|
|
func ProjectOutputs(outputs []OutputProjection, existing []OutputFile, now time.Time) []OutputFile {
|
|
now = now.UTC()
|
|
files := make([]OutputFile, 0, len(outputs))
|
|
for _, output := range outputs {
|
|
createdAt := now
|
|
if existingOutput, ok := FindOutputByPath(existing, output.Path); ok {
|
|
createdAt = existingOutput.CreatedAt
|
|
}
|
|
files = append(files, OutputFile{
|
|
Path: output.Path,
|
|
Kind: output.Kind,
|
|
SourcePath: output.SourcePath,
|
|
Transform: output.Transform,
|
|
URL: output.URL,
|
|
SHA256: output.SHA256,
|
|
Size: output.Size,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: now,
|
|
})
|
|
}
|
|
return files
|
|
}
|
|
|
|
func CurrentOwnerScope(pipelineID, destinationID string) OwnerScope {
|
|
return OwnerScope{PipelineID: pipelineID, DestinationID: destinationID}
|
|
}
|
|
|
|
func (s SharedRootState) Owner(scope OwnerScope) (OwnerRecord, bool) {
|
|
for _, owner := range s.Owners {
|
|
if owner.Scope == scope {
|
|
return owner, true
|
|
}
|
|
}
|
|
return OwnerRecord{}, false
|
|
}
|
|
|
|
func (s SharedRootState) SourceManifest(scope OwnerScope) (bundle.Manifest, bool) {
|
|
owner, ok := s.Owner(scope)
|
|
if !ok {
|
|
return bundle.Manifest{}, false
|
|
}
|
|
return owner.Source.Manifest, true
|
|
}
|
|
|
|
func (s SharedRootState) ManagedOutputPaths(scope OwnerScope) []string {
|
|
paths := make([]string, 0, len(s.Outputs))
|
|
for _, output := range s.Outputs {
|
|
if output.Owner == scope {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (s SharedRootState) AllManagedOutputPaths() []string {
|
|
paths := make([]string, 0, len(s.Outputs))
|
|
for _, output := range s.Outputs {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func RemoveMissingSharedRootOwnerOutputs(s SharedRootState, scope OwnerScope, missingPaths []string) (SharedRootState, bool) {
|
|
if len(missingPaths) == 0 {
|
|
return s, false
|
|
}
|
|
missing := pathSet(missingPaths)
|
|
next := s
|
|
next.Outputs = make([]SharedRootOutputFile, 0, len(s.Outputs))
|
|
changed := false
|
|
for _, output := range s.Outputs {
|
|
if output.Owner == scope {
|
|
if _, remove := missing[output.Path]; remove {
|
|
changed = true
|
|
continue
|
|
}
|
|
}
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
return next, changed
|
|
}
|
|
|
|
func RemoveMissingSharedRootOutputs(s SharedRootState, missingPaths []string) (SharedRootState, bool) {
|
|
if len(missingPaths) == 0 {
|
|
return s, false
|
|
}
|
|
missing := pathSet(missingPaths)
|
|
next := s
|
|
next.Outputs = make([]SharedRootOutputFile, 0, len(s.Outputs))
|
|
changed := false
|
|
for _, output := range s.Outputs {
|
|
if _, remove := missing[output.Path]; remove {
|
|
changed = true
|
|
continue
|
|
}
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
return next, changed
|
|
}
|
|
|
|
func CatalogOutputsForOwner(outputs []CatalogOutputFile, scope OwnerScope) []CatalogOutputFile {
|
|
selected := make([]CatalogOutputFile, 0, len(outputs))
|
|
for _, output := range outputs {
|
|
if output.PipelineID == scope.PipelineID && output.DestinationID == scope.DestinationID {
|
|
selected = append(selected, output)
|
|
}
|
|
}
|
|
return selected
|
|
}
|
|
|
|
func CatalogManagedOutputPaths(s CatalogState) []string {
|
|
paths := make([]string, 0, len(s.Outputs))
|
|
for _, output := range s.Outputs {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func RemoveMissingCatalogOwnerOutputs(s CatalogState, scope OwnerScope, missingPaths []string) (CatalogState, bool) {
|
|
if len(missingPaths) == 0 {
|
|
return s, false
|
|
}
|
|
missing := pathSet(missingPaths)
|
|
next := s
|
|
next.Outputs = make([]CatalogOutputFile, 0, len(s.Outputs))
|
|
changed := false
|
|
for _, output := range s.Outputs {
|
|
if output.PipelineID == scope.PipelineID && output.DestinationID == scope.DestinationID {
|
|
if _, remove := missing[output.Path]; remove {
|
|
changed = true
|
|
continue
|
|
}
|
|
}
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
return next, changed
|
|
}
|
|
|
|
func RemoveMissingCatalogOutputs(s CatalogState, missingPaths []string) (CatalogState, bool) {
|
|
if len(missingPaths) == 0 {
|
|
return s, false
|
|
}
|
|
missing := pathSet(missingPaths)
|
|
next := s
|
|
next.Outputs = make([]CatalogOutputFile, 0, len(s.Outputs))
|
|
changed := false
|
|
for _, output := range s.Outputs {
|
|
if _, remove := missing[output.Path]; remove {
|
|
changed = true
|
|
continue
|
|
}
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
return next, changed
|
|
}
|
|
|
|
func (s SharedRootState) OutputOwner(path string) (OwnerScope, bool) {
|
|
for _, output := range s.Outputs {
|
|
if output.Path == path {
|
|
return output.Owner, true
|
|
}
|
|
}
|
|
return OwnerScope{}, false
|
|
}
|
|
|
|
func (s SharedRootState) PathOwnershipConflict(scope OwnerScope, paths []string) (PathOwnershipConflict, bool) {
|
|
for _, path := range paths {
|
|
owner, exists := s.OutputOwner(path)
|
|
if exists && owner != scope {
|
|
return PathOwnershipConflict{
|
|
Path: path,
|
|
Owner: owner,
|
|
CurrentOwner: scope,
|
|
Detail: ComparisonDetail{
|
|
Kind: ComparisonDetailSharedRootOutputOwner,
|
|
Path: path,
|
|
CurrentOwner: scope,
|
|
ConflictingOwner: owner,
|
|
},
|
|
}, true
|
|
}
|
|
}
|
|
return PathOwnershipConflict{}, false
|
|
}
|
|
|
|
func ProjectSharedRootOutputs(outputs []OutputProjection, existing []SharedRootOutputFile, scope OwnerScope, source bundle.Manifest, now time.Time) []SharedRootOutputFile {
|
|
now = now.UTC()
|
|
files := make([]SharedRootOutputFile, 0, len(outputs))
|
|
for _, output := range outputs {
|
|
createdAt := now
|
|
if existingOutput, ok := findSharedRootOutput(existing, output.Path); ok && existingOutput.Owner == scope {
|
|
createdAt = existingOutput.CreatedAt
|
|
}
|
|
files = append(files, SharedRootOutputFile{
|
|
Path: output.Path,
|
|
Kind: output.Kind,
|
|
SourcePath: output.SourcePath,
|
|
Transform: output.Transform,
|
|
URL: output.URL,
|
|
SHA256: output.SHA256,
|
|
Size: output.Size,
|
|
Owner: scope,
|
|
SourceID: source.ID,
|
|
SourceDigest: source.Digest,
|
|
SourceCreated: source.Created,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: now,
|
|
})
|
|
}
|
|
return files
|
|
}
|
|
|
|
func ReplaceOwnerOutputs(s SharedRootState, scope OwnerScope, owner OwnerRecord, planned []SharedRootOutputFile) (SharedRootState, error) {
|
|
if conflict, ok := s.PathOwnershipConflict(scope, sharedRootOutputPaths(planned)); ok {
|
|
return SharedRootState{}, fmt.Errorf("state output path %q is owned by %s/%s", conflict.Path, conflict.Owner.PipelineID, conflict.Owner.DestinationID)
|
|
}
|
|
if err := validatePlannedSharedRootOutputs(scope, planned); err != nil {
|
|
return SharedRootState{}, err
|
|
}
|
|
next := s
|
|
next.Owners = upsertOwner(s.Owners, owner)
|
|
next.Outputs = make([]SharedRootOutputFile, 0, len(s.Outputs)+len(planned))
|
|
for _, output := range s.Outputs {
|
|
if output.Owner != scope {
|
|
next.Outputs = append(next.Outputs, output)
|
|
}
|
|
}
|
|
next.Outputs = append(next.Outputs, planned...)
|
|
return next, nil
|
|
}
|
|
|
|
func MergeOwnerOutputs(s SharedRootState, scope OwnerScope, owner OwnerRecord, planned []SharedRootOutputFile) (SharedRootState, error) {
|
|
if conflict, ok := s.PathOwnershipConflict(scope, sharedRootOutputPaths(planned)); ok {
|
|
return SharedRootState{}, fmt.Errorf("state output path %q is owned by %s/%s", conflict.Path, conflict.Owner.PipelineID, conflict.Owner.DestinationID)
|
|
}
|
|
if err := validatePlannedSharedRootOutputs(scope, planned); err != nil {
|
|
return SharedRootState{}, err
|
|
}
|
|
next := s
|
|
next.Owners = upsertOwner(s.Owners, owner)
|
|
outputs := make([]SharedRootOutputFile, 0, len(s.Outputs)+len(planned))
|
|
indexByPath := make(map[string]int, len(s.Outputs)+len(planned))
|
|
for _, output := range s.Outputs {
|
|
indexByPath[output.Path] = len(outputs)
|
|
outputs = append(outputs, output)
|
|
}
|
|
for _, output := range planned {
|
|
if index, exists := indexByPath[output.Path]; exists {
|
|
outputs[index] = output
|
|
continue
|
|
}
|
|
indexByPath[output.Path] = len(outputs)
|
|
outputs = append(outputs, output)
|
|
}
|
|
next.Outputs = outputs
|
|
return next, nil
|
|
}
|
|
|
|
func findSharedRootOutput(outputs []SharedRootOutputFile, path string) (SharedRootOutputFile, bool) {
|
|
for _, output := range outputs {
|
|
if output.Path == path {
|
|
return output, true
|
|
}
|
|
}
|
|
return SharedRootOutputFile{}, false
|
|
}
|
|
|
|
func sharedRootOutputPaths(outputs []SharedRootOutputFile) []string {
|
|
paths := make([]string, 0, len(outputs))
|
|
for _, output := range outputs {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func rejectDuplicateSharedRootOutputs(outputs []SharedRootOutputFile) error {
|
|
seen := make(map[string]struct{}, len(outputs))
|
|
for _, output := range outputs {
|
|
if _, exists := seen[output.Path]; exists {
|
|
return fmt.Errorf("state output path %q is duplicated", output.Path)
|
|
}
|
|
seen[output.Path] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePlannedSharedRootOutputs(scope OwnerScope, outputs []SharedRootOutputFile) error {
|
|
if err := rejectDuplicateSharedRootOutputs(outputs); err != nil {
|
|
return err
|
|
}
|
|
for _, output := range outputs {
|
|
if output.Owner != scope {
|
|
return fmt.Errorf("state output path %q is owned by %s/%s, not %s/%s", output.Path, output.Owner.PipelineID, output.Owner.DestinationID, scope.PipelineID, scope.DestinationID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func upsertOwner(owners []OwnerRecord, owner OwnerRecord) []OwnerRecord {
|
|
next := append([]OwnerRecord(nil), owners...)
|
|
for index, existing := range next {
|
|
if existing.Scope == owner.Scope {
|
|
next[index] = owner
|
|
return next
|
|
}
|
|
}
|
|
return append(next, owner)
|
|
}
|
|
|
|
func pathSet(paths []string) map[string]struct{} {
|
|
set := make(map[string]struct{}, len(paths))
|
|
for _, path := range paths {
|
|
set[path] = struct{}{}
|
|
}
|
|
return set
|
|
}
|