crossplane/internal/controller/apiextensions/composite/ready.go

277 lines
8.8 KiB
Go

/*
Copyright 2022 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 composite
import (
"context"
"k8s.io/utils/pointer"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource"
iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
// Error strings
const (
errInvalidCheck = "invalid"
errPaveObject = "cannot lookup field paths in supplied object"
errFmtRequiresFieldPath = "type %q requires a field path"
errFmtRequiresMatchString = "type %q requires a match string"
errFmtRequiresMatchConditions = "type %q requires a valid match condition"
errFmtRequiresMatchInteger = "type %q requires a match integer"
errFmtUnknownCheck = "unknown type %q"
errFmtRunCheck = "cannot run readiness check at index %d"
)
// ReadinessCheckType is used for readiness check types.
type ReadinessCheckType string
// The possible values for readiness check type.
const (
ReadinessCheckTypeNonEmpty ReadinessCheckType = "NonEmpty"
ReadinessCheckTypeMatchString ReadinessCheckType = "MatchString"
ReadinessCheckTypeMatchInteger ReadinessCheckType = "MatchInteger"
ReadinessCheckTypeMatchCondition ReadinessCheckType = "MatchCondition"
ReadinessCheckTypeNone ReadinessCheckType = "None"
)
// ReadinessCheck is used to indicate how to tell whether a resource is ready
// for consumption
type ReadinessCheck struct {
// Type indicates the type of probe you'd like to use.
Type ReadinessCheckType
// FieldPath shows the path of the field whose value will be used.
FieldPath *string
// MatchString is the value you'd like to match if you're using "MatchString" type.
MatchString *string
// MatchInt is the value you'd like to match if you're using "MatchInt" type.
MatchInteger *int64
// MatchCondition is the condition you'd like to match if you're using "MatchCondition" type.
// +optional
MatchCondition *MatchConditionReadinessCheck `json:"matchCondition,omitempty"`
}
// MatchConditionReadinessCheck is used to indicate how to tell whether a resource is ready
// for consumption
type MatchConditionReadinessCheck struct {
// Type indicates the type of condition you'd like to use.
// +kubebuilder:default="Ready"
Type string `json:"type,omitempty"`
// Status is the status of the condition you'd like to match.
// +kubebuilder:default="True"
Status string `json:"status,omitempty"`
}
// ReadinessCheckFromV1 derives a ReadinessCheck from the supplied v1.ReadinessCheck.
func ReadinessCheckFromV1(in *v1.ReadinessCheck) ReadinessCheck {
if in == nil {
return ReadinessCheck{}
}
out := ReadinessCheck{
Type: ReadinessCheckType(in.Type),
}
if in.FieldPath != "" {
out.FieldPath = pointer.String(in.FieldPath)
}
// NOTE(negz): ComposedTemplate doesn't use pointer values for optional
// strings, so today the empty string and 0 are equivalent to "unset".
if in.MatchString != "" {
out.MatchString = pointer.String(in.MatchString)
}
if in.MatchInteger != 0 {
out.MatchInteger = pointer.Int64(in.MatchInteger)
}
if in.MatchCondition != nil {
out.MatchCondition = &MatchConditionReadinessCheck{
Type: in.MatchCondition.Type,
Status: in.MatchCondition.Status,
}
}
return out
}
// ReadinessCheckFromDesiredReadinessCheck derives a ReadinessCheck from the supplied iov1alpha1.DesiredReadinessCheck.
func ReadinessCheckFromDesiredReadinessCheck(in *iov1alpha1.DesiredReadinessCheck) ReadinessCheck {
if in == nil {
return ReadinessCheck{}
}
out := ReadinessCheck{
Type: ReadinessCheckType(in.Type),
FieldPath: in.FieldPath,
MatchString: in.MatchString,
MatchInteger: in.MatchInteger,
}
if in.MatchCondition != nil {
out.MatchCondition = &MatchConditionReadinessCheck{
Type: in.MatchCondition.Type,
Status: in.MatchCondition.Status,
}
}
return out
}
// ReadinessChecksFromComposedTemplate derives readiness checks from the supplied
// composed template.
func ReadinessChecksFromComposedTemplate(t *v1.ComposedTemplate) []ReadinessCheck {
if t == nil {
return nil
}
out := make([]ReadinessCheck, len(t.ReadinessChecks))
for i := range t.ReadinessChecks {
out[i] = ReadinessCheckFromV1(&t.ReadinessChecks[i])
}
return out
}
// ReadinessChecksFromDesiredResource derives readiness checks from the supplied desired
// resource.
func ReadinessChecksFromDesiredResource(dr *iov1alpha1.DesiredResource) []ReadinessCheck {
if dr == nil {
return nil
}
out := make([]ReadinessCheck, len(dr.ReadinessChecks))
for i := range dr.ReadinessChecks {
out[i] = ReadinessCheckFromDesiredReadinessCheck(&dr.ReadinessChecks[i])
}
return out
}
// TODO(negz): Ideally we'd validate P&T readiness checks (which are specified
// in the Composition) using a webhook. We still need to validate the output of
// a Composition Function Pipeline, though.
// Validate returns an error if the readiness check is invalid.
func (c ReadinessCheck) Validate() error {
switch c.Type {
case ReadinessCheckTypeNone:
// This type has no dependencies.
return nil
case ReadinessCheckTypeNonEmpty:
// This type only needs a field path.
case ReadinessCheckTypeMatchString:
if c.MatchString == nil {
return errors.Errorf(errFmtRequiresMatchString, c.Type)
}
case ReadinessCheckTypeMatchInteger:
if c.MatchInteger == nil {
return errors.Errorf(errFmtRequiresMatchInteger, c.Type)
}
case ReadinessCheckTypeMatchCondition:
if c.MatchCondition == nil {
return errors.Errorf(errFmtRequiresMatchConditions, c.Type)
}
return nil
default:
return errors.Errorf(errFmtUnknownCheck, c.Type)
}
if c.FieldPath == nil {
return errors.Errorf(errFmtRequiresFieldPath, c.Type)
}
return nil
}
// IsReady runs the readiness check against the supplied object.
func (c ReadinessCheck) IsReady(p *fieldpath.Paved, o ConditionedObject) (bool, error) {
if err := c.Validate(); err != nil {
return false, errors.Wrap(err, errInvalidCheck)
}
switch c.Type {
case ReadinessCheckTypeNone:
return true, nil
case ReadinessCheckTypeNonEmpty:
if _, err := p.GetValue(*c.FieldPath); err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return true, nil
case ReadinessCheckTypeMatchString:
val, err := p.GetString(*c.FieldPath)
if err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return val == *c.MatchString, nil
case ReadinessCheckTypeMatchInteger:
val, err := p.GetInteger(*c.FieldPath)
if err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return val == *c.MatchInteger, nil
case ReadinessCheckTypeMatchCondition:
// we should have checked this outside of this function
val := o.GetCondition(xpv1.ConditionType(c.MatchCondition.Type))
return string(val.Status) == c.MatchCondition.Status, errors.New(errInvalidCheck)
}
return false, nil
}
// A ReadinessChecker checks whether a composed resource is ready or not.
type ReadinessChecker interface {
IsReady(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error)
}
// A ReadinessCheckerFn checks whether a composed resource is ready or not.
type ReadinessCheckerFn func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error)
// IsReady reports whether a composed resource is ready or not.
func (fn ReadinessCheckerFn) IsReady(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
return fn(ctx, o, rc...)
}
// A ConditionedObject is a runtime object with conditions.
type ConditionedObject interface {
resource.Object
resource.Conditioned
}
// IsReady returns whether the composed resource is ready.
func IsReady(_ context.Context, o ConditionedObject, rc ...ReadinessCheck) (bool, error) {
// kept as a safety net, but defaulting should ensure this is never hit
if len(rc) == 0 {
return resource.IsConditionTrue(o.GetCondition(xpv1.TypeReady)), nil
}
paved, err := fieldpath.PaveObject(o)
if err != nil {
return false, errors.Wrap(err, errPaveObject)
}
for i := range rc {
ready, err := rc[i].IsReady(paved, o)
if err != nil {
return false, errors.Wrapf(err, errFmtRunCheck, i)
}
if !ready {
return false, nil
}
}
return true, nil
}