crossplane-runtime/pkg/resource/reference_resolver.go

259 lines
8.2 KiB
Go

/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"fmt"
"reflect"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Error strings
const (
errGetReferencerStatus = "could not get referenced resource status"
errUpdateReferencer = "could not update resource referencer"
errBuildAttribute = "could not build the attribute"
errAssignAttribute = "could not assign the attribute"
)
// ReferenceStatusType is an enum type for the possible values for a Reference Status
type ReferenceStatusType int
const (
// ReferenceStatusUnknown is the default value
ReferenceStatusUnknown ReferenceStatusType = iota
// ReferenceNotFound shows that the reference is not found
ReferenceNotFound
// ReferenceNotReady shows that the reference is not ready
ReferenceNotReady
// ReferenceReady shows that the reference is ready
ReferenceReady
)
func (t ReferenceStatusType) String() string {
return []string{"Unknown", "NotFound", "NotReady", "Ready"}[t]
}
// ReferenceStatus has the name and status of a reference
type ReferenceStatus struct {
Name string
Status ReferenceStatusType
}
func (r ReferenceStatus) String() string {
return fmt.Sprintf("{reference:%s status:%s}", r.Name, r.Status)
}
// referencesAccessErr is used to indicate that one or more references can not
// be accessed
type referencesAccessErr struct {
statuses []ReferenceStatus
}
// newReferenceAccessErr returns a referencesAccessErr if any of the given
// references are not ready
func newReferenceAccessErr(statuses []ReferenceStatus) error {
for _, st := range statuses {
if st.Status != ReferenceReady {
return &referencesAccessErr{statuses}
}
}
return nil
}
func (r *referencesAccessErr) Error() string {
return fmt.Sprintf("%s", r.statuses)
}
// IsReferencesAccessError returns true if the given error indicates that some
// of the `AttributeReferencer` fields are referring to objects that are not
// accessible, either they are not ready or they do not yet exist
func IsReferencesAccessError(err error) bool {
_, result := err.(*referencesAccessErr)
return result
}
// A CanReference is a resource that can reference another resource in its
// spec in order to automatically resolve corresponding spec field values
// by inspecting the referenced resource.
type CanReference runtime.Object
// An AttributeReferencer resolves cross-resource attribute references. See
// https://github.com/crossplaneio/crossplane/blob/master/design/one-pager-cross-resource-referencing.md
// for more information
type AttributeReferencer interface {
// GetStatus retries the referenced resource, as well as other non-managed
// resources (like a `Provider`) and reports their readiness for use as a
// referenced resource.
GetStatus(ctx context.Context, res CanReference, r client.Reader) ([]ReferenceStatus, error)
// Build retrieves the referenced resource, as well as other non-managed
// resources (like a `Provider`), and builds the referenced attribute,
// returning it as a string value.
Build(ctx context.Context, res CanReference, r client.Reader) (value string, err error)
// Assign accepts a managed resource object, and assigns the given value to
// its corresponding property.
Assign(res CanReference, value string) error
}
// An AttributeReferencerFinder returns all types within the supplied object
// that satisfy AttributeReferencer.
type AttributeReferencerFinder interface {
FindReferencers(obj interface{}) []AttributeReferencer
}
// An AttributeReferencerFinderFn satisfies AttributeReferencerFinder.
type AttributeReferencerFinderFn func(obj interface{}) []AttributeReferencer
// FindReferencers finds all AttributeReferencers.
func (fn AttributeReferencerFinderFn) FindReferencers(obj interface{}) []AttributeReferencer {
return fn(obj)
}
// An APIManagedReferenceResolver finds and resolves a resource's references,
// then updates it in the Kubernetes API.
type APIManagedReferenceResolver struct {
client client.Client
finder AttributeReferencerFinder
}
// An APIManagedReferenceResolverOption configures an
// APIManagedReferenceResolver.
type APIManagedReferenceResolverOption func(*APIManagedReferenceResolver)
// WithAttributeReferencerFinder specifies an AttributeReferencerFinder used to
// find AttributeReferencers.
func WithAttributeReferencerFinder(f AttributeReferencerFinder) APIManagedReferenceResolverOption {
return func(r *APIManagedReferenceResolver) {
r.finder = f
}
}
// NewAPIManagedReferenceResolver returns an APIManagedReferenceResolver. The
// resolver uses reflection to recursively finds all pointer types in a struct
// that satisfy AttributeReferencer by default. It assesses only pointers,
// structs, and slices because it is assumed that only struct fields or slice
// elements that are pointers to a struct will satisfy AttributeReferencer.
func NewAPIManagedReferenceResolver(c client.Client, o ...APIManagedReferenceResolverOption) *APIManagedReferenceResolver {
r := &APIManagedReferenceResolver{
client: c,
finder: AttributeReferencerFinderFn(findReferencers),
}
for _, rro := range o {
rro(r)
}
return r
}
// ResolveReferences resolves references made to other managed resources
func (r *APIManagedReferenceResolver) ResolveReferences(ctx context.Context, res CanReference) error {
// Retrieve all the referencer fields from the managed resource.
referencers := r.finder.FindReferencers(res)
// If there are no referencers exit early.
if len(referencers) == 0 {
return nil
}
// Make sure that all the references are ready.
allStatuses := []ReferenceStatus{}
for _, referencer := range referencers {
statuses, err := referencer.GetStatus(ctx, res, r.client)
if err != nil {
return errors.Wrap(err, errGetReferencerStatus)
}
allStatuses = append(allStatuses, statuses...)
}
if err := newReferenceAccessErr(allStatuses); err != nil {
return err
}
existing := res.DeepCopyObject()
// Build and assign the attributes.
for _, referencer := range referencers {
val, err := referencer.Build(ctx, res, r.client)
if err != nil {
return errors.Wrap(err, errBuildAttribute)
}
if err := referencer.Assign(res, val); err != nil {
return errors.Wrap(err, errAssignAttribute)
}
}
// Don't update if nothing changed during reference assignment.
if cmp.Equal(existing, res) {
return nil
}
// Persist the updated managed resource.
return errors.Wrap(r.client.Update(ctx, res), errUpdateReferencer)
}
// findReferencers recursively finds all pointer types in a struct that satisfy
// AttributeReferencer. It assesses only pointers, structs, and slices because
// it is assumed that only struct fields or slice elements that are pointers to
// a struct will satisfy AttributeReferencer.
func findReferencers(obj interface{}) []AttributeReferencer { // nolint:gocyclo
// NOTE(negz): This function is slightly over our complexity goal, but is
// easier to follow as a single function.
referencers := []AttributeReferencer{}
switch v := reflect.ValueOf(obj); v.Kind() {
case reflect.Ptr:
if v.IsNil() || !v.CanInterface() {
return nil
}
if ar, ok := v.Interface().(AttributeReferencer); ok {
referencers = append(referencers, ar)
}
if v.Elem().CanInterface() {
referencers = append(referencers, findReferencers(v.Elem().Interface())...)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if !v.Field(i).CanInterface() {
continue
}
referencers = append(referencers, findReferencers(v.Field(i).Interface())...)
}
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
if !v.Index(i).CanInterface() {
continue
}
referencers = append(referencers, findReferencers(v.Index(i).Interface())...)
}
}
return referencers
}