konditions

package
v0.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2024 License: MIT Imports: 6 Imported by: 8

Documentation

Index

Constants

This section is empty.

Variables

View Source
var LockNotReleasedErr = errors.New("Condition's lock was not released")
View Source
var NotInitializedConditionsErr = errors.New("Conditions is not initialized")

Functions

This section is empty.

Types

type Condition

type Condition struct {
	// The type of the condition you want to have control over. The type is a user-defined value that extends the ConditionType. The type
	// serves as a way to identify the condition and it can be fetched from the Conditions type by using any of the finder methods.
	// ---
	// +required
	// +kubebuilder:validation:Type=string
	// +kubebuilder:validation:Required
	// +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$`
	// +kubebuilder:validation:MaxLength=316
	Type ConditionType `json:"type" protobuf:"bytes,1,opt,name=type"`

	// Current status of the condition. This field should mutate over the lifetime of the condition. By default, it starts as
	// ConditionInitialized and it's up to the user to modify the status to reflect where the condition is, relative to its lifetime.
	// ---
	// +required
	// +kubebuilder:validation:Type=string
	// +kubebuilder:validation:Required
	// +kubebuilder:validation:MaxLength=128
	Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status"`

	// LastTransitionTime is the last time the condition transitioned from one status to another. This value is set automatically by
	// the Conditions' method and as such, don't need to be set by the user.
	// ---
	// +required
	// +kubebuilder:validation:Required
	// +kubebuilder:validation:Type=string
	// +kubebuilder:validation:Format=date-time
	LastTransitionTime meta.Time `json:"lastTransitionTime" protobuf:"bytes,4,opt,name=lastTransitionTime"`

	// Reason represents the details about the transition and its current state.
	// For instance, it can hold the description of an error.Error() if the status is set to
	// ConditionError. This field is optional and should be used to give additionnal context.
	// Since this value can be overriden by future changes to the status of the condition,
	// users might want to also record the Reason through Kubernete's EventRecorder.
	// ---
	// +optional
	// +kubebuilder:validation:MaxLength=1024
	// +kubebuilder:validation:MinLength=1
	Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"`
}

Condition is an individual condition that makes the Conditions type. Each of those conditions are created to isolate some behavior the user wants control over.

func (*Condition) DeepCopy

func (in *Condition) DeepCopy() *Condition

DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.

func (*Condition) DeepCopyInto

func (in *Condition) DeepCopyInto(out *Condition)

type ConditionStatus

type ConditionStatus string
const (
	// ConditionInitialized is the first status a condition can have, while this value can be set manually
	// by a user, it often is set using the `conditions.FindOrInitializeFor(ConditionType)` helper function. This
	// default value aims at giving user the ability to set a condition themselves with sensible defaults.
	ConditionInitialized ConditionStatus = "Initialized"

	// ConditionCompleted should be used when a condition has ran to completion. This means that no further action
	// are needed for this condition (besides termination, see below).
	ConditionCompleted ConditionStatus = "Completed"

	// ConditionCreated is useful when an external resource needs to be created and configured in multiple
	// reconciliation loop. This state can indicated that some initial work was successfully done but more
	// work needs to be done.
	ConditionCreated ConditionStatus = "Created"

	// ConditionTerminating is useful when a custom resource is marked for deletion but a finalizer
	// was configured on the object. Terminating can be used if the termination requires more than one reconciliation loop.
	// This can be particularly useful when deletion of some external resources is done asynchronously and the user doesn't
	// want to block a reconciliation loop only for assuring that said resource was successfuly created.
	ConditionTerminating ConditionStatus = "Terminating"

	// ConditionTerminated is done to indicate that the finalizer attached to the object is removed and the condition
	// doesn't need to do additional work. A condition that is terminated shouldn't need to be worked on.
	ConditionTerminated ConditionStatus = "Terminated"

	// ConditionError means an error occurred. The error can be of any type. When a condition is marked as errored, it should not
	// be worked on again. Graceful errors should not be marked here but rather be idenfitied through the eventRecorder.
	// Errors are fatal, and the Reason of a condition can be used to store the String() value of the error to help the user
	// know what happened.
	ConditionError ConditionStatus = "Error"

	// ConditionLocked is a special status to tell the reconciler that a condition is being worked on. This is useful when
	// a condition is linked to an external resource (cloud provider, third party, etc.) and you want to have avoid creating
	// duplicate of a resources externally. This can help avoid multiple reconciliation happen at a same time. A reconciliation
	// should attempt to `Locked` the resource first, if the update/patch is succesful, then it means this reconciliation loop
	// as acquired a lock on this condition. It is important to note, however, that it's not a "real" lock. We're in a distributed system and
	// the etcd/kubernetes client interaction include layers of caching and logic.
	ConditionLocked ConditionStatus = "Locked"
)

