|
| 1 | +# Controlled Rollout of Composition Functions |
| 2 | + |
| 3 | +* Owner: Adam Wolfe Gordon (@adamwg) |
| 4 | +* Reviewers: Crossplane Maintainers |
| 5 | +* Status: Accepted |
| 6 | + |
| 7 | +## Background |
| 8 | + |
| 9 | +Crossplane allows multiple revisions of a composition to be available via the |
| 10 | +CompositionRevision API. An XR can specify which revision to use by name or |
| 11 | +label selector, with the newest revision being used by default. This allows |
| 12 | +users to gradually roll out new revisions of compositions by either manually or |
| 13 | +automatically (via a controller) updating the composition revision in use for |
| 14 | +each XR. |
| 15 | + |
| 16 | +With composition functions, some or all composition logic moves out of the |
| 17 | +composition itself and into functions. Crossplane allows only a single revision |
| 18 | +of a function to be active at once; the active revision is the only one with a |
| 19 | +corresponding deployment in the control plane. While composition changes can |
| 20 | +still be gradually rolled out, function changes are all or nothing: the new |
| 21 | +version of a function is used by all composition revisions, and therefore all |
| 22 | +XRs, immediately. |
| 23 | + |
| 24 | +With "generic" functions such as `function-go-templating` or `function-kcl` that |
| 25 | +take source code written inline in the composition as input, this is generally a |
| 26 | +tolerable problem. The functions themselves change slowly compared with the |
| 27 | +compositions using them, and the code that directly composes resources is |
| 28 | +versioned with the compositions as it would be when using patch and |
| 29 | +transform. The requirement to inline code into YAML also naturally keeps the |
| 30 | +custom code users write for these functions relatively small and easy to |
| 31 | +inspect. |
| 32 | + |
| 33 | +With non-generic functions, the code responsible for composing resources lives |
| 34 | +entirely outside of the composition and may be arbitrarily complex. This means |
| 35 | +composition revisions cannot be used to gradually roll out changes to |
| 36 | +composition logic: the logic is in functions, which have only one active |
| 37 | +revision. |
| 38 | + |
| 39 | +## Goals |
| 40 | + |
| 41 | +* Allow users to progressively roll out changes to functions that compose |
| 42 | + resources. This solves [crossplane#6139]. |
| 43 | +* Maintain current behavior for users who do not wish to roll out function |
| 44 | + changes progressively. |
| 45 | + |
| 46 | +## Proposal |
| 47 | + |
| 48 | +This document proposes two changes in Crossplane to allow for progressive |
| 49 | +rollout of composition functions: |
| 50 | + |
| 51 | +1. Allow multiple revisions of a function to be active (i.e., able to serve |
| 52 | + requests) at once, and |
| 53 | +2. Allow composition pipeline steps to reference a specific function revision so |
| 54 | + that different revisions of a composition can use different revisions of a |
| 55 | + function. |
| 56 | + |
| 57 | +To limit risk, both of these changes will be introduced behind a feature flag, |
| 58 | +which will initially be off by default. Since neither change is useful by |
| 59 | +itself, a single feature flag will control both. |
| 60 | + |
| 61 | +### Package Manager Changes |
| 62 | + |
| 63 | +Crossplane already supports multiple revisions of function packages; however, |
| 64 | +only one revision at a time can be active. The active revision is the only one |
| 65 | +with a running deployment and there is a single endpoint (Kubernetes Service) |
| 66 | +for each function. The service is updated by Crossplane to point at the active |
| 67 | +revision’s deployment, but the endpoint is recorded in each function revision |
| 68 | +resource and the composition controller looks up endpoints by finding the active |
| 69 | +revision. |
| 70 | + |
| 71 | +To allow for multiple active revisions, we will add a new field to the |
| 72 | +`Function` resource called `activeRevisionLimit`. This setting controls how many |
| 73 | +revisions Crossplane will keep active at any given time. Its name mirrors the |
| 74 | +`revisionHistoryLimit` setting and its value must be no greater than the |
| 75 | +`revisionHistoryLimit`. By default, the `activeRevisionLimit` will be 1, |
| 76 | +maintaining today’s behavior. For all package types other than `Function`, 1 |
| 77 | +will be the only valid value for `activeRevisionLimit`, since multiple active |
| 78 | +revisions do not make sense for providers or configurations. |
| 79 | + |
| 80 | +For example, to maintain up to four revisions and up to two active revisions, |
| 81 | +the user would create a function resource like the following: |
| 82 | + |
| 83 | +```yaml |
| 84 | +apiVersion: pkg.crossplane.io/v1 |
| 85 | +kind: Function |
| 86 | +metadata: |
| 87 | + name: function-patch-and-transform |
| 88 | +spec: |
| 89 | + package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2 |
| 90 | + revisionHistoryLimit: 4 |
| 91 | + activeRevisionLimit: 2 |
| 92 | +``` |
| 93 | +
|
| 94 | +When `revisionActivationPolicy` is `Automatic`, the revisions with the highest |
| 95 | +revision numbers (up to the limit) will be active. If a new revision is created |
| 96 | +and `activeRevisionLimit` revisions are already active, the active one with the |
| 97 | +lowest revision number will be deactivated. When `revisionActivationPolicy` is |
| 98 | +`Manual`, `activeRevisionLimit` is ignored by the package manager and it's left |
| 99 | +to users to activate and deactivate revisions as they wish. |
| 100 | + |
| 101 | +We will update the package manager’s runtime to name services after the |
| 102 | +associated package revision rather than the package, and create a service per |
| 103 | +active revision. This is required to allow multiple active revisions to serve |
| 104 | +traffic. The endpoints used by the composition controller to connect to |
| 105 | +functions are already recorded in the function revision resources, so this is a |
| 106 | +natural change (each active function revision will now have a distinct |
| 107 | +endpoint). |
| 108 | + |
| 109 | +One additional change is necessary in the package manager to enable the |
| 110 | +composition changes described below. The package manager needs to copy labels |
| 111 | +from packages to their revisions, so that users can set a label on a function |
| 112 | +and then use it to select the relevant revision in their composition. Labels on |
| 113 | +compositions already work this way (they get copied to composition revisions), |
| 114 | +so this change will make package revisions more similar to composition |
| 115 | +revisions. |
| 116 | + |
| 117 | +### Composition Changes |
| 118 | + |
| 119 | +To enable compositions to refer to specific function revisions, we will add two |
| 120 | +new optional fields to composition pipeline steps: `functionRevisionRef` and |
| 121 | +`functionRevisionSelector`, mirroring the `compositionRevisionRef` and |
| 122 | +`compositionRevisionSelector` found in XRs. These new fields will allow |
| 123 | +compositions to select a specific function revision by name or by label. |
| 124 | + |
| 125 | +For example, a user wishing to roll out a new version of |
| 126 | +`function-patch-and-transform` to only certain XRs may update their existing |
| 127 | +installation of the function by applying the following manifest: |
| 128 | + |
| 129 | +```yaml |
| 130 | +apiVersion: pkg.crossplane.io/v1 |
| 131 | +kind: Function |
| 132 | +metadata: |
| 133 | + name: function-patch-and-transform |
| 134 | + labels: |
| 135 | + release-channel: alpha |
| 136 | +spec: |
| 137 | + package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2 |
| 138 | + revisionHistoryLimit: 4 |
| 139 | + activeRevisionLimit: 2 |
| 140 | +``` |
| 141 | + |
| 142 | +The package manager will create a new function revision with the |
| 143 | +`release-channel: alpha` label. The user would then update their composition to |
| 144 | +reference the labeled function revision, causing a new composition revision to |
| 145 | +be created: |
| 146 | + |
| 147 | +```yaml |
| 148 | +apiVersion: apiextensions.crossplane.io/v1 |
| 149 | +kind: Composition |
| 150 | +metadata: |
| 151 | + name: example |
| 152 | + labels: |
| 153 | + release-channel: alpha |
| 154 | +spec: |
| 155 | + compositeTypeRef: |
| 156 | + apiVersion: custom-api.example.org/v1alpha1 |
| 157 | + kind: AcmeBucket |
| 158 | + mode: Pipeline |
| 159 | + pipeline: |
| 160 | + - step: patch-and-transform |
| 161 | + functionRef: |
| 162 | + name: function-patch-and-transform |
| 163 | + functionRevisionSelector: |
| 164 | + matchLabels: |
| 165 | + release-channel: alpha |
| 166 | + input: |
| 167 | + # Removed for brevity |
| 168 | +``` |
| 169 | + |
| 170 | +Finally, they would update one or more XRs to use the new composition revision: |
| 171 | + |
| 172 | +```yaml |
| 173 | +apiVersion: custom-api.example.org/v1alpha1 |
| 174 | +kind: AcmeBucket |
| 175 | +metadata: |
| 176 | + name: my-bucket |
| 177 | +spec: |
| 178 | + compositionUpdatePolicy: Manual |
| 179 | + compositionRevisionSelector: |
| 180 | + matchLabels: |
| 181 | + release-channel: alpha |
| 182 | +``` |
| 183 | + |
| 184 | +Today, the function runner invoked by the composition controller finds the |
| 185 | +function endpoint for a pipeline step by first listing all function revisions |
| 186 | +with the `pkg.crossplane.io/package` label set to the function's name, then |
| 187 | +finding the single active revision in the list. This logic will change as |
| 188 | +follows: |
| 189 | + |
| 190 | +1. If `functionRevisionRef` is specified, the revision will be fetched directly |
| 191 | + by name. |
| 192 | +2. If `functionRevisionSelector` is specified, the relevant `matchLabels` will |
| 193 | + be used when listing revisions (in addition to `pkg.crossplane.io/package`). |
| 194 | +3. When iterating over multiple returned revisions (because the |
| 195 | + `functionRevisionSelector` is not given or matches multiple revisions), the |
| 196 | + highest numbered active revision will be used. |
| 197 | + |
| 198 | +Note that in the case where no function revision is specified, and there is only |
| 199 | +one active revision for the function, the behavior will not change from today. |
| 200 | + |
| 201 | +If no active function revision is found for a pipeline step, the composition |
| 202 | +pipeline will fail. In this case, the composition controller should set an error |
| 203 | +condition on the XR and raise an event. |
| 204 | + |
| 205 | +## Alternatives Considered |
| 206 | + |
| 207 | +* Do nothing. For functions that don't take input, users can work around the |
| 208 | + limitation today by installing multiple versions of a function with unique |
| 209 | + names and referencing these names in their compositions. This workaround |
| 210 | + effectively simulates what is proposed in this design without support from |
| 211 | + Crossplane itself. However, it does not work for functions that take input due |
| 212 | + to [crossplane#5294], and does not work well with the package manager's |
| 213 | + dependency resolution system when using Configurations. |
| 214 | +* Make it easier for composition functions to take input from external sources |
| 215 | + such as OCI images or git repositories. This would allow function input to be |
| 216 | + versioned separately from both functions and compositions, with new |
| 217 | + composition revisions being created to reference new input versions. However, |
| 218 | + this change would introduce significant additional complexity to the |
| 219 | + composition controller, and would not solve the problem for functions that |
| 220 | + don't take input. |
| 221 | + |
| 222 | +[crossplane#6139]: https://github.com/crossplane/crossplane/issues/6139 |
| 223 | +[crossplane#5294]: https://github.com/crossplane/crossplane/issues/5294 |
0 commit comments