diff --git a/apis/common/v1/resource.go b/apis/common/v1/resource.go index bbefdbd..4a89432 100644 --- a/apis/common/v1/resource.go +++ b/apis/common/v1/resource.go @@ -77,10 +77,27 @@ type SecretKeySelector struct { Key string `json:"key"` } +// A ReferenceResolutionPolicy is a value for resolution policy for reference. +type ReferenceResolutionPolicy string + +const ( + // ReferencePolicyOptional is a resolution option. + // When the ReferenceResolutionPolicy is set to ReferencePolicyOptional the + // execution could continue even if the reference cannot be resolved. + ReferencePolicyOptional ReferenceResolutionPolicy = "Optional" +) + // A Reference to a named object. type Reference struct { // Name of the referenced object. Name string `json:"name"` + // Policy of the referenced object. + Policy ReferenceResolutionPolicy `json:"policy,omitempty"` +} + +// IsReferenceResolutionPolicyOptional checks whether the resolution policy of relevant reference is Optional. +func (in *Reference) IsReferenceResolutionPolicyOptional() bool { + return in.Policy == ReferencePolicyOptional } // A TypedReference refers to an object by Name, Kind, and APIVersion. It is diff --git a/pkg/reference/reference.go b/pkg/reference/reference.go index 06396e8..ea032a1 100644 --- a/pkg/reference/reference.go +++ b/pkg/reference/reference.go @@ -182,9 +182,9 @@ func (rr MultiResolutionResponse) Validate() error { return errors.New(errNoMatches) } - for _, v := range rr.ResolvedValues { + for i, v := range rr.ResolvedValues { if v == "" { - return errors.New(errNoValue) + return getReferenceError(&rr.ResolvedReferences[i], errors.New(errNoValue)) } } @@ -216,11 +216,11 @@ func (r *APIResolver) Resolve(ctx context.Context, req ResolutionRequest) (Resol // The reference is already set - resolve it. if req.Reference != nil { if err := r.client.Get(ctx, types.NamespacedName{Name: req.Reference.Name}, req.To.Managed); err != nil { - return ResolutionResponse{}, errors.Wrap(err, errGetManaged) + return ResolutionResponse{}, getReferenceError(req.Reference, errors.Wrap(err, errGetManaged)) } rsp := ResolutionResponse{ResolvedValue: req.Extract(req.To.Managed), ResolvedReference: req.Reference} - return rsp, rsp.Validate() + return rsp, getReferenceError(req.Reference, rsp.Validate()) } // The reference was not set, but a selector was. Select a reference. @@ -255,7 +255,7 @@ func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRe vals := make([]string, len(req.References)) for i := range req.References { if err := r.client.Get(ctx, types.NamespacedName{Name: req.References[i].Name}, req.To.Managed); err != nil { - return MultiResolutionResponse{}, errors.Wrap(err, errGetManaged) + return MultiResolutionResponse{}, getReferenceError(&req.References[i], errors.Wrap(err, errGetManaged)) } vals[i] = req.Extract(req.To.Managed) } @@ -285,6 +285,13 @@ func (r *APIResolver) ResolveMultiple(ctx context.Context, req MultiResolutionRe return rsp, rsp.Validate() } +func getReferenceError(ref *xpv1.Reference, err error) error { + if !ref.IsReferenceResolutionPolicyOptional() { + return err + } + return nil +} + // ControllersMustMatch returns true if the supplied Selector requires that a // reference be to a managed resource whose controller reference matches the // referencing resource. diff --git a/pkg/reference/reference_test.go b/pkg/reference/reference_test.go index ec03413..a105747 100644 --- a/pkg/reference/reference_test.go +++ b/pkg/reference/reference_test.go @@ -91,6 +91,7 @@ func TestResolve(t *testing.T) { now := metav1.Now() value := "coolv" ref := &xpv1.Reference{Name: "cool"} + optionalRef := &xpv1.Reference{Name: "cool", Policy: xpv1.ReferencePolicyOptional} controlled := &fake.Managed{} controlled.SetName(value) @@ -204,6 +205,26 @@ func TestResolve(t *testing.T) { }, }, }, + "OptionalPolicy": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: ResolutionRequest{ + Reference: optionalRef, + To: To{Managed: &fake.Managed{}}, + Extract: func(resource.Managed) string { return "" }, + }, + }, + want: want{ + rsp: ResolutionResponse{ + ResolvedReference: optionalRef, + }, + err: nil, + }, + }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{ @@ -282,6 +303,7 @@ func TestResolveMultiple(t *testing.T) { now := metav1.Now() value := "coolv" ref := xpv1.Reference{Name: "cool"} + optionalRef := xpv1.Reference{Name: "cool", Policy: xpv1.ReferencePolicyOptional} controlled := &fake.Managed{} controlled.SetName(value) @@ -396,6 +418,27 @@ func TestResolveMultiple(t *testing.T) { }, }, }, + "OptionalPolicy": { + reason: "No error should be returned when the resolution policy is Optional", + c: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + from: &fake.Managed{}, + args: args{ + req: MultiResolutionRequest{ + References: []xpv1.Reference{optionalRef}, + To: To{Managed: &fake.Managed{}}, + Extract: func(resource.Managed) string { return "" }, + }, + }, + want: want{ + rsp: MultiResolutionResponse{ + ResolvedValues: []string{""}, + ResolvedReferences: []xpv1.Reference{optionalRef}, + }, + err: nil, + }, + }, "ListError": { reason: "Should return errors encountered while listing potential referenced resources", c: &test.MockClient{