type ConditionType

type ConditionType string

type ConditionalResource

type ConditionalResource interface {
	Conditions() *Conditions

	client.Object
}

ConditionObject is an interface for your CRD with the added method Conditions() defined by the user. This interface exists to simplify the usage of Lock and can be implemented by adding the conditions getter your CRD.

type MyCRDSpec struct { ... }
type MyCRDStatus struct {
	// ... Other fields ...

	Conditions konditions.Conditions `json:"conditions"`
}

type MyCRD struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   DNSRecordSpec   `json:"spec,omitempty"`
	Status DNSRecordStatus `json:"status,omitempty"`
}

func (m MyCRD) Conditions() *konditions.Conditions {
	return &m.Status.Conditions
}

type Conditions

type Conditions []Condition

func (Conditions) AnyWithStatus

func (c Conditions) AnyWithStatus(status ConditionStatus) bool

Check if any of the condition matches the ConditionStatus. It returns true if *any* of the conditions in the set has a status that matches the provided ConditionStatus.

	hasError := conditions.AnyWithStatus(ConditionError)
 if hasError {
		// ... Mark the object as errored ...
	}

func (*Conditions) DeepCopy

func (in *Conditions) DeepCopy() Conditions

DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status.

func (*Conditions) DeepCopyInto

func (in *Conditions) DeepCopyInto(out *Conditions)

DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.

func (Conditions) FindOrInitializeFor

func (c Conditions) FindOrInitializeFor(ct ConditionType) Condition

Find or initialize a condition for the type given. If a condition exists for the type given, it will return a *copy* of the condition If none exists, it will create a new condition for the type specified and the status will be set to ConditionInitialized

Once a condition is configured and ready to be stored in a conditions, you'll have to add it back by calling `Conditions.SetCondition(condition)`

c := conditions.FindOrInitializeFor(ConditionType("Example"))
c.Status = ConditionCompleted
conditions.SetCondition(c)

func (Conditions) FindStatus

func (c Conditions) FindStatus(conditionStatus ConditionStatus) *Condition

Find the first condition that matches `ConditionStatus`

This is useful,for instance, when you have a bunch of condition and you'd like to know if any of them has had an Error. It's important to understand that statuses aren't unique within a set of conditions, and as such, the condition returned is the first encountered.

errCondition := conditions.FindStatusCondition(api.conditionError)
if errCondition != nil {
	// Log the error and mark the top level status as errored
}

Even though a pointer is returned by the method, note that the value returned points to a *copy* of the condition in Conditions. This is because FindStatus can return an empty result and it's more explicit to return `nil` then it is to return a zered Condition.

func (Conditions) FindType

func (c Conditions) FindType(conditionType ConditionType) *Condition

Find a condition that matches `ConditionType`.

This method is similar to FindStatus but instead operates on the ConditionType. Since it is expected for types to be unique within a Conditions set, it should either return the same condition or nil.

Even though a pointer is returned by the method, note that the value returned points to a *copy* of the condition in Conditions. This is because FindStatus can return an empty result and it's more explicit to return `nil` then it is to return a zered Condition.

func (*Conditions) RemoveConditionWith

func (c *Conditions) RemoveConditionWith(conditionType ConditionType) (removed bool)

