Skip to content

Commit fb1e022

Browse files
Special case CRDs in dependency filter (#97)
Since it's expected they will be frequently skipped, cannot also skip future objects as well.
1 parent 28533ce commit fb1e022

2 files changed

Lines changed: 267 additions & 1 deletion

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// Copyright 2020 The Kubernetes Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package filters
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"sigs.k8s.io/cli-utils/pkg/apis/actuation"
12+
"sigs.k8s.io/cli-utils/pkg/apply/filter"
13+
"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
14+
"sigs.k8s.io/cli-utils/pkg/common"
15+
"sigs.k8s.io/cli-utils/pkg/object"
16+
)
17+
18+
//go:generate stringer -type=Relationship -linecomment
19+
type Relationship int
20+
21+
const (
22+
RelationshipDependent Relationship = iota // Dependent
23+
RelationshipDependency // Dependency
24+
)
25+
26+
//go:generate stringer -type=Phase -linecomment
27+
type Phase int
28+
29+
const (
30+
PhaseActuation Phase = iota // Actuation
31+
PhaseReconcile // Reconcile
32+
)
33+
34+
// DependencyFilter implements ValidationFilter interface to determine if an
35+
// object can be applied or deleted based on the status of it's dependencies.
36+
type DependencyFilter struct {
37+
TaskContext *taskrunner.TaskContext
38+
ActuationStrategy actuation.ActuationStrategy
39+
DryRunStrategy common.DryRunStrategy
40+
}
41+
42+
const DependencyFilterName = "DependencyFilter"
43+
44+
// Name returns the name of the filter for logs and events.
45+
func (dnrf DependencyFilter) Name() string {
46+
return DependencyFilterName
47+
}
48+
49+
// Filter returns an error if the specified object should be skipped because at
50+
// least one of its dependencies is Not Found or Not Reconciled.
51+
// Typed Errors:
52+
// - DependencyPreventedActuationError
53+
// - DependencyActuationMismatchError
54+
func (dnrf DependencyFilter) Filter(obj *unstructured.Unstructured) error {
55+
id := object.UnstructuredToObjMetadata(obj)
56+
57+
switch dnrf.ActuationStrategy {
58+
case actuation.ActuationStrategyApply:
59+
// For apply, check dependencies (outgoing)
60+
for _, depID := range dnrf.TaskContext.Graph().Dependencies(id) {
61+
err := dnrf.filterByRelationship(id, depID, filter.RelationshipDependency)
62+
if err != nil {
63+
return err
64+
}
65+
}
66+
case actuation.ActuationStrategyDelete:
67+
// For delete, check dependents (incoming)
68+
for _, depID := range dnrf.TaskContext.Graph().Dependents(id) {
69+
err := dnrf.filterByRelationship(id, depID, filter.RelationshipDependent)
70+
if err != nil {
71+
return err
72+
}
73+
}
74+
default:
75+
return filter.NewFatalError(fmt.Errorf("invalid actuation strategy: %q", dnrf.ActuationStrategy))
76+
}
77+
return nil
78+
}
79+
80+
func (dnrf DependencyFilter) filterByRelationship(aID, bID object.ObjMetadata, relationship filter.Relationship) error {
81+
// Dependency on an invalid object is considered an invalid dependency, making both objects invalid.
82+
// For applies: don't prematurely apply something that depends on something that hasn't been applied (because invalid).
83+
// For deletes: don't prematurely delete something that depends on something that hasn't been deleted (because invalid).
84+
// These can't be caught be subsequent checks, because invalid objects aren't in the inventory.
85+
if dnrf.TaskContext.IsInvalidObject(bID) {
86+
// Should have been caught in validation
87+
return filter.NewFatalError(fmt.Errorf("invalid %s: %s",
88+
strings.ToLower(relationship.String()),
89+
bID))
90+
}
91+
92+
status, found := dnrf.TaskContext.InventoryManager().ObjectStatus(bID)
93+
if !found {
94+
// Status is registered during planning.
95+
// So if status is not found, the object is external (NYI) or invalid.
96+
// Should have been caught in validation.
97+
return filter.NewFatalError(fmt.Errorf("unknown %s actuation strategy: %s",
98+
strings.ToLower(relationship.String()), bID))
99+
}
100+
101+
// Dependencies must have the same actuation strategy.
102+
// If there is a mismatch, skip both.
103+
if status.Strategy != dnrf.ActuationStrategy {
104+
// Skip!
105+
return &DependencyActuationMismatchError{
106+
Object: aID,
107+
Strategy: dnrf.ActuationStrategy,
108+
Relationship: relationship,
109+
Relation: bID,
110+
RelationStrategy: status.Strategy,
111+
}
112+
}
113+
114+
if status.Actuation == actuation.ActuationSkipped && bID.GroupKind.Kind == "CustomResourceDefinition" {
115+
return nil
116+
}
117+
118+
switch status.Actuation {
119+
case actuation.ActuationPending:
120+
// If actuation is still pending, dependency sorting is probably broken.
121+
return filter.NewFatalError(fmt.Errorf("premature %s: %s %s actuation %s: %s",
122+
strings.ToLower(dnrf.ActuationStrategy.String()),
123+
strings.ToLower(relationship.String()),
124+
strings.ToLower(status.Strategy.String()),
125+
strings.ToLower(status.Actuation.String()),
126+
bID))
127+
case actuation.ActuationSkipped, actuation.ActuationFailed:
128+
// Skip!
129+
return &DependencyPreventedActuationError{
130+
Object: aID,
131+
Strategy: dnrf.ActuationStrategy,
132+
Relationship: relationship,
133+
Relation: bID,
134+
RelationPhase: filter.PhaseActuation,
135+
RelationActuationStatus: status.Actuation,
136+
RelationReconcileStatus: status.Reconcile,
137+
}
138+
case actuation.ActuationSucceeded:
139+
// Don't skip!
140+
default:
141+
// Should never happen
142+
return filter.NewFatalError(fmt.Errorf("invalid %s actuation status %q: %s",
143+
strings.ToLower(relationship.String()),
144+
strings.ToLower(status.Actuation.String()),
145+
bID))
146+
}
147+
148+
// DryRun skips WaitTasks, so reconcile status can be ignored
149+
if dnrf.DryRunStrategy.ClientOrServerDryRun() {
150+
// Don't skip!
151+
return nil
152+
}
153+
154+
switch status.Reconcile {
155+
case actuation.ReconcilePending:
156+
// If reconcile is still pending, dependency sorting is probably broken.
157+
return filter.NewFatalError(fmt.Errorf("premature %s: %s %s reconcile %s: %s",
158+
strings.ToLower(dnrf.ActuationStrategy.String()),
159+
strings.ToLower(relationship.String()),
160+
strings.ToLower(status.Strategy.String()),
161+
strings.ToLower(status.Reconcile.String()),
162+
bID))
163+
case actuation.ReconcileSkipped, actuation.ReconcileFailed, actuation.ReconcileTimeout:
164+
// Skip!
165+
return &DependencyPreventedActuationError{
166+
Object: aID,
167+
Strategy: dnrf.ActuationStrategy,
168+
Relationship: relationship,
169+
Relation: bID,
170+
RelationPhase: filter.PhaseReconcile,
171+
RelationActuationStatus: status.Actuation,
172+
RelationReconcileStatus: status.Reconcile,
173+
}
174+
case actuation.ReconcileSucceeded:
175+
// Don't skip!
176+
default:
177+
// Should never happen
178+
return filter.NewFatalError(fmt.Errorf("invalid %s reconcile status %q: %s",
179+
strings.ToLower(relationship.String()),
180+
strings.ToLower(status.Reconcile.String()),
181+
bID))
182+
}
183+
184+
// Don't skip!
185+
return nil
186+
}
187+
188+
type DependencyPreventedActuationError struct {
189+
Object object.ObjMetadata
190+
Strategy actuation.ActuationStrategy
191+
Relationship filter.Relationship
192+
193+
Relation object.ObjMetadata
194+
RelationPhase filter.Phase
195+
RelationActuationStatus actuation.ActuationStatus
196+
RelationReconcileStatus actuation.ReconcileStatus
197+
}
198+
199+
func (e *DependencyPreventedActuationError) Error() string {
200+
switch e.RelationPhase {
201+
case filter.PhaseActuation:
202+
return fmt.Sprintf("%s %s %s %s: %s",
203+
strings.ToLower(e.Relationship.String()),
204+
strings.ToLower(e.Strategy.String()),
205+
strings.ToLower(e.RelationPhase.String()),
206+
strings.ToLower(e.RelationActuationStatus.String()),
207+
e.Relation)
208+
case filter.PhaseReconcile:
209+
return fmt.Sprintf("%s %s %s %s: %s",
210+
strings.ToLower(e.Relationship.String()),
211+
strings.ToLower(e.Strategy.String()),
212+
strings.ToLower(e.RelationPhase.String()),
213+
strings.ToLower(e.RelationReconcileStatus.String()),
214+
e.Relation)
215+
default:
216+
return fmt.Sprintf("invalid phase: %s", e.RelationPhase)
217+
}
218+
}
219+
220+
func (e *DependencyPreventedActuationError) Is(err error) bool {
221+
if err == nil {
222+
return false
223+
}
224+
tErr, ok := err.(*DependencyPreventedActuationError)
225+
if !ok {
226+
return false
227+
}
228+
return e.Object == tErr.Object &&
229+
e.Strategy == tErr.Strategy &&
230+
e.Relationship == tErr.Relationship &&
231+
e.Relation == tErr.Relation &&
232+
e.RelationPhase == tErr.RelationPhase &&
233+
e.RelationActuationStatus == tErr.RelationActuationStatus &&
234+
e.RelationReconcileStatus == tErr.RelationReconcileStatus
235+
}
236+
237+
type DependencyActuationMismatchError struct {
238+
Object object.ObjMetadata
239+
Strategy actuation.ActuationStrategy
240+
Relationship filter.Relationship
241+
242+
Relation object.ObjMetadata
243+
RelationStrategy actuation.ActuationStrategy
244+
}
245+
246+
func (e *DependencyActuationMismatchError) Error() string {
247+
return fmt.Sprintf("%s scheduled for %s: %s",
248+
strings.ToLower(e.Relationship.String()),
249+
strings.ToLower(e.RelationStrategy.String()),
250+
e.Relation)
251+
}
252+
253+
func (e *DependencyActuationMismatchError) Is(err error) bool {
254+
if err == nil {
255+
return false
256+
}
257+
tErr, ok := err.(*DependencyActuationMismatchError)
258+
if !ok {
259+
return false
260+
}
261+
return e.Object == tErr.Object &&
262+
e.Strategy == tErr.Strategy &&
263+
e.Relationship == tErr.Relationship &&
264+
e.Relation == tErr.Relation &&
265+
e.RelationStrategy == tErr.RelationStrategy
266+
}

pkg/applier/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec
7777
Inv: invInfo,
7878
InvPolicy: options.InventoryPolicy,
7979
},
80-
filter.DependencyFilter{
80+
filters.DependencyFilter{
8181
TaskContext: taskContext,
8282
ActuationStrategy: actuation.ActuationStrategyApply,
8383
DryRunStrategy: options.DryRunStrategy,

0 commit comments

Comments
 (0)