Preserve child identity in inheritance history
This commit is contained in:
@@ -44,6 +44,7 @@ type inheritanceOperation struct {
|
||||
templatePolicies collectionPolicyScope
|
||||
effectivePolicies []collectionPolicyScope
|
||||
sequences []sequenceHistoryContribution
|
||||
identityFields []string
|
||||
}
|
||||
|
||||
func cloneInheritancePolicyTemplates(templates []collectionPolicyScope) []collectionPolicyScope {
|
||||
@@ -107,6 +108,7 @@ func cloneInheritanceOperations(operations []inheritanceOperation) []inheritance
|
||||
cloned[index].templatePolicies = cloneCollectionPolicyScopeForInheritance(operation.templatePolicies)
|
||||
cloned[index].effectivePolicies = cloneInheritancePolicyTemplates(operation.effectivePolicies)
|
||||
cloned[index].sequences = cloneSequenceHistoryContributions(operation.sequences)
|
||||
cloned[index].identityFields = cloneStrings(operation.identityFields)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ func appendInheritanceExplanation(explanation *Explanation, working **document.N
|
||||
}
|
||||
before := valueSnapshot(*working, query, secrets)
|
||||
replaceHistoryPath(working, operation.transfer.to, operation.parent)
|
||||
// A record keeps its own identity throughout inheritance. Restore it
|
||||
// before taking enclosing snapshots as well as direct field snapshots.
|
||||
for _, field := range operation.identityFields {
|
||||
identity, _ := documentAtPath(operation.effective, Keys(field))
|
||||
replaceHistoryPath(working, operation.transfer.to.Key(field), identity)
|
||||
}
|
||||
afterParent := valueSnapshot(*working, query, secrets)
|
||||
parent, parentPresent := transferredOperationOrigin(operation, operationPathForQuery(operation.transfer.to, query))
|
||||
if pathsIntersect(operation.transfer.to, query) && parentPresent {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
@@ -114,7 +115,7 @@ func projectedInheritanceChanges(query Path, operation inheritanceOperation) []C
|
||||
return []Change{{Path: operation.control.path.clone(), Origin: origin,
|
||||
Dependencies: []Path{operation.control.path.clone()}}}
|
||||
}
|
||||
if operation.transfer == nil || !pathsIntersect(operation.transfer.to, query) {
|
||||
if operation.transfer == nil || !pathsIntersect(operation.transfer.to, query) || inheritanceOperationIdentity(operation, query) {
|
||||
return nil
|
||||
}
|
||||
path := query
|
||||
@@ -155,6 +156,9 @@ func projectedInheritanceChanges(query Path, operation inheritanceOperation) []C
|
||||
}
|
||||
|
||||
func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) {
|
||||
if inheritanceOperationIdentity(operation, destination) {
|
||||
return Origin{}, false
|
||||
}
|
||||
relative, ok := relativeHistoryPath(operation.transfer.to, destination)
|
||||
if !ok {
|
||||
return Origin{}, false
|
||||
@@ -168,3 +172,12 @@ func transferredOperationOrigin(operation inheritanceOperation, destination Path
|
||||
operation.transfer.from, operation.transfer.to, *operation.transfer)
|
||||
return transferred[destination.String()], true
|
||||
}
|
||||
|
||||
func inheritanceOperationIdentity(operation inheritanceOperation, path Path) bool {
|
||||
if operation.transfer == nil || len(operation.identityFields) == 0 {
|
||||
return false
|
||||
}
|
||||
relative, within := relativeHistoryPath(operation.transfer.to, path)
|
||||
return within && len(relative.segments) != 0 && relative.segments[0].kind != pathSegmentIndex &&
|
||||
slices.Contains(operation.identityFields, relative.segments[0].key)
|
||||
}
|
||||
|
||||
87
internal/engine/inheritance_identity_history_test.go
Normal file
87
internal/engine/inheritance_identity_history_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecordInheritanceHistoryPreservesChildIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, compound := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("compound=%t", compound), func(t *testing.T) {
|
||||
key, reference := "id", "{id: base}"
|
||||
identities := map[string]string{"id": "child"}
|
||||
if compound {
|
||||
key, reference = "[id, tenant]", "{id: base, tenant: parent-team}"
|
||||
identities["tenant"] = "child-team"
|
||||
}
|
||||
body := "configkit:\n sequences: [{destination: [items], key: " + key + "}]\nitems:\n - id: base\n tenant: parent-team\n value: inherited\n - id: child\n tenant: child-team\n extends: " + reference + "\n"
|
||||
loaded, err := testLoad(Bytes("records", []byte(body)), Options[map[string]any]{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
child := Keys("items").Index(1)
|
||||
for field, want := range identities {
|
||||
path := child.Key(field)
|
||||
info, found := loaded.Metadata().Lookup(path)
|
||||
if !found || !info.Explicit || len(info.Origin.Inheritance) != 0 {
|
||||
t.Fatalf("child identity provenance = %#v, found %t", info, found)
|
||||
}
|
||||
for _, change := range loaded.Metadata().History(path) {
|
||||
if change.Origin.Operation == ChangeInheritance {
|
||||
t.Fatalf("child identity received inheritance history: %#v", change)
|
||||
}
|
||||
}
|
||||
explanation, found := loaded.Explain(path)
|
||||
if !found {
|
||||
t.Fatalf("missing identity explanation at %s", path)
|
||||
}
|
||||
for _, change := range explanation.History {
|
||||
for _, snapshot := range []ValueSnapshot{change.Before, change.After} {
|
||||
if snapshot.Present && snapshot.YAML != want+"\n" {
|
||||
t.Fatalf("child identity snapshot = %q, want %q", snapshot.YAML, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, query := range []Path{child, Keys("items"), {}} {
|
||||
explanation, found := loaded.Explain(query)
|
||||
if !found {
|
||||
t.Fatalf("missing ancestor explanation at %s", query)
|
||||
}
|
||||
relative, _ := relativeHistoryPath(query, child)
|
||||
for _, change := range explanation.History {
|
||||
for _, snapshot := range []ValueSnapshot{change.Before, change.After} {
|
||||
if !snapshot.Present {
|
||||
continue
|
||||
}
|
||||
wrapped := "snapshot:\n " + strings.ReplaceAll(snapshot.YAML, "\n", "\n ") + "\n"
|
||||
root := parseItemPolicyDocument(t, "history", wrapped)
|
||||
for field, want := range identities {
|
||||
value, present := documentAtPath(root, appendDestination(Keys("snapshot"), relative.Key(field)))
|
||||
if present && value.Text != want {
|
||||
t.Fatalf("ancestor %s snapshot has child %s = %q", query, field, value.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
payload, found := loaded.Explain(child.Key("value"))
|
||||
if !found || payload.Effective.YAML != "inherited\n" {
|
||||
t.Fatalf("inherited payload = %#v, found %t", payload, found)
|
||||
}
|
||||
inherited := false
|
||||
for _, change := range payload.History {
|
||||
inherited = inherited || change.Origin.Operation == ChangeInheritance
|
||||
}
|
||||
if !inherited {
|
||||
t.Fatal("ordinary inherited payload lost its history")
|
||||
}
|
||||
parent, found := loaded.Explain(Keys("items").Index(0).Key("id"))
|
||||
if !found || parent.Effective.YAML != "base\n" || len(parent.History) == 0 {
|
||||
t.Fatalf("parent identity history = %#v, found %t", parent, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,12 @@ func applyInheritanceScopeTemplates(adapter inheritanceScopeAdapter, order []int
|
||||
parentPolicyScope, replaced = lowerCollectionPoliciesForClaims(parentPolicyScope, claims.ordered, declarations, adapter.applicationPolicy())
|
||||
}
|
||||
merged := inheritanceParentPayload(parent, node.identityFields)
|
||||
historyParent := parent
|
||||
if len(node.identityFields) != 0 {
|
||||
// The retained parent snapshot is the inherited payload, before
|
||||
// child fields are applied, with parent identity excluded.
|
||||
historyParent = document.Clone(merged)
|
||||
}
|
||||
mergedPolicyScope, policyDiagnostics := mergeInheritanceCollectionPolicies(
|
||||
parentPolicyScope, childPolicyScope, node.path, parent, payload, adapter.applicationPolicy(), replaced,
|
||||
)
|
||||
@@ -316,12 +322,13 @@ func applyInheritanceScopeTemplates(adapter inheritanceScopeAdapter, order []int
|
||||
resolvedPolicies[index] = inheritancePolicyScopeAtPath(mergedPolicyScope, node.path)
|
||||
result.transfers = append(result.transfers, transfer)
|
||||
result.operations = append(result.operations, inheritanceOperation{
|
||||
transfer: &transfer, parent: parent, effective: merged,
|
||||
transfer: &transfer, parent: historyParent, effective: merged,
|
||||
parentOrigins: resolvedOrigins[node.parent], effectiveOrigins: mergedOrigins,
|
||||
parentPolicies: cloneInheritancePolicyTemplates(parentPolicies),
|
||||
templatePolicies: cloneCollectionPolicyScopeForInheritance(templatePolicies),
|
||||
effectivePolicies: cloneInheritancePolicyTemplates(effectivePolicies),
|
||||
sequences: cloneSequenceHistoryContributions(sequenceContributions),
|
||||
identityFields: cloneStrings(node.identityFields),
|
||||
})
|
||||
resolvedPolicyTemplates[index] = effectivePolicies
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user