Remove the conditionType from the conditions set. The return value indicates whether a condition was removed or not.

Since all conditions are identified by a ConditionType, only the type is needed when removing a condition from the set. The changes won't be persisted until you actually run the update/patch command to the Kubernetes server.

myResource.conditions.RemoveConditionWith(ConditionType("A Controller Step"))
if err := reconciler.Status().Update(&myResource); err != nil {
	// ... deal with k8s error ...
}

func (*Conditions) SetCondition

func (c *Conditions) SetCondition(newCondition Condition) error

Set the given condition into the Conditions. The return value indicates whether the condition was changed in the stack or not.

This is the main method you'll use on a Conditions set to add/update a condition that you have operated on. The condition will be stored in the set but won't be persisted until you actually run the update/patch command to the Kubernetes server.

myNewCondition := Condition{
	Type: ConditionType("A Controlled Step"),
	Status: ConditionCreated,
	Reason: "Item Created, waiting until it becomes available",
}
myResource.conditions.SetCondition(myNewCondition)
if err := reconciler.Status().Update(&myResource); err != nil {
	// ... deal with k8s error ...
}

func (Conditions) TypeHasStatus

func (c Conditions) TypeHasStatus(conditionType ConditionType, status ConditionStatus) bool

Check if the condition with ConditionType matches the status provided.

This is an utility method that can be useful if the condition is not needed and the user only wants to know if a certain condition has the expected Status.

isCompleted := conditions.TypeHasStatus(ConditionType("Example"), ConditionCompleted)
if isCompleted {
	// ... Do something ...
}

type Lock

type Lock struct {
	// contains filtered or unexported fields
}

Lock is and advisory lock that can be used to make sure you have control over a condition before running a task that would create external resources. Even though this is named a Lock, be aware that we're working in a distributed system and the lock is at the application level. This does not have strong atomic guarantees but it doesn't mean it's not useful either.

Kubernetes operate cache layers where client of the Kubernetes API can sometime operate on a stale cache. This would usually result in an error when *updating* the custom resource (or its status):

var res Resource
if err := reconciler.Get(ctx, &res, request); err != nil {
	// No error on fetching, hit the cache, but the cache is stale
}

bucket, err := createBucketForResource(ctx, res)
if err != nil {
	// No error here
}

res.Status.BucketName = bucket.Name
res.Status.Conditions.SetCondition(Condition{
	Type: ConditionType("Bucket"),
	Status: konditions.ConditionCreated,
	Reason: "Bucket Created",
})

if err := reconciler.Status().Update(ctx, &res); err != nil {
	// The cache was stale, conflict on update..
	// !Boom!
}

In the example above, the error happens after all the work was executed, which leaves the user with fetching the resource again, and trying to eventually get a fresh copy of the resource so you can make the update, this means you have wrap most of the conciler logic in a retry block, making the whole reconciliation process harder to reason about. It also have the downside of blocking the execution of the reconciliation loop, which can cause congestion in your pipeline.

This is where the Lock comes handy. Before any work starts, it will attempt to modify the condition's Status to `ConditionLocked`. If the condition is successfully updated, it means the cache is not stale at the time of updating the condition and it should be safe to start working on the Task.

It is the user's job to set the condition at the end of the Task to the end state desired, but the Lock will operate on any error that the Task returns: You don't have to update the condition on errors.

var res Resource
if err := reconciler.Get(ctx, &res, request); err != nil {
	// No error on fetching, hit the cache, but the cache is stale
}

lock := konditions.NewLock(res, reconciler.Client, ConditionType("Bucket"))
lock.Execute(ctx, func(condition Condition) (Condition, error) {
	bucket, err := createBucketForResource(ctx, &res)
	if err != nil {
		return err
	}

	res.Status.BucketName = bucket.Name
  condition.Status = konditions.ConditionCreated
	condition.Reason = "Bucket Created"

	return condition, reconciler.Status().Update(ctx, &res)
})

The condition with type "Bucket" will have its Status go through a few stages:

  • Initialized
  • Locked
  • Created *or* Error

func NewLock

func NewLock(obj ConditionalResource, c client.Client, ct ConditionType) *Lock

NewLock returns a fully configured lock to run. Providing the CRD as a ConditionalResource and the condition type you want to operate on, the lock will fetch the condition, and configure itself to be executed when needed.

The lock will hold a copy of the condition with ConditionType at the time of its initialized.

The Client interface is usually the reconciler controller you are within.

lock := konditions.NewLock(res, reconciler.Client, ConditionType("Bucket"))

func (*Lock) Condition

func (l *Lock) Condition() Condition

Returns a copy of the condition for which the lock has been created

This is a helper method to allow creator of locks to easily retrieve the condition outside the execution loop. This can be useful if the lock is created but you need to check something about the condition before calling `Execute` Returns a copy of the condition for which the lock has been created

This is a helper method to allow creator of locks to easily retrieve the condition outside the execution loop. This can be useful if the lock is created but you need to check something about the condition before calling `Execute`.

This method returns a copy of the condition at the time of the creation of the lock.

func (*Lock) Execute

func (l *Lock) Execute(ctx context.Context, task Task) (err error)

Execute the task after successfully setting the condition to ConditionLocked. Calling Execute will attempt to change the condition's Status to ConditionLocked. If successful, it will then call Task(condition) where the condition is a copy of the condition before the Lock was initialized, this means that even if the condition is "locked", the condition passed will have the status *before* it was locked, giving the opportunity to the task to analyze what the status of the condition was.

If the task returns an error, the condition will be updated to ConditionError and the Reason will be set to the error.Error().

It is up to the Task to set the condition to its final state with the appropriate reason. By returning the condition, the Lock will use the returned condition and the Lock will update the status' subresource of the custom resource.

If any error happens while communicating with the Kubernetes API, it will be returned. If it were to happen, the condition will not be updated, the error can then be passed to the reconciler so it retries the reconciliation loop.

if err := lock.Execute(ctx, task); err != nil {
	return ctrl.Result{}, err
}

The Execution loop will always return Kubernetes API error first as they surround the call to the Task. So, if the Task returns an error, but updating the condition to the K8s API server also returns an error, the K8s error will be returned. It is possible in the future errors are wrapped, or a slice of error is returned, but either options also bring a bunch of pros/cons to consider and at this time, I (P-O) just don't know which direction is the more user friendly.

It is *required* that the Task changes the status of the Condition to its final value. If the condition still has the status ConditionLocked when the task returns, the Execute method will set the Condition to ConditionError with the Error set to `LockNotReleasedErr`.

type Task

type Task func(Condition) (Condition, error)

Task is a unit of work on a given Condition as specified by the lock. The condition is a copy of the condition *before* the lock was obtained. This is useful as the status can be useful to make a decision.

The condition can be updated from within the Task as it is required that the user returns the condition at the end. If the error is `nil`, the condition returned will replace the condition that existed before the Task ran.

lock := konditions.NewLock(res, ConditionType("Bucket"))
lock.Execute(ctx, reconciler.Client, func(condition Condition) (Condition, error) {
	// Even if the condition status is currently `ConditionLocked`, the condition passed
	// will be a copy with the Status prior to locking it.
	// In this example, the condition didn't exist, and as such, `FindOrInitializeFor`
	// returned the condition with the default value: ConditionInitialized

	if condition.Status == ConditionTerminating {
		if err := deleteBucketForResource(&res); err != nil {
			return err
		}
		condition.Status = ConditionTerminated
		condition.Reason = "Bucket deleted"

		return condition, reconciler.Status().Update(ctx, &res)
	}

	if condition.Status == ConditionInitialized {
		bucket, err := createBucketForResource(ctx, &res)
		if err != nil {
			return err
		}

		res.Status.BucketName = bucket.Name
		condition.Status = konditions.ConditionCreated
		condition.Reason = "Bucket Created"

		return condition, reconciler.Status().Update(ctx, &res)
	}
})

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL