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

Commit

Permalink
Duck type clients and reconcilers (#402)
Browse files Browse the repository at this point in the history
The clients used by reconciler runtime are now able to interact with
duck typed resources, with some limitations. The support extends to
working inside reconcilers, including with the ResourceReconciler.

Duck typed resources are structured types that implement the
client.Object interface but are not registered in the scheme for a GVK.
The APIVersion and Kind fields must be set on the object.

Using ResourceReconciler with a duck type is similar to using an
unstructured type and then casting to the duck type, however, starting
with a duck type will allow the resource to participate in common
structured operations like setting the status observed generation,
initializing conditions, and coalescing condition timestamps when the
condition did not otherwise change.

Known limitations include:
- Create client methods are not supported. Resources always need to be
  created with a concrete type.
- Update client methods are not supported. Use Patch methods instead.
- ResourceReconciler will patch status if a mutation is detected instead
  of update. The bytes of the patch must be defined on the
  ReconcilerTestCase for tests that result in a patch request.
  Ephemeral values, like LastTransitionTime timestamps, need to be
  pinned to known values. Use ReconcilerTestCase#Now for timestamp
  values.

To help solve the ephemeral nature of time, this change also introduces
a mechanism to retrieve the current time off the context via
rtime.RetrieveNow. Each root reconciler stashes the current time.
Additionally, each test case can define the Now field with a custom
timestamp that is stashed as the current time.

The condition manager Manage method is deprecated in favor of
ManageWithContext, which will use the stashed time when constructing
conditions. The status InitializeConditions method should be updated
to accept a context when it is defined, the no arg variant is
deprecated. When using either deprecated method without a context,
time.Now is used instead of the stashed time.

Signed-off-by: Scott Andrews <[email protected]>
  • Loading branch information
scothis authored Aug 10, 2023
1 parent 4dff503 commit 376b743
Show file tree
Hide file tree
Showing 23 changed files with 1,606 additions and 60 deletions.
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

0 comments on commit 376b743

Please sign in to comment.