Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions bundle/direct/bundle_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,8 @@ func shouldSkipNormalized(cfg *dresources.ResourceLifecycleConfig, path *structp
// reports an update, and so does a field the user removed from config (Old != nil) — that
// is a deletion the user asked for, not a backend addition.
//
// The rule is gated on a field within the same object (ignore_remote_additions.when_set).
// The rule may be gated on a field within the same object (ignore_remote_additions.when_set);
// when when_set is omitted the rule always applies to a declared object.
// For cluster specs that gate is policy_id: an attached cluster policy supplies values
// server-side — "fixed" elements always, "defaultValue" elements when the request sets
// apply_policy_default_values — so the remote spec is legitimately a superset of what the
Expand Down Expand Up @@ -729,9 +730,13 @@ func shouldSkipRemoteAddition(cfg *dresources.ResourceLifecycleConfig, path *str
// are validated against the state type by TestResourcesYMLRemoteAdditionGates.
continue
}
value, err := structaccess.Get(object, rule.WhenSet)
if err != nil || allEmpty(value) {
continue
// A nil gate (when_set omitted) always applies to a declared object; otherwise the
// rule fires only when the gate field is set.
if rule.WhenSet != nil {
value, err := structaccess.Get(object, rule.WhenSet)
if err != nil || allEmpty(value) {
continue
}
}
return deployplan.ReasonRemoteAddition, true
}
Expand Down
58 changes: 58 additions & 0 deletions bundle/direct/bundle_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,64 @@ func TestShouldSkipRemoteAddition(t *testing.T) {
}
}

func TestShouldSkipRemoteAdditionNoGate(t *testing.T) {
// A rule with when_set omitted always applies to a declared object.
jobCluster, err := structpath.ParsePattern("job_clusters[*].new_cluster")
require.NoError(t, err)
cfg := &dresources.ResourceLifecycleConfig{
IgnoreRemoteAdditions: []dresources.RemoteAdditionRule{
{Field: jobCluster},
},
}

state := &jobs.JobSettings{JobClusters: []jobs.JobCluster{{
JobClusterKey: "small",
NewCluster: &compute.ClusterSpec{},
}}}

tests := []struct {
name string
path string
change deployplan.ChangeDesc
expected bool
}{
{
name: "no gate: any addition inside a declared object is skipped",
path: "job_clusters[job_cluster_key='small'].new_cluster.custom_tags['CostCenter']",
change: deployplan.ChangeDesc{Remote: "dev-1234"},
expected: true,
},
{
name: "config disagrees with remote: still an update",
path: "job_clusters[job_cluster_key='small'].new_cluster.custom_tags['CostCenter']",
change: deployplan.ChangeDesc{New: "mine", Remote: "dev-1234"},
expected: false,
},
{
// The object itself is absent from config, so the addition is real drift
// even without a gate.
name: "gated object absent from config: still drift",
path: "job_clusters[job_cluster_key='other'].new_cluster.custom_tags['CostCenter']",
change: deployplan.ChangeDesc{Remote: "dev-1234"},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := structpath.ParsePath(tt.path)
require.NoError(t, err)

change := tt.change
reason, ok := shouldSkipRemoteAddition(cfg, path, &change, state)
assert.Equal(t, tt.expected, ok)
if tt.expected {
assert.Equal(t, deployplan.ReasonRemoteAddition, reason)
}
})
}
}

// Types for TestPrepareChangesWholeBlockOverlap: two levels of nesting under an
// optional pointer.
type threeWayInner struct {
Expand Down
13 changes: 7 additions & 6 deletions bundle/direct/dresources/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ func (b *BackendDefaultRule) UnmarshalYAML(unmarshal func(any) error) error {
return nil
}

// RemoteAdditionRule marks a sub-object whose contents the backend co-owns whenever the
// object's WhenSet field is set. Inside such an object, a field the config never declared
// coming back set from the remote is an addition by the backend, not drift.
// RemoteAdditionRule marks a sub-object whose contents the backend co-owns. Inside such an
// object, a field the config never declared coming back set from the remote is an addition
// by the backend, not drift.
//
// Field is a prefix pattern selecting the object (omitted = the resource root); WhenSet is a
// path within that object, relative to it, whose value gates the rule.
// Field is a prefix pattern selecting the object (omitted = the resource root). WhenSet is a
// path within that object, relative to it, whose value gates the rule; when omitted the rule
// always applies to a declared object.
type RemoteAdditionRule struct {
Field *structpath.PatternNode `yaml:"field"`
WhenSet *structpath.PathNode `yaml:"when_set"`
WhenSet *structpath.PathNode `yaml:"when_set,omitempty"`
}

// ResourceLifecycleConfig defines lifecycle behavior for a resource type.
Expand Down
13 changes: 7 additions & 6 deletions bundle/direct/dresources/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,18 @@ func TestResourcesYMLRemoteAdditionGates(t *testing.T) {

for _, rule := range rc.IgnoreRemoteAdditions {
field := rule.Field
require.False(t, rule.WhenSet.IsRoot(),
"%s: ignore_remote_additions entry %q needs a when_set", resourceType, field.String())

if !field.IsRoot() {
assert.NoError(t, structaccess.ValidatePattern(adapter.StateType(), field),
"%s: ignore_remote_additions field %q does not resolve in the state type", resourceType, field.String())
}
gate, err := structpath.ParsePattern(joinPattern(field, rule.WhenSet.String()))
require.NoError(t, err)
assert.NoError(t, structaccess.ValidatePattern(adapter.StateType(), gate),
"%s: ignore_remote_additions when_set %q does not resolve under %q", resourceType, rule.WhenSet, field.String())
// when_set is optional; a nil gate means the rule always applies.
if rule.WhenSet != nil {
gate, err := structpath.ParsePattern(joinPattern(field, rule.WhenSet.String()))
require.NoError(t, err)
assert.NoError(t, structaccess.ValidatePattern(adapter.StateType(), gate),
"%s: ignore_remote_additions when_set %q does not resolve under %q", resourceType, rule.WhenSet, field.String())
}
}
}
}
Expand Down
Loading