Skip to content

Commit 6bae35a

Browse files
committed
Don't delete resources when required field paths don't exist
If we hit a composition error that is likely due to a misconfigured composition we just return a fatal error. Previously we'd skip the affected composed resource, which could result in it being deleted if it already existed. We'll now only skip rendering a composed resource if a required field path is not present _and_ the composed resource doesn't exist yet. Signed-off-by: Nic Cope <nicc@rk0n.org>
1 parent 27b41e8 commit 6bae35a

File tree

8 files changed

+513
-851
lines changed

8 files changed

+513
-851
lines changed

fn.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
88

99
"github.com/crossplane/crossplane-runtime/pkg/errors"
10+
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
1011
"github.com/crossplane/crossplane-runtime/pkg/logging"
1112
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
1213

@@ -114,10 +115,14 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe
114115
}
115116

116117
if input.Environment != nil {
117-
// Run all patches that are from the (observed) XR to the environment or from the environment to the (desired) XR.
118-
if err := RenderEnvironmentPatches(env, oxr.Resource, dxr.Resource, input.Environment.Patches); err != nil {
119-
response.Fatal(rsp, errors.Wrapf(err, "cannot render ToEnvironment patches from the composite resource"))
120-
return rsp, nil
118+
// Run all patches that are from the (observed) XR to the environment or
119+
// from the environment to the (desired) XR.
120+
for i := range input.Environment.Patches {
121+
p := &input.Environment.Patches[i]
122+
if err := ApplyEnvironmentPatch(p, env, oxr.Resource, dxr.Resource); err != nil {
123+
response.Fatal(rsp, errors.Wrapf(err, "cannot apply the %q environment patch at index %d", p.GetType(), i))
124+
return rsp, nil
125+
}
121126
}
122127
}
123128

@@ -155,8 +160,8 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe
155160
}
156161
}
157162

158-
ocd, ok := observed[resource.Name(t.Name)]
159-
if ok {
163+
ocd, exists := observed[resource.Name(t.Name)]
164+
if exists {
160165
existing++
161166
log.Debug("Resource template corresponds to existing composed resource", "metadata-name", ocd.Resource.GetName())
162167

@@ -192,10 +197,53 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe
192197
"name", ocd.Resource.GetName())
193198
}
194199

195-
// TODO(negz): No need for multiple errors?
196-
if err := RenderComposedPatches(ocd.Resource, dcd.Resource, oxr.Resource, dxr.Resource, env, t.Patches); err != nil {
197-
response.Fatal(rsp, errors.Wrapf(err, "cannot render patches for composed resource %q", t.Name))
198-
return rsp, nil
200+
// Run all patches that are to a desired composed resource, or from an
201+
// observed composed resource.
202+
skip := false
203+
for i := range t.Patches {
204+
p := &t.Patches[i]
205+
if err := ApplyComposedPatch(p, ocd.Resource, dcd.Resource, oxr.Resource, dxr.Resource, env); err != nil {
206+
if fieldpath.IsNotFound(err) {
207+
// This is a patch from a required field path that does not
208+
// exist. The point of FromFieldPathPolicyRequired is to
209+
// block creation of the new 'to' resource until the 'from'
210+
// field path exists.
211+
//
212+
// The only kind of resource we could be patching to that
213+
// might not exist at this point is a composed resource. So
214+
// if we're patching to a composed resource that doesn't
215+
// exist we want to avoid creating it. Otherwise, we just
216+
// treat the patch from a required field path the same way
217+
// we'd treat a patch from an optional field path and skip
218+
// it.
219+
if p.GetPolicy().GetFromFieldPathPolicy() == v1beta1.FromFieldPathPolicyRequired {
220+
if ToComposedResource(p) && !exists {
221+
response.Warning(rsp, errors.Wrapf(err, "not adding new composed resource %q to desired state because %q patch at index %d has 'policy.fromFieldPath: Required'", t.Name, p.GetType(), i))
222+
223+
// There's no point processing further patches.
224+
// They'll either be from an observed composed
225+
// resource that doesn't exist yet, or to a desired
226+
// composed resource that we'll discard.
227+
skip = true
228+
break
229+
}
230+
response.Warning(rsp, errors.Wrapf(err, "cannot render composed resource %q %q patch at index %d: ignoring 'policy.fromFieldPath: Required' because 'to' resource already exists", t.Name, p.GetType(), i))
231+
}
232+
233+
// If any optional field path isn't found we just skip this
234+
// patch and move on. The path may be populated by a
235+
// subsequent patch.
236+
continue
237+
}
238+
response.Fatal(rsp, errors.Wrapf(err, "cannot render composed resource %q %q patch at index %d", t.Name, p.GetType(), i))
239+
return rsp, nil
240+
}
241+
}
242+
243+
// Skip adding this resource to the desired state because it doesn't
244+
// exist yet, and a required FromFieldPath was not (yet) found.
245+
if skip {
246+
continue
199247
}
200248

201249
desired[resource.Name(t.Name)] = dcd

fn_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ func TestRunFunction(t *testing.T) {
385385
},
386386
},
387387
"FailedPatchNotSaved": {
388-
reason: "If we fail to patch a desired resource produced by a previous Function in the pipeline we should return a warning result, and leave the original desired resource untouched.",
388+
reason: "If we fail to patch a desired resource produced by a previous Function in the pipeline we should return a fatal result.",
389389
args: args{
390390
req: &fnv1beta1.RunFunctionRequest{
391391
Input: resource.MustStructObject(&v1beta1.Resources{
@@ -451,7 +451,7 @@ func TestRunFunction(t *testing.T) {
451451
Results: []*fnv1beta1.Result{
452452
{
453453
Severity: fnv1beta1.Severity_SEVERITY_FATAL,
454-
Message: fmt.Sprintf("cannot render patches for composed resource %q: cannot apply the %q patch at index 1: spec.widgets: not an array", "cool-resource", "FromCompositeFieldPath"),
454+
Message: fmt.Sprintf("cannot render composed resource %q %q patch at index 1: spec.widgets: not an array", "cool-resource", "FromCompositeFieldPath"),
455455
},
456456
},
457457
},

input/v1beta1/resources_patches.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ const (
3636
type PatchPolicy struct {
3737
// FromFieldPath specifies how to patch from a field path. The default is
3838
// 'Optional', which means the patch will be a no-op if the specified
39-
// fromFieldPath does not exist. Use 'Required' if the patch should fail if
40-
// the specified path does not exist.
39+
// fromFieldPath does not exist. Use 'Required' to prevent the creation of a
40+
// new composed resource until the required path exists.
4141
// +kubebuilder:validation:Enum=Optional;Required
4242
// +optional
4343
FromFieldPath *FromFieldPathPolicy `json:"fromFieldPath,omitempty"`

0 commit comments

Comments
 (0)