Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Duck type clients and reconcilers #402

Merged
merged 5 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,28 @@ Within an existing Kubebuilder or controller-runtime project, reconciler-runtime
- [Status](#status)
- [Finalizers](#finalizers)
- [ResourceManager](#resourcemanager)
- [Time](#time)
- [Breaking Changes](#breaking-changes)
- [Current Deprecations](#current-deprecations)
- [Contributing](#contributing)
- [Acknowledgements](#acknowledgements)
- [License](#license)

## Reconcilers

Reconcilers can operate on three different types of objects:
- structured types (e.g. [`corev1.Pod`](https://pkg.go.dev/k8s.io/api/core/v1#Pod))
- unstructured types (e.g. [`unstructured.Unstructured`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured#Unstructured))
- semi-structured duck types (e.g. [`PodSpecable`](https://pkg.go.dev/knative.dev/pkg/apis/duck/v1#PodSpecable), [`ProvisionedService`](https://servicebinding.io/spec/core/1.0.0/#provisioned-service))

Structured types are often the best choice as they allow easy interaction with the full object and have full client support. The type must be registered with the [`Scheme`](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#Scheme). The type must be pre-defined and compiled into the controller.

Unstructured types are useful when the resources are not known at compile time and full access to the resource and client methods is desired. Since the type is not known in advance, it cannot be registered with the scheme. Interacting with the object is difficult as traversing the object requires lots of casts or reflection. The [`TypeMeta`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta) `APIVersion` and `Kind` fields must be defined for the client to operate on the object.

Semi-structured duck types offer a middle ground. They are strongly typed, but only cover a subset of the full object. They are intended to facilitate normalized operations across a number of concrete types that share a common subset of their own schema. The concrete objects compatible with this type are not required to be known at compile time. Because duck types are not full objects, client operations for `Create` and `Update` are disallowed (`Patch` is available). Like unstructured objects, the duck type should not be registered in the scheme, and the [`TypeMeta`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta) `APIVersion` and `Kind` fields must be defined for the client to operate on the object.

The controller-runtime client is able to work with structured and unstructured objects natively, reconciler-runtime adds support for duck typed objects via the [`duck.NewDuckAwareClientWrapper`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/duck#NewDuckAwareClientWrapper).

<a name="parentreconciler" />

### ResourceReconciler
Expand Down Expand Up @@ -566,7 +581,7 @@ The table test pattern is used to declare each test case in a test suite with th

The tests make extensive use of given and mutated resources. It is recommended to use a library like [dies](https://dies.dev) to reduce boilerplate code and to highlight the delta unique to each test.

There are two test suites, one for reconcilers and an optimized harness for testing sub reconcilers.
There are three test suites: for [testing reconcilers](#reconcilertests), an optimized harness for [testing sub reconcilers](#subreconcilertests), and for [testing admission webhooks](#admissionwebhooktests).

<a name="reconcilertestsuite" />

Expand Down Expand Up @@ -942,6 +957,12 @@ If configured, a [finalizer](#finalizers) can be managed on the resource which w

If requested, the managed resource will be tracked for the resource.

### Time

Reconcilers that capture timestamps can be natoriously difficult to test, as the output will be different for every execution. While we don't have a time machine, reconciler-runtime provides an alterate API to fetch the current time within a reconciler. [`rtime.RetrieveTime(context.Context)`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/time#RetrieveTime) can be used within a reconciler to get the [`time.Time`](https://pkg.go.dev/time#Time) when the reconciler request started processing. The value returned is guarenteed to remain stable for the lifespan of the reconcile request. Calls to [`time.Now`](https://pkg.go.dev/time#Now) will continue to return an up to date timestamp.

Reconciler tests can seed this timestamp by defining the [`Now`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/testing#ReconcilerTestCase.Now) field on the test case. The reconciler will be run with the desired time instead of "now". The timestamp set on the test case can also be used in the expectations to pin values that would otherwise float.

## Breaking Changes

Known breaking changes are captured in the [release notes](https://github.com/vmware-labs/reconciler-runtime/releases), it is strongly recomened to review the release notes before upgrading to a new version of reconciler-runtime. When possible, breaking changes are first marked as deprecations before full removal in a later release. Patch releases will be issued to fix significant bugs and unintentional breaking changes.
Expand All @@ -950,6 +971,11 @@ We strive to release reconciler-runtime against the latest Kuberentes and contro

reconciler-runtime is rapidly evolving. While we strive for API compatability between releases, functionality that is better handled using a different API may be removed. Release version numbers follow semver.

### Current Deprecations

- status `InitiazeConditions()` is deprecated in favor of `InitializeConditions(context.Context)`. Support may be removed in a future release, users are encuraged to migrate.
- `ConditionSet#Manage` is deprecated in favor of `ConditionSet#ManageWithContext`. Support may be removed in a future release, users are encuraged to migrate.

## Contributing

The reconciler-runtime project team welcomes contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). For more detailed information, refer to [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
26 changes: 21 additions & 5 deletions apis/conditionset.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ SPDX-License-Identifier: Apache-2.0
package apis

import (
"context"
"fmt"
"reflect"
"sort"
"time"

"fmt"

rtime "github.com/vmware-labs/reconciler-runtime/time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -46,8 +47,6 @@ type ConditionsAccessor interface {
SetConditions([]metav1.Condition)
}

type NowFunc func() time.Time

// ConditionSet is an abstract collection of the possible ConditionType values
// that a particular resource might expose. It also holds the "happy condition"
// for that resource, which we define to be one of Ready or Succeeded depending
Expand Down Expand Up @@ -156,14 +155,23 @@ var _ ConditionManager = (*conditionsImpl)(nil)
type conditionsImpl struct {
ConditionSet
accessor ConditionsAccessor
now time.Time
}

// Deprecated: use ManageWithContext
// Manage creates a ConditionManager from an accessor object using the original
// ConditionSet as a reference. Status must be a pointer to a struct.
func (r ConditionSet) Manage(status ConditionsAccessor) ConditionManager {
return r.ManageWithContext(context.TODO(), status)
}

// Manage creates a ConditionManager from an accessor object using the original
// ConditionSet as a reference. Status must be a pointer to a struct.
func (r ConditionSet) ManageWithContext(ctx context.Context, status ConditionsAccessor) ConditionManager {
return conditionsImpl{
accessor: status,
ConditionSet: r,
now: rtime.RetrieveNow(ctx),
}
}

Expand Down Expand Up @@ -210,7 +218,7 @@ func (r conditionsImpl) SetCondition(new metav1.Condition) {
}
}
}
new.LastTransitionTime = metav1.NewTime(time.Now()).Rfc3339Copy()
new.LastTransitionTime = metav1.NewTime(r.now).Rfc3339Copy()
conditions = append(conditions, new)
// Sorted for convenience of the consumer, i.e. kubectl.
sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
Expand Down Expand Up @@ -266,6 +274,10 @@ func (r conditionsImpl) MarkTrue(t string, reason, messageFormat string, message
Message: fmt.Sprintf(messageFormat, messageA...),
})

if len(r.dependents) == 0 {
return
}

// check the dependents.
for _, cond := range r.dependents {
c := r.GetCondition(cond)
Expand Down Expand Up @@ -294,6 +306,10 @@ func (r conditionsImpl) MarkUnknown(t string, reason, messageFormat string, mess
Message: fmt.Sprintf(messageFormat, messageA...),
})

if len(r.dependents) == 0 {
return
}

// check the dependents.
isDependent := false
for _, cond := range r.dependents {
Expand Down
Loading