design/one-pager-controlled-rollout-of-composition-functions.md
Crossplane allows multiple revisions of a composition to be available via the CompositionRevision API. An XR can specify which revision to use by name or label selector, with the newest revision being used by default. This allows users to gradually roll out new revisions of compositions by either manually or automatically (via a controller) updating the composition revision in use for each XR.
With composition functions, some or all composition logic moves out of the composition itself and into functions. Crossplane allows only a single revision of a function to be active at once; the active revision is the only one with a corresponding deployment in the control plane. While composition changes can still be gradually rolled out, function changes are all or nothing: the new version of a function is used by all composition revisions, and therefore all XRs, immediately.
With "generic" functions such as function-go-templating or function-kcl that
take source code written inline in the composition as input, this is generally a
tolerable problem. The functions themselves change slowly compared with the
compositions using them, and the code that directly composes resources is
versioned with the compositions as it would be when using patch and
transform. The requirement to inline code into YAML also naturally keeps the
custom code users write for these functions relatively small and easy to
inspect.
With non-generic functions, the code responsible for composing resources lives entirely outside of the composition and may be arbitrarily complex. This means composition revisions cannot be used to gradually roll out changes to composition logic: the logic is in functions, which have only one active revision.
This document proposes two changes in Crossplane to allow for progressive rollout of composition functions:
To limit risk, both of these changes will be introduced behind a feature flag, which will initially be off by default. Since neither change is useful by itself, a single feature flag will control both.
Crossplane already supports multiple revisions of function packages; however, only one revision at a time can be active. The active revision is the only one with a running deployment and there is a single endpoint (Kubernetes Service) for each function. The service is updated by Crossplane to point at the active revision’s deployment, but the endpoint is recorded in each function revision resource and the composition controller looks up endpoints by finding the active revision.
To allow for multiple active revisions, we will add a new field to the
Function resource called activeRevisionLimit. This setting controls how many
revisions Crossplane will keep active at any given time. Its name mirrors the
revisionHistoryLimit setting and its value must be no greater than the
revisionHistoryLimit. By default, the activeRevisionLimit will be 1,
maintaining today’s behavior. For all package types other than Function, 1
will be the only valid value for activeRevisionLimit, since multiple active
revisions do not make sense for providers or configurations.
For example, to maintain up to four revisions and up to two active revisions, the user would create a function resource like the following:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-patch-and-transform
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2
revisionHistoryLimit: 4
activeRevisionLimit: 2
When revisionActivationPolicy is Automatic, the revisions with the highest
revision numbers (up to the limit) will be active. If a new revision is created
and activeRevisionLimit revisions are already active, the active one with the
lowest revision number will be deactivated. When revisionActivationPolicy is
Manual, activeRevisionLimit is ignored by the package manager and it's left
to users to activate and deactivate revisions as they wish.
We will update the package manager’s runtime to name services after the associated package revision rather than the package, and create a service per active revision. This is required to allow multiple active revisions to serve traffic. The endpoints used by the composition controller to connect to functions are already recorded in the function revision resources, so this is a natural change (each active function revision will now have a distinct endpoint).
One additional change is necessary in the package manager to enable the composition changes described below. The package manager needs to copy labels from packages to their revisions, so that users can set a label on a function and then use it to select the relevant revision in their composition. Labels on compositions already work this way (they get copied to composition revisions), so this change will make package revisions more similar to composition revisions.
To enable compositions to refer to specific function revisions, we will add two
new optional fields to composition pipeline steps: functionRevisionRef and
functionRevisionSelector, mirroring the compositionRevisionRef and
compositionRevisionSelector found in XRs. These new fields will allow
compositions to select a specific function revision by name or by label.
For example, a user wishing to roll out a new version of
function-patch-and-transform to only certain XRs may update their existing
installation of the function by applying the following manifest:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-patch-and-transform
labels:
release-channel: alpha
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2
revisionHistoryLimit: 4
activeRevisionLimit: 2
The package manager will create a new function revision with the
release-channel: alpha label. The user would then update their composition to
reference the labeled function revision, causing a new composition revision to
be created:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
labels:
release-channel: alpha
spec:
compositeTypeRef:
apiVersion: custom-api.example.org/v1alpha1
kind: AcmeBucket
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
functionRevisionSelector:
matchLabels:
release-channel: alpha
input:
# Removed for brevity
Finally, they would update one or more XRs to use the new composition revision:
apiVersion: custom-api.example.org/v1alpha1
kind: AcmeBucket
metadata:
name: my-bucket
spec:
compositionUpdatePolicy: Manual
compositionRevisionSelector:
matchLabels:
release-channel: alpha
Today, the function runner invoked by the composition controller finds the
function endpoint for a pipeline step by first listing all function revisions
with the pkg.crossplane.io/package label set to the function's name, then
finding the single active revision in the list. This logic will change as
follows:
functionRevisionRef is specified, the revision will be fetched directly
by name.functionRevisionSelector is specified, the relevant matchLabels will
be used when listing revisions (in addition to pkg.crossplane.io/package).functionRevisionSelector is not given or matches multiple revisions), the
highest numbered active revision will be used.Note that in the case where no function revision is specified, and there is only one active revision for the function, the behavior will not change from today.
If no active function revision is found for a pipeline step, the composition pipeline will fail. In this case, the composition controller should set an error condition on the XR and raise an event.