github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/docs/design-docs/08-package-variant.md (about) 1 # Package Variant Controller 2 3 * Author(s): @johnbelamaric, @natasha41575 4 * Approver: @mortent 5 6 ## Why 7 8 When deploying workloads across large fleets of clusters, it is often necessary 9 to modify the workload configuration for a specific cluster. Additionally, those 10 workloads may evolve over time with security or other patches that require 11 updates. [Configuration as Data](06-config-as-data.md) in general and [Package 12 Orchestration](07-package-orchestration.md) in particular can assist in this. 13 However, they are still centered around manual, one-by-one hydration and 14 configuration of a workload. 15 16 This proposal introduces concepts and a set of resources for automating the 17 creation and lifecycle management of package variants. These are designed to 18 address several different dimensions of scalability: 19 - Number of different workloads for a given cluster 20 - Number of clusters across which those workloads are deployed 21 - Different types or characteristics of those clusters 22 - Complexity of the organizations deploying those workloads 23 - Changes to those workloads over time 24 25 ## See Also 26 - [Package Orchestration](07-package-orchestration.md) 27 - [#3347](https://github.com/GoogleContainerTools/kpt/issues/3347) Bulk package 28 creation 29 - [#3243](https://github.com/GoogleContainerTools/kpt/issues/3243) Support bulk 30 package upgrades 31 - [#3488](https://github.com/GoogleContainerTools/kpt/issues/3488) Porch: 32 BaseRevision controller aka Fan Out controller - but more 33 - [Managing Package 34 Revisions](https://docs.google.com/document/d/1EzUUDxLm5jlEG9d47AQOxA2W6HmSWVjL1zqyIFkqV1I/edit?usp=sharing) 35 - [Porch UpstreamPolicy Resource 36 API](https://docs.google.com/document/d/1OxNon_1ri4YOqNtEQivBgeRzIPuX9sOyu-nYukjwN1Q/edit?usp=sharing&resourcekey=0-2nDYYH5Kw58IwCatA4uDQw) 37 38 ## Core Concepts 39 40 For this solution, "workloads" are represented by packages. "Package" is a more 41 general concept, being an arbitrary bundle of resources, and therefore is 42 sufficient to solve the originally stated problem. 43 44 The basic idea here is to introduce a PackageVariant resource that manages the 45 derivation of a variant of a package from the original source package, and to 46 manage the evolution of that variant over time. This effectively automates the 47 human-centered process for variant creation one might use with `kpt`: 48 1. Clone an upstream package locally 49 1. Make changes to the local package, setting values in resources and 50 executing KRM functions 51 1. Push the package to a new repository and tag it as a new version 52 53 Similarly, PackageVariant can manage the process of updating a package when a 54 new version of the upstream package is published. In the human-centered 55 workflow, a user would use `kpt pkg update` to pull in changes to their 56 derivative package. When using a PackageVariant resource, the change would be 57 made to the upstream specification in the resource, and the controller would 58 propose a new Draft package reflecting the outcome of `kpt pkg update`. 59 60 By automating this process, we open up the possibility of performing systematic 61 changes that tie back to our different dimensions of scalability. We can use 62 data about the specific variant we are creating to lookup additional context in 63 the Porch cluster, and copy that information into the variant. That context is a 64 well-structured resource, not simply key/value pairs. KRM functions within the 65 package can interpret the resource, modifying other resources in the package 66 accordingly. The context can come from multiple sources that vary differently 67 along those dimensions of scalability. For example, one piece of information may 68 vary by region, another by individual site, another by cloud provider, and yet 69 another based on whether we are deploying to development, staging, or production. 70 By utilizing resources in the Porch cluster as our input model, we can represent 71 this complexity in a manageable model that is reused across many packages, 72 rather than scattered in package-specific templates or key/value pairs without 73 any structure. KRM functions, also reused across packages but configured as 74 needed for the specific package, are used to interpret the resources within the 75 package. This decouples authoring of the packages, creation of the input model, 76 and deploy-time use of that input model within the packages, allowing those 77 activities to be performed by different teams or organizations. 78 79 We refer to the mechanism described above as *configuration injection*. It 80 enables dynamic, context-aware creation of variants. Another way to think about 81 it is as a continuous reconciliation, much like other Kubernetes controllers. In 82 this case, the inputs are a parent package `P` and a context `C` (which may be a 83 collection of many independent resources), with the output being the derived 84 package `D`. When a new version of `C` is created by updates to in-cluster 85 resources, we get a new revision of `D`, customized based upon the updated 86 context. Similarly, the user (or an automation) can monitor for new versions of 87 `P`; when one arrives, the PackageVariant can be updated to point to that new 88 version, resulting in a newly proposed Draft of `D`, updated to reflect the 89 upstream changes. This will be explained in more detail below. 90 91 This proposal also introduces a way to "fan-out", or create multiple 92 PackageVariant resources declaratively based upon a list or selector, with the 93 PackageVariantSet resource. This is combined with the injection mechanism to 94 enable generation of large sets of variants that are specialized to a particular 95 target repository, cluster, or other resource. 96 97 ## Basic Package Cloning 98 99 The PackageVariant resource controls the creation and lifecycle of a variant 100 of a package. That is, it defines the original (upstream) package, the new 101 (downstream) package, and the changes (mutations) that need to be made to 102 transform the upstream into the downstream. It also allows the user to specify 103 policies around adoption, deletion, and update of package revisions that are 104 under the control of the package variant controller. 105 106 The simple clone operation is shown in *Figure 1*. 107 108 |  |  | 109 | :---: | :---: | 110 | *Figure 1: Basic Package Cloning* | *Legend* | 111 112 113 Note that *proposal* and *approval* are not handled by the package variant 114 controller. Those are left to humans or other controllers. The exception is the 115 proposal of deletion (there is no concept of a "Draft" deletion), which the 116 package variant control will do, depending upon the specified deletion policy. 117 118 ### PackageRevision Metadata 119 120 The package variant controller utilizes Porch APIs. This means that it is not 121 just doing a `clone` operation, but in fact creating a Porch PackageRevision 122 resource. In particular, that resource can contain Kubernetes metadata that is 123 not part of the package as stored in the repository. 124 125 Some of that metadata is necessary for the management of the PackageRevision 126 by the package variant controller - for example, the owner reference indicating 127 which PackageVariant created the PackageRevision. These are not under the user's 128 control. However, the PackageVariant resource does make the annotations and 129 labels of the PackageRevision available as values that may be controlled 130 during the creation of the PackageRevision. This can assist in additional 131 automation workflows. 132 133 ## Introducing Variance 134 Just cloning is not that interesting, so the PackageVariant resource also 135 allows you to control various ways of mutating the original package to create 136 the variant. 137 138 ### Package Context[^porch17] 139 Every kpt package that is fetched with `--for-deployment` will contain a 140 ConfigMap called `kptfile.kpt.dev`. Analogously, when Porch creates a package 141 in a deployment repository, it will create this ConfigMap, if it does not 142 already exist. Kpt (or Porch) will automatically add a key `name` to the 143 ConfigMap data, with the value of the package name. This ConfigMap can then 144 be used as input to functions in the Kpt function pipeline. 145 146 This process holds true for package revisions created via the package variant 147 controller as well. Additionally, the author of the PackageVariant resource 148 can specify additional key-value pairs to insert into the package 149 context, as shown in *Figure 2*. 150 151 |  | 152 | :---: | 153 | *Figure 2: Package Context Mutation * | 154 155 While this is convenient, it can be easily abused, leading to 156 over-parameterization. The preferred approach is configuration injection, as 157 described below, since it allows inputs to adhere to a well-defined, reusable 158 schema, rather than simple key/value pairs. 159 160 ### Kptfile Function Pipeline Editing[^porch18] 161 In the manual workflow, one of the ways we edit packages is by running KRM 162 functions imperatively. PackageVariant offers a similar capability, by 163 allowing the user to add functions to the beginning of the downstream package 164 `Kptfile` mutators pipeline. These functions will then execute before the 165 functions present in the upstream pipeline. It is not exactly the same as 166 running functions imperatively, because they will also be run in every 167 subsequent execution of the downstream package function pipeline. But it can 168 achieve the same goals. 169 170 For example, consider an upstream package that includes a Namespace resource. 171 In many organizations, the deployer of the workload may not have the permissions 172 to provision cluster-scoped resources like namespaces. This means that they 173 would not be able to use this upstream package without removing the Namespace 174 resource (assuming that they only have access to a pipeline that deploys with 175 constrained permissions). By adding a function that removes Namespace resources, 176 and a call to `set-namespace`, they can take advantage of the upstream package. 177 178 Similarly, the Kptfile pipeline editing feature provides an easy mechanism for 179 the deployer to create and set the namespace if their downstream package 180 application pipeline allows it, as seen in *Figure 3*.[^setns] 181 182 |  | 183 | :---: | 184 | *Figure 3: Kptfile Function Pipeline Editing * | 185 186 ### Configuration Injection[^porch18] 187 188 Adding values to the package context or functions to the pipeline works 189 for configuration that is under the control of the creator of the PackageVariant 190 resource. However, in more advanced use cases, we may need to specialize the 191 package based upon other contextual information. This particularly comes into 192 play when the user deploying the workload does not have direct control over the 193 context in which it is being deployed. For example, one part of the organization 194 may manage the infrastructure - that is, the cluster in which we are deploying 195 the workload - and another part the actual workload. We would like to be able to 196 pull in inputs specified by the infrastructure team automatically, based the 197 cluster to which we are deploying the workload, or perhaps the region in which 198 that cluster is deployed. 199 200 To facilitate this, the package variant controller can "inject" configuration 201 directly into the package. This means it will use information specific to this 202 instance of the package to lookup a resource in the Porch cluster, and copy that 203 information into the package. Of course, the package has to be ready to receive 204 this information. So, there is a protocol for facilitating this dance: 205 - Packages may contain resources annotated with `kpt.dev/config-injection` 206 - Often, these will also be `config.kubernetes.io/local-config` resources, as 207 they are likely just used by local functions as input. But this is not 208 mandatory. 209 - The package variant controller will look for any resource in the Kubernetes 210 cluster matching the Group, Version, and Kind of the package resource, and 211 satisfying the *injection selector*. 212 - The package variant controller will copy the `spec` field from the matching 213 in-cluster resource to the in-package resource, or the `data` field in the 214 case of a ConfigMap. 215 216 |  | 217 | :---: | 218 | *Figure 4: Configuration Injection* | 219 220 221 Note that because we are injecting data *from the Kubernetes cluster*, we can 222 also monitor that data for changes. For each resource we inject, the package 223 variant controller will establish a Kubernetes "watch" on the resource (or 224 perhaps on the collection of such resources). A change to that resource will 225 result in a new Draft package with the updated configuration injected. 226 227 There are a number of additional details that will be described in the detailed 228 design below, along with the specific API definition. 229 230 ## Lifecycle Management 231 232 ### Upstream Changes 233 The package variant controller allows you to specific a specific upstream 234 package revision to clone, or you can specify a floating tag[^notimplemented]. 235 236 If you specify a specific upstream revision, then the downstream will not be 237 changed unless the PackageVariant resource itself is modified to point to a new 238 revision. That is, the user must edit the PackageVariant, and change the 239 upstream package reference. When that is done, the package variant controller 240 will update any existing Draft package under its ownership by doing the 241 equivalent of a `kpt pkg update` to update the downstream to be based upon 242 the new upstream revision. If a Draft does not exist, then the package variant 243 controller will create a new Draft based on the current published downstream, 244 and apply the `kpt pkg update`. This updated Draft must then be proposed and 245 approved like any other package change. 246 247 If a floating tag is used, then explicit modification of the PackageVariant is 248 not needed. Rather, when the floating tag is moved to a new tagged revision of 249 the upstream package, the package revision controller will notice and 250 automatically propose and update to that revision. For example, the upstream 251 package author may designate three floating tags: stable, beta, and alpha. The 252 upstream package author can move these tags to specific revisions, and any 253 PackageVariant resource tracking them will propose updates to their downstream 254 packages. 255 256 ### Adoption and Deletion Policies 257 When a PackageVariant resource is created, it will have a particular 258 repository and package name as the downstream. The adoption policy controls 259 whether the package variant controller takes over an existing package with that 260 name, in that repository. 261 262 Analogously, when a PackageVariant resource is deleted, a decision must be 263 made about whether or not to delete the downstream package. This is controlled 264 by the deletion policy. 265 266 ## Fan Out of Variant Generation[^pvsimpl] 267 268 When used with a single package, the package variant controller mostly helps us 269 handle the time dimension - producing new versions of a package as the upstream 270 changes, or as injected resources are updated. It can also be useful for 271 automating common, systematic changes made when bringing an external package 272 into an organization, or an organizational package into a team repository. 273 274 That is useful, but not extremely compelling by itself. More interesting is when 275 we use PackageVariant as a primitive for automations that act on other 276 dimensions of scale. That means writing controllers that emit PackageVariant 277 resources. For example, we can create a controller that instantiates a 278 PackageVariant for each developer in our organization, or we can create 279 a controller to manage PackageVariants across environments. The ability to not 280 only clone a package, but make systematic changes to that package enables 281 flexible automation. 282 283 Workload controllers in Kubernetes are a useful analogy. In Kubernetes, we have 284 different workload controllers such as Deployment, StatefulSet, and DaemonSet. 285 Ultimately, all of these result in Pods; however, the decisions about what Pods 286 to create, how to schedule them across Nodes, how to configure those Pods, and 287 how to manage those Pods as changes happen are very different with each workload 288 controller. Similarly, we can build different controllers to handle different 289 ways in which we want to generate PackageRevisions. The PackageVariant 290 resource provides a convenient primitive for all of those controllers, allowing 291 a them to leverage a range of well-defined operations to mutate the packages as 292 needed. 293 294 A common need is the ability to generate many variants of a package based on 295 a simple list of some entity. Some examples include generating package variants 296 to spin up development environments for each developer in an organization; 297 instantiating the same package, with slight configuration changes, across a 298 fleet of clusters; or instantiating some package per customer. 299 300 The package variant set controller is designed to fill this common need. This 301 controller consumes PackageVariantSet resources, and outputs PackageVariant 302 resources. The PackageVariantSet defines: 303 - the upstream package 304 - targeting criteria 305 - a template for generating one PackageVariant per target 306 307 Three types of targeting are supported: 308 - An explicit list of repositories and package names 309 - A label selector for Repository objects 310 - An arbitrary object selector 311 312 Rules for generating a PackageVariant are associated with a list of targets 313 using a template. That template can have explicit values for various 314 PackageVariant fields, or it can use [Common Expression Language 315 (CEL)](https://github.com/google/cel-go) expressions to specify the field 316 values. 317 318 *Figure 5* shows an example of creating PackageVariant resources based upon the 319 explicitly list of repositories. In this example, for the `cluster-01` and 320 `cluster-02` repositories, no template is defined the resulting PackageVariants; 321 it simply takes the defaults. However, for `cluster-03`, a template is defined 322 to change the downstream package name to `bar`. 323 324 |  | 325 | :---: | 326 | *Figure 5: PackageVariantSet with Repository List* | 327 328 It is also possible to target the same package to a repository more than once, 329 using different names. This is useful, for example, if the package is used to 330 provision namespaces and you would like to provision many namespaces in the same 331 cluster. It is also useful if a repository is shared across multiple clusters. 332 In *Figure 6*, two PackageVariant resources for creating the `foo` package in 333 the repository `cluster-01` are generated, one for each listed package name. 334 Since no `packageNames` field is listed for `cluster-02`, only one instance is 335 created for that repository. 336 337 |  | 338 | :---: | 339 | *Figure 6: PackageVariantSet with Package List* | 340 341 *Figure 7* shows an example that combines a repository label selector with 342 configuration injection that various based upon the target. The template for the 343 PackageVariant includes a CEL expression for the one of the injectors, so that 344 the injection varies systematically based upon attributes of the target. 345 346 |  | 347 | :---: | 348 | *Figure 7: PackageVariantSet with Repository Selector* | 349 350 ## Detailed Design 351 352 ### PackageVariant API 353 354 The Go types below defines the `PackageVariantSpec`. 355 356 ```go 357 type PackageVariantSpec struct { 358 Upstream *Upstream `json:"upstream,omitempty"` 359 Downstream *Downstream `json:"downstream,omitempty"` 360 361 AdoptionPolicy AdoptionPolicy `json:"adoptionPolicy,omitempty"` 362 DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"` 363 364 Labels map[string]string `json:"labels,omitempty"` 365 Annotations map[string]string `json:"annotations,omitempty"` 366 367 PackageContext *PackageContext `json:"packageContext,omitempty"` 368 Pipeline *kptfilev1.Pipeline `json:"pipeline,omitempty"` 369 Injectors []InjectionSelector `json:"injectors,omitempty"` 370 } 371 372 type Upstream struct { 373 Repo string `json:"repo,omitempty"` 374 Package string `json:"package,omitempty"` 375 Revision string `json:"revision,omitempty"` 376 } 377 378 type Downstream struct { 379 Repo string `json:"repo,omitempty"` 380 Package string `json:"package,omitempty"` 381 } 382 383 type PackageContext struct { 384 Data map[string]string `json:"data,omitempty"` 385 RemoveKeys []string `json:"removeKeys,omitempty"` 386 } 387 388 type InjectionSelector struct { 389 Group *string `json:"group,omitempty"` 390 Version *string `json:"version,omitempty"` 391 Kind *string `json:"kind,omitempty"` 392 Name string `json:"name"` 393 } 394 395 ``` 396 397 #### Basic Spec Fields 398 399 The `Upstream` and `Downstream` fields specify the source package and 400 destination repository and package name. The `Repo` fields refer to the names 401 Porch Repository resources in the same namespace as the PackageVariant resource. 402 The `Downstream` does not contain a revision, because the package variant 403 controller will only create Draft packages. The `Revision` of the eventual 404 PackageRevision resource will be determined by Porch at the time of approval. 405 406 The `Labels` and `Annotations` fields list metadata to include on the created 407 PackageRevision. These values are set *only* at the time a Draft package is 408 created. They are ignored for subsequent operations, even if the PackageVariant 409 itself has been modified. This means users are free to change these values on 410 the PackageRevision; the package variant controller will not touch them again. 411 412 `AdoptionPolicy` controls how the package variant controller behaves if it finds 413 an existing PackageRevision Draft matching the `Downstream`. If the 414 `AdoptionPolicy` is `adoptExisting`, then the package variant controller will 415 take ownership of the Draft, associating it with this PackageVariant. This means 416 that it will begin to reconcile the Draft, just as if it had created it in the 417 first place. An `AdoptionPolicy` of `adoptNone` (the default) will simply ignore 418 any matching Drafts that were not created by the controller. 419 420 `DeletionPolicy` controls how the package variant controller behaves with 421 respect to PackageRevisions that it has created when the PackageVariant resource 422 itself is deleted. A value of `delete` (the default) will delete the 423 PackageRevision (potentially removing it from a running cluster, if the 424 downstream package has been deployed). A value of `orphan` will remove the owner 425 references and leave the PackageRevisions in place. 426 427 #### Package Context Injection 428 429 PackageVariant resource authors may specify key-value pairs in the 430 `spec.packageContext.data` field of the resource. These key-value pairs will be 431 automatically added to the `data` of the `kptfile.kpt.dev` ConfigMap, if it 432 exists. 433 434 Specifying the key `name` is invalid and must fail validation of the 435 PackageVariant. This key is reserved for kpt or Porch to set to the package 436 name. Similarly, `package-path` is reserved and will result in an error. 437 438 The `spec.packageContext.removeKeys` field can also be used to specify a list of 439 keys that the package variant controller should remove from the `data` field of 440 the `kptfile.kpt.dev` ConfigMap. 441 442 When creating or updating a package, the package variant controller will ensure 443 that: 444 - The `kptfile.kpt.dev` ConfigMap exists, failing if not 445 - All of the key-value pairs in `spec.packageContext.data` exist in the `data` 446 field of the ConfigMap. 447 - None of the keys listed in `spec.packageContext.removeKeys` exist in the 448 ConfigMap. 449 450 Note that if a user adds a key via PackageVariant, then changes the 451 PackageVariant to no longer add that key, it will NOT be removed automatically, 452 unless the user also lists the key in the `removeKeys` list. This avoids the 453 need to track which keys were added by PackageVariant. 454 455 Similarly, if a user manually adds a key in the downstream that is also listed 456 in the `removeKeys` field, the package variant controller will remove that key 457 the next time it needs to update the downstream package. There will be no 458 attempt to coordinate "ownership" of these keys. 459 460 If the controller is unable to modify the ConfigMap for some reason, this is 461 considered an error and should prevent generation of the Draft. This will result 462 in the condition `Ready` being set to `False`. 463 464 #### Kptfile Function Pipeline Editing 465 466 PackageVariant resource creators may specify a list of KRM functions to add to 467 the beginning of the Kptfile's pipeline. These functions are listed in the field 468 `spec.pipeline`, which is a 469 [Pipeline](https://github.com/GoogleContainerTools/kpt/blob/cf1f326486214f6b4469d8432287a2fa705b48f5/pkg/api/kptfile/v1/types.go#L236), 470 just as in the Kptfile. The user can therefore prepend both `validators` and 471 `mutators`. 472 473 Functions added in this way are always added to the *beginning* of the Kptfile 474 pipeline. In order to enable management of the list on subsequent 475 reconciliations, functions added by the package variant controller will use the 476 `Name` field of the 477 [Function](https://github.com/GoogleContainerTools/kpt/blob/cf1f326486214f6b4469d8432287a2fa705b48f5/pkg/api/kptfile/v1/types.go#L283). 478 In the Kptfile, each function will be named as the dot-delimited concatenation 479 of `PackageVariant`, the name of the PackageVariant resource, the function name 480 as specified in the pipeline of the PackageVariant resource (if present), and 481 the positional location of the function in the array. 482 483 For example, if the PackageVariant resource contains: 484 485 ```yaml 486 apiVersion: config.porch.kpt.dev/v1alpha1 487 kind: PackageVariant 488 metadata: 489 namespace: default 490 name: my-pv 491 spec: 492 ... 493 pipeline: 494 mutators: 495 - image: gcr.io/kpt-fn/set-namespace:v0.1 496 configMap: 497 namespace: my-ns 498 name: my-func 499 - image: gcr.io/kpt-fn/set-labels:v0.1 500 configMap: 501 app: foo 502 ``` 503 504 Then the resulting Kptfile will have these two entries prepended to its 505 `mutators` list: 506 507 ```yaml 508 pipeline: 509 mutators: 510 - image: gcr.io/kpt-fn/set-namespace:v0.1 511 configMap: 512 namespace: my-ns 513 name: PackageVariant.my-pv.my-func.0 514 - image: gcr.io/kpt-fn/set-labels:v0.1 515 configMap: 516 app: foo 517 name: PackageVariant.my-pv..1 518 ``` 519 520 During subsequent reconciliations, this allows the controller to identify the 521 functions within its control, remove them all, and re-add them based on its 522 updated content. By including the PackageVariant name, we enable chains of 523 PackageVariants to add functions, so long as the user is careful about their 524 choice of resource names and avoids conflicts. 525 526 If the controller is unable to modify the Pipeline for some reason, this is 527 considered an error and should prevent generation of the Draft. This will result 528 in the condition `Ready` being set to `False`. 529 530 #### Configuration Injection Details 531 532 As described [above](#configuration-injection), configuration injection is a 533 process whereby in-package resources are matched to in-cluster resources, and 534 the `spec` of the in-cluster resources is copied to the in-package resource. 535 536 Configuration injection is controlled by a combination of in-package resources 537 with annotations, and *injectors* (also known as *injection selectors*) defined 538 on the PackageVariant resource. Package authors control the injection points 539 they allow in their packages, by flagging specific resources as *injection 540 points* with an annotation. Creators of the PackageVariant resource specify how 541 to map in-cluster resources to those injection points using the injection 542 selectors. Injection selectors are defined in the `spec.injectors` field of the 543 PackageVariant. This field is an ordered array of structs containing a GVK 544 (group, version, kind) tuple as separate fields, and name. Only the name is 545 required. To identify a match, all fields present must match the in-cluster 546 object, and all *GVK* fields present must match the in-package resource. In 547 general the name will not match the in-package resource; this is discussed in 548 more detail below. 549 550 The annotations, along with the GVK of the annotated resource, allow a package 551 to "advertise" the injections it can accept and understand. These injection 552 points effectively form a configuration API for the package, and the injection 553 selectors provide a way for the PackageVariant author to specify the inputs 554 for those APIs from the possible values in the management cluster. If we define 555 those APIs carefully, they can be used across many packages; since they are 556 KRM resources, we can apply versioning and schema validation to them as well. 557 This creates a more maintainable, automatable set of APIs for package 558 customization than simple key/value pairs. 559 560 As an example, we may define a GVK that contains service endpoints that many 561 applications use. In each application package, we would then include an instance 562 of that resource, say called "service-endpoints", and configure a function to 563 propagate the values from that resource to others within our package. As those 564 endpoints may vary by region, in our Porch cluster we can create an instance of 565 this GVK for each region: "useast1-service-endpoints", 566 "useast2-service-endpoints", "uswest1-service-endpoints", etc. When we 567 instantiate the PackageVariant for a cluster, we want to inject the resource 568 corresponding to the region in which the cluster exists. Thus, for each cluster 569 we will create a PackageVariant resource pointing to the upstream package, but 570 with injection selector name values that are specific to the region for that 571 cluster. 572 573 It is important to realize that the name of the in-package resource and the in- 574 cluster resource need not match. In fact, it would be an unusual coincidence if 575 they did match. The names in the package are the same across PackageVariants 576 using that upstream, but we want to inject different resources for each one such 577 PackageVariant. We also do not want to change the name in the package, because 578 it likely has meaning within the package and will be used by functions in the 579 package. Also, different owners control the names of the in-package and in- 580 cluster resources. The names in the package are in the control of the package 581 author. The names in the cluster are in the control of whoever populates the 582 cluster (for example, some infrastructure team). The selector is the glue 583 between them, and is in control of the PackageVariant resource creator. 584 585 The GVK on the other hand, has to be the same for the in-package resource and 586 the in-cluster resource, because it tells us the API schema for the resource. 587 Also, the namespace of the in-cluster object needs to be the same as the 588 PackageVariant resource, or we could leak resources from namespaces to which 589 our PackageVariant user does not have access. 590 591 With that understanding, the injection process works as follows: 592 593 1. The controller will examine all in-package resources, looking for those with 594 an annotation named `kpt.dev/config-injection`, with one of the following 595 values: `required` or `optional`. We will call these "injection points". It 596 is the responsibility of the package author to define these injection points, 597 and to specify which are required and which are optional. Optional injection 598 points are a way of specifying default values. 599 1. For each injection point, a condition will be created *in the 600 downstream PackageRevision*, with ConditionType set to the dot-delimited 601 concatenation of `config.injection`, with the in-package resource kind and 602 name, and the value set to `False`. Note that since the package author 603 controls the name of the resource, kind and name are sufficient to 604 disambiguate the injection point. We will call this ConditionType the 605 "injection point ConditionType". 606 1. For each required injection point, the injection point ConditionType will 607 be added to the PackageRevision `readinessGates` by the package variant 608 controller. Optional injection points' ConditionTypes must not be added to 609 the `readinessGates` by the package variant controller, but humans or other 610 actors may do so at a later date, and the package variant controller should 611 not remove them on subsequent reconciliations. Also, this relies upon 612 `readinessGates` gating publishing the package to a *deployment* repository, 613 but not gating publishing to a blueprint repository. 614 1. The injection processing will proceed as follows. For each injection point: 615 - The controller will identify all in-cluster objects in the same 616 namespace as the PackageVariant resource, with GVK matching the injection 617 point (the in-package resource). If the controller is unable to load this 618 objects (e.g., there are none and the CRD is not installed), the injection 619 point ConditionType will be set to `False`, with a message indicating that 620 the error, and processing proceeds to the next injection point. Note that 621 for `optional` injection this may be an acceptable outcome, so it does not 622 interfere with overall generation of the Draft. 623 - The controller will look through the list of injection selectors in 624 order and checking if any of the in-cluster objects match the selector. If 625 so, that in-cluster object is selected, and processing of the list of 626 injection selectors stops. Note that the namespace is set based upon the 627 PackageVariant resource, the GVK is set based upon the in-package resource, 628 and all selectors require name. Thus, at most one match is possible for any 629 given selector. Also note that *all fields present in the selector* must 630 match the in-cluster resource, and only the *GVK fields present in the 631 selector* must match the in-package resource. 632 - If no in-cluster object is selected, the injection point ConditionType will 633 be set to `False` with a message that no matching in-cluster resource was 634 found, and processing proceeds to the next injection point. 635 - If a matching in-cluster object is selected, then it is injected as 636 follows: 637 - For ConfigMap resources, the `data` field from the in-cluster resource is 638 copied to the `data` field of the in-package resource (the injection 639 point), overwriting it. 640 - For other resource types, the `spec` field from the in-cluster resource 641 is copied to the `spec` field of the in-package resource (the injection 642 point), overwriting it. 643 - An annotation with name `kpt.dev/injected-resource-name` and value set to 644 the name of the in-cluster resource is added (or overwritten) in the 645 in-package resource. 646 647 If the the overall injection cannot be completed for some reason, or if any of 648 the below problems exist in the upstream package, it is considered an error and 649 should prevent generation of the Draft: 650 - There is a resource annotated as an injection point but having an invalid 651 annotation value (i.e., other than `required` or `optional`). 652 - There are ambiguous condition types due to conflicting GVK and name 653 values. These must be disambiguated in the upstream package, if so. 654 655 This will result in the condition `Ready` being set to `False`. 656 657 Note that whether or not all `required` injection points are fulfilled does not 658 affect the *PackageVariant* conditions, only the *PackageRevision* conditions. 659 660 **A Further Note on Selectors** 661 662 Note that by allowing the use of GVK, not just name, in the selector, more 663 precision in selection is enabled. This is a way to constrain the injections 664 that will be done. That is, if the package has 10 different objects with 665 `config-injection` annotation, the PackageVariant could say it only wants to 666 replace certain GVKs, allowing better control. 667 668 Consider, for example, if the cluster contains these resources: 669 670 - GVK1 foo 671 - GVK1 bar 672 - GVK2 foo 673 - GVK2 bar 674 675 If we could only define injection selectors based upon name, it would be 676 impossible to ever inject one GVK with `foo` and another with `bar`. Instead, 677 by using GVK, we can accomplish this with a list of selectors like: 678 679 - GVK1 foo 680 - GVK2 bar 681 682 That said, often name will be sufficiently unique when combined with the 683 in-package resource GVK, and so making the selector GVK optional is more 684 convenient. This allows a single injector to apply to multiple injection points 685 with different GVKs. 686 687 #### Order of Mutations 688 689 During creation, the first thing the controller does is clone the upstream 690 package to create the downstream package. 691 692 For update, first note that changes to the downstream PackageRevision can be 693 triggered for several different reasons: 694 695 1. The PackageVariant resource is updated, which could change any of the options 696 for introducing variance, or could also change the upstream package revision 697 referenced. 698 1. A new revision of the upstream package has been selected due to a floating 699 tag change, or due to a force retagging of the upstream. 700 1. An injected in-cluster object is updated. 701 702 The downstream PackageRevision may have been updated by humans or other 703 automation actors since creation, so we cannot simply recreate the downstream 704 PackageRevision from scratch when one of these changes happens. Instead, the 705 controller must maintain the later edits by doing the equivalent of a `kpt pkg 706 update`, in the case of changes to the upstream for any reason. Any other 707 changes require reapplication of the PackageVariant functionality. With that 708 understanding, we can see that the controller will perform mutations on the 709 downstream package in this order, for both creation and update: 710 711 1. Create (via Clone) or Update (via `kpt pkg update` equivalent) 712 - This is done by the Porch server, not by the package variant controller 713 directly. 714 - This means that Porch will run the Kptfile pipeline after clone or 715 update. 716 1. Package variant controller applies configured mutations 717 - Package Context Injections 718 - Kptfile KRM Function Pipeline Additions/Changes 719 - Config Injection 720 1. Package variant controller saves the PackageRevision and 721 PackageRevisionResources. 722 - Porch server executes the Kptfile pipeline 723 724 The package variant controller mutations edit resources (including the Kptfile), 725 based on the contents of the PackageVariant and the injected in-cluster 726 resources, but cannot affect one another. The results of those mutations 727 throughout the rest of the package is materialized by the execution of the 728 Kptfile pipeline during the save operation. 729 730 #### PackageVariant Status 731 732 PackageVariant sets the following status conditions: 733 - `Stalled` is set to True if there has been a failure that most likely 734 requires user intervention. 735 - `Ready` is set to True if the last reconciliation successfully produced an 736 up-to-date Draft. 737 738 The PackageVariant resource will also contain a `DownstreamTargets` field, 739 containing a list of downstream `Draft` and `Proposed` PackageRevisions owned by 740 this PackageVariant resource, or the latest `Published` PackageRevision if there 741 are none in `Draft` or `Proposed` state. Typically, there is only a single 742 Draft, but use of the `adopt` value for `AdoptionPolicy` could result in 743 multiple Drafts being owned by the same PackageVariant. 744 745 ### PackageVariantSet API[^pvsimpl] 746 747 The Go types below defines the `PackageVariantSetSpec`. 748 749 ```go 750 // PackageVariantSetSpec defines the desired state of PackageVariantSet 751 type PackageVariantSetSpec struct { 752 Upstream *pkgvarapi.Upstream `json:"upstream,omitempty"` 753 Targets []Target `json:"targets,omitempty"` 754 } 755 756 type Target struct { 757 // Exactly one of Repositories, RepositorySeletor, and ObjectSelector must be 758 // populated 759 // option 1: an explicit repositories and package names 760 Repositories []RepositoryTarget `json:"repositories,omitempty"` 761 762 // option 2: a label selector against a set of repositories 763 RepositorySelector *metav1.LabelSelector `json:"repositorySelector,omitempty"` 764 765 // option 3: a selector against a set of arbitrary objects 766 ObjectSelector *ObjectSelector `json:"objectSelector,omitempty"` 767 768 // Template specifies how to generate a PackageVariant from a target 769 Template *PackageVariantTemplate `json:"template,omitempty"` 770 } 771 ``` 772 773 At the highest level, a PackageVariantSet is just an upstream, and a list of 774 targets. For each target, there is a set of criteria for generating a list, and 775 a set of rules (a template) for creating a PackageVariant from each list entry. 776 777 Since `template` is optional, lets start with describing the different types of 778 targets, and how the criteria in each is used to generate a list that seeds the 779 PackageVariant resources. 780 781 The `Target` structure must include exactly one of three different ways of 782 generating the list. The first is a simple list of repositories and package 783 names for each of those repositories[^repo-pkg-expr]. The package name list is 784 needed for uses cases in which you want to repeatedly instantiate the same 785 package in a single repository. For example, if a repository represents the 786 contents of a cluster, you may want to instantiate a namespace package once for 787 each namespace, with a name matching the namespace. 788 789 This example shows using the `repositories` field: 790 791 ```yaml 792 apiVersion: config.porch.kpt.dev/v1alpha2 793 kind: PackageVariantSet 794 metadata: 795 namespace: default 796 name: example 797 spec: 798 upstream: 799 repo: example-repo 800 package: foo 801 revision: v1 802 targets: 803 - repositories: 804 - name: cluster-01 805 - name: cluster-02 806 - name: cluster-03 807 packageNames: 808 - foo-a 809 - foo-b 810 - foo-c 811 - name: cluster-04 812 packageNames: 813 - foo-a 814 - foo-b 815 ``` 816 817 In this case, PackageVariant resources are created for each of these pairs of 818 downstream repositories and packages names: 819 820 | Repository | Package Name | 821 | ---------- | ------------ | 822 | cluster-01 | foo | 823 | cluster-02 | foo | 824 | cluster-03 | foo-a | 825 | cluster-03 | foo-b | 826 | cluster-03 | foo-c | 827 | cluster-04 | foo-a | 828 | cluster-04 | foo-b | 829 830 All of those PackageVariants have the same upstream. 831 832 The second criteria targeting is via a label selector against Porch Repository 833 objects, along with a list of package names. Those packages will be instantiated 834 in each matching repository. Just like in the first example, not listing a 835 package name defaults to one package, with the same name as the upstream 836 package. Suppose, for example, we have these four repositories defined in our 837 Porch cluster: 838 839 | Repository | Labels | 840 | ---------- | ------------------------------------- | 841 | cluster-01 | region=useast1, env=prod, org=hr | 842 | cluster-02 | region=uswest1, env=prod, org=finance | 843 | cluster-03 | region=useast2, env=prod, org=hr | 844 | cluster-04 | region=uswest1, env=prod, org=hr | 845 846 If we create a PackageVariantSet with the following `spec`: 847 848 ```yaml 849 spec: 850 upstream: 851 repo: example-repo 852 package: foo 853 revision: v1 854 targets: 855 - repositorySelector: 856 matchLabels: 857 env: prod 858 org: hr 859 - repositorySelector: 860 matchLabels: 861 region: uswest1 862 packageNames: 863 - foo-a 864 - foo-b 865 - foo-c 866 ``` 867 868 then PackageVariant resources will be created with these repository and package 869 names: 870 871 | Repository | Package Name | 872 | ---------- | ------------ | 873 | cluster-01 | foo | 874 | cluster-03 | foo | 875 | cluster-04 | foo | 876 | cluster-02 | foo-a | 877 | cluster-02 | foo-b | 878 | cluster-02 | foo-c | 879 | cluster-04 | foo-a | 880 | cluster-04 | foo-b | 881 | cluster-04 | foo-c | 882 883 Finally, the third possibility allows the use of *arbitrary* resources in the 884 Porch cluster as targeting criteria. The `objectSelector` looks like this: 885 886 ```yaml 887 spec: 888 upstream: 889 repo: example-repo 890 package: foo 891 revision: v1 892 targets: 893 - objectSelector: 894 apiVersion: krm-platform.bigco.com/v1 895 kind: Team 896 matchLabels: 897 org: hr 898 role: dev 899 ``` 900 901 It works exactly like the repository selector - in fact the repository selector 902 is equivalent to the object selector with the `apiVersion` and `kind` values set 903 to point to Porch Repository resources. That is, the repository name comes from 904 the object name, and the package names come from the listed package names. In 905 the description of the template, we will see how to derive different repository 906 names from the objects. 907 908 #### PackageVariant Template 909 910 As previously discussed, the list entries generated by the target criteria 911 result in PackageVariant entries. If no template is specified, then 912 PackageVariant default are used, along with the downstream repository name and 913 package name as described in the previous section. The template allows the user 914 to have control over all of the values in the resulting PackageVariant. The 915 template API is shown below. 916 917 ```go 918 type PackageVariantTemplate struct { 919 // Downstream allows overriding the default downstream package and repository name 920 // +optional 921 Downstream *DownstreamTemplate `json:"downstream,omitempty"` 922 923 // AdoptionPolicy allows overriding the PackageVariant adoption policy 924 // +optional 925 AdoptionPolicy *pkgvarapi.AdoptionPolicy `json:"adoptionPolicy,omitempty"` 926 927 // DeletionPolicy allows overriding the PackageVariant deletion policy 928 // +optional 929 DeletionPolicy *pkgvarapi.DeletionPolicy `json:"deletionPolicy,omitempty"` 930 931 // Labels allows specifying the spec.Labels field of the generated PackageVariant 932 // +optional 933 Labels map[string]string `json:"labels,omitempty"` 934 935 // LabelsExprs allows specifying the spec.Labels field of the generated PackageVariant 936 // using CEL to dynamically create the keys and values. Entries in this field take precedent over 937 // those with the same keys that are present in Labels. 938 // +optional 939 LabelExprs []MapExpr `json:"labelExprs,omitempty"` 940 941 // Annotations allows specifying the spec.Annotations field of the generated PackageVariant 942 // +optional 943 Annotations map[string]string `json:"annotations,omitempty"` 944 945 // AnnotationsExprs allows specifying the spec.Annotations field of the generated PackageVariant 946 // using CEL to dynamically create the keys and values. Entries in this field take precedent over 947 // those with the same keys that are present in Annotations. 948 // +optional 949 AnnotationExprs []MapExpr `json:"annotationExprs,omitempty"` 950 951 // PackageContext allows specifying the spec.PackageContext field of the generated PackageVariant 952 // +optional 953 PackageContext *PackageContextTemplate `json:"packageContext,omitempty"` 954 955 // Pipeline allows specifying the spec.Pipeline field of the generated PackageVariant 956 // +optional 957 Pipeline *PipelineTemplate `json:"pipeline,omitempty"` 958 959 // Injectors allows specifying the spec.Injectors field of the generated PackageVariant 960 // +optional 961 Injectors []InjectionSelectorTemplate `json:"injectors,omitempty"` 962 } 963 964 // DownstreamTemplate is used to calculate the downstream field of the resulting 965 // package variants. Only one of Repo and RepoExpr may be specified; 966 // similarly only one of Package and PackageExpr may be specified. 967 type DownstreamTemplate struct { 968 Repo *string `json:"repo,omitempty"` 969 Package *string `json:"package,omitempty"` 970 RepoExpr *string `json:"repoExpr,omitempty"` 971 PackageExpr *string `json:"packageExpr,omitempty"` 972 } 973 974 // PackageContextTemplate is used to calculate the packageContext field of the 975 // resulting package variants. The plain fields and Exprs fields will be 976 // merged, with the Exprs fields taking precedence. 977 type PackageContextTemplate struct { 978 Data map[string]string `json:"data,omitempty"` 979 RemoveKeys []string `json:"removeKeys,omitempty"` 980 DataExprs []MapExpr `json:"dataExprs,omitempty"` 981 RemoveKeyExprs []string `json:"removeKeyExprs,omitempty"` 982 } 983 984 // InjectionSelectorTemplate is used to calculate the injectors field of the 985 // resulting package variants. Exactly one of the Name and NameExpr fields must 986 // be specified. The other fields are optional. 987 type InjectionSelectorTemplate struct { 988 Group *string `json:"group,omitempty"` 989 Version *string `json:"version,omitempty"` 990 Kind *string `json:"kind,omitempty"` 991 Name *string `json:"name,omitempty"` 992 993 NameExpr *string `json:"nameExpr,omitempty"` 994 } 995 996 // MapExpr is used for various fields to calculate map entries. Only one of 997 // Key and KeyExpr may be specified; similarly only on of Value and ValueExpr 998 // may be specified. 999 type MapExpr struct { 1000 Key *string `json:"key,omitempty"` 1001 Value *string `json:"value,omitempty"` 1002 KeyExpr *string `json:"keyExpr,omitempty"` 1003 ValueExpr *string `json:"valueExpr,omitempty"` 1004 } 1005 1006 // PipelineTemplate is used to calculate the pipeline field of the resulting 1007 // package variants. 1008 type PipelineTemplate struct { 1009 // Validators is used to caculate the pipeline.validators field of the 1010 // resulting package variants. 1011 // +optional 1012 Validators []FunctionTemplate `json:"validators,omitempty"` 1013 1014 // Mutators is used to caculate the pipeline.mutators field of the 1015 // resulting package variants. 1016 // +optional 1017 Mutators []FunctionTemplate `json:"mutators,omitempty"` 1018 } 1019 1020 // FunctionTemplate is used in generating KRM function pipeline entries; that 1021 // is, it is used to generate Kptfile Function objects. 1022 type FunctionTemplate struct { 1023 kptfilev1.Function `json:",inline"` 1024 1025 // ConfigMapExprs allows use of CEL to dynamically create the keys and values in the 1026 // function config ConfigMap. Entries in this field take precedent over those with 1027 // the same keys that are present in ConfigMap. 1028 // +optional 1029 ConfigMapExprs []MapExpr `json:"configMapExprs,omitempty"` 1030 } 1031 ``` 1032 1033 This is a pretty complicated structure. To make it more understandable, the 1034 first thing to notice is that many fields have a plain version, and an `Expr` 1035 version. The plain version is used when the value is static across all the 1036 PackageVariants; the `Expr` version is used when the value needs to vary across 1037 PackageVariants. 1038 1039 Let's consider a simple example. Suppose we have a package for provisioning 1040 namespaces called "base-ns". We want to instantiate this several times in the 1041 `cluster-01` repository. We could do this with this PackageVariantSet: 1042 1043 ```yaml 1044 apiVersion: config.porch.kpt.dev/v1alpha2 1045 kind: PackageVariantSet 1046 metadata: 1047 namespace: default 1048 name: example 1049 spec: 1050 upstream: 1051 repo: platform-catalog 1052 package: base-ns 1053 revision: v1 1054 targets: 1055 - repositories: 1056 - name: cluster-01 1057 packageNames: 1058 - ns-1 1059 - ns-2 1060 - ns-3 1061 ``` 1062 1063 That will produce three PackageVariant resources with the same upstream, all 1064 with the same downstream repo, and each with a different downstream package 1065 name. If we also want to set some labels identically across the packages, we can 1066 do that with the `template.labels` field: 1067 1068 ```yaml 1069 apiVersion: config.porch.kpt.dev/v1alpha2 1070 kind: PackageVariantSet 1071 metadata: 1072 namespace: default 1073 name: example 1074 spec: 1075 upstream: 1076 repo: platform-catalog 1077 package: base-ns 1078 revision: v1 1079 targets: 1080 - repositories: 1081 - name: cluster-01 1082 packageNames: 1083 - ns-1 1084 - ns-2 1085 - ns-3 1086 template: 1087 labels: 1088 package-type: namespace 1089 org: hr 1090 ``` 1091 1092 The resulting PackageVariant resources will include `labels` in their `spec`, 1093 and will be identical other than their names and the `downstream.package`: 1094 1095 ```yaml 1096 apiVersion: config.porch.kpt.dev/v1alpha1 1097 kind: PackageVariant 1098 metadata: 1099 namespace: default 1100 name: example-aaaa 1101 spec: 1102 upstream: 1103 repo: platform-catalog 1104 package: base-ns 1105 revision: v1 1106 downstream: 1107 repo: cluster-01 1108 package: ns-1 1109 labels: 1110 package-type: namespace 1111 org: hr 1112 --- 1113 apiVersion: config.porch.kpt.dev/v1alpha1 1114 kind: PackageVariant 1115 metadata: 1116 namespace: default 1117 name: example-aaab 1118 spec: 1119 upstream: 1120 repo: platform-catalog 1121 package: base-ns 1122 revision: v1 1123 downstream: 1124 repo: cluster-01 1125 package: ns-2 1126 labels: 1127 package-type: namespace 1128 org: hr 1129 --- 1130 1131 apiVersion: config.porch.kpt.dev/v1alpha1 1132 kind: PackageVariant 1133 metadata: 1134 namespace: default 1135 name: example-aaac 1136 spec: 1137 upstream: 1138 repo: platform-catalog 1139 package: base-ns 1140 revision: v1 1141 downstream: 1142 repo: cluster-01 1143 package: ns-3 1144 labels: 1145 package-type: namespace 1146 org: hr 1147 ``` 1148 1149 When using other targeting means, the use of the `Expr` fields becomes more 1150 likely, because we have more possible sources for different field values. The 1151 `Expr` values are all [Common Expression Language (CEL)](https://github.com/google/cel-go) 1152 expressions, rather than static values. This allows the user to construct values 1153 based upon various fields of the targets. Consider again the 1154 `repositorySelector` example, where we have these repositories in the cluster. 1155 1156 | Repository | Labels | 1157 | ---------- | ------------------------------------- | 1158 | cluster-01 | region=useast1, env=prod, org=hr | 1159 | cluster-02 | region=uswest1, env=prod, org=finance | 1160 | cluster-03 | region=useast2, env=prod, org=hr | 1161 | cluster-04 | region=uswest1, env=prod, org=hr | 1162 1163 If we create a PackageVariantSet with the following `spec`, we can use the 1164 `Expr` fields to add labels to the PackageVariantSpecs (and thus to the 1165 resulting PackageRevisions later) that vary based on cluster. We can also use 1166 this to vary the `injectors` defined for each PackageVariant, resulting in each 1167 PackageRevision having different resources injected. This `spec`: 1168 1169 ```yaml 1170 spec: 1171 upstream: 1172 repo: example-repo 1173 package: foo 1174 revision: v1 1175 targets: 1176 - repositorySelector: 1177 matchLabels: 1178 env: prod 1179 org: hr 1180 template: 1181 labelExprs: 1182 key: org 1183 valueExpr: "repository.labels['org']" 1184 injectorExprs: 1185 - nameExpr: "repository.labels['region'] + '-endpoints'" 1186 ``` 1187 1188 will result in three PackageVariant resources, one for each Repository with the 1189 labels env=prod and org=hr. The `labels` and `injectors` fields of the 1190 PackageVariantSpec will be different for each of these PackageVariants, as 1191 determined by the use of the `Expr` fields in the template, as shown here: 1192 1193 ```yaml 1194 apiVersion: config.porch.kpt.dev/v1alpha1 1195 kind: PackageVariant 1196 metadata: 1197 namespace: default 1198 name: example-aaaa 1199 spec: 1200 upstream: 1201 repo: example-repo 1202 package: foo 1203 revision: v1 1204 downstream: 1205 repo: cluster-01 1206 package: foo 1207 labels: 1208 org: hr 1209 injectors: 1210 name: useast1-endpoints 1211 --- 1212 apiVersion: config.porch.kpt.dev/v1alpha1 1213 kind: PackageVariant 1214 metadata: 1215 namespace: default 1216 name: example-aaab 1217 spec: 1218 upstream: 1219 repo: example-repo 1220 package: foo 1221 revision: v1 1222 downstream: 1223 repo: cluster-03 1224 package: foo 1225 labels: 1226 org: hr 1227 injectors: 1228 name: useast2-endpoints 1229 --- 1230 apiVersion: config.porch.kpt.dev/v1alpha1 1231 kind: PackageVariant 1232 metadata: 1233 namespace: default 1234 name: example-aaac 1235 spec: 1236 upstream: 1237 repo: example-repo 1238 package: foo 1239 revision: v1 1240 downstream: 1241 repo: cluster-04 1242 package: foo 1243 labels: 1244 org: hr 1245 injectors: 1246 name: uswest1-endpoints 1247 ``` 1248 1249 Since the injectors are different for each PackageVariant, the resulting 1250 PackageRevisions will each have different resources injected. 1251 1252 When CEL expressions are evaluated, they have an environment associated with 1253 them. That is, there are certain objects that are accessible within the CEL 1254 expression. For CEL expressions used in the PackageVariantSet `template` field, 1255 the following variables are available: 1256 1257 | CEL Variable | Variable Contents | 1258 | -------------- | ------------------------------------------------------------ | 1259 | repoDefault | The default repository name based on the targeting criteria. | 1260 | packageDefault | The default package name based on the targeting criteria. | 1261 | upstream | The upstream PackageRevision. | 1262 | repository | The downstream Repository. | 1263 | target | The target object (details vary; see below). | 1264 1265 There is one expression that is an exception to the table above. Since the 1266 `repository` value corresponds to the Repository of the downstream, we must 1267 first evaluate the `downstream.repoExpr` expression to *find* that 1268 repository. Thus, for that expression only, `repository` is not a valid 1269 variable. 1270 1271 There is one more variable available across all CEL expressions: the `target` 1272 variable. This variable has a meaning that varies depending on the type of 1273 target, as follows: 1274 1275 | Target Type | `target` Variable Contents | 1276 | ------------------- | -------------------------- | 1277 | Repo/Package List | A struct with two fields: `repo` and `package`, the same as the `repoDefault` and `packageDefault` values. | 1278 | Repository Selector | The Repository selected by the selector. Although not recommended, this could be different than the `repository` value, which can be altered with `downstream.repo` or `downstream.repoExpr`. | 1279 | Object Selector | The Object selected by the selector. | 1280 1281 For the various resource variables - `upstream`, `repository`, and `target` - 1282 arbitrary access to all fields of the object could lead to security concerns. 1283 Therefore, only a subset of the data is available for use in CEL expressions. 1284 Specifically, the following fields: `name`, `namespace`, `labels`, and 1285 `annotations`. 1286 1287 Given the slight quirk with the `repoExpr`, it may be helpful to state the 1288 processing flow for the template evaluation: 1289 1290 1. The upstream PackageRevision is loaded. It must be in the same namespace as 1291 the PackageVariantSet[^multi-ns-reg]. 1292 1. The targets are determined. 1293 1. For each target: 1294 1. The CEL environment is prepared with `repoDefault`, `packageDefault`, 1295 `upstream`, and `target` variables. 1296 1. The downstream repository is determined and loaded, as follows: 1297 - If present, `downstream.repoExpr` is evaluated using the CEL 1298 environment, and the result used as the downstream repository name. 1299 - Otherwise, if `downstream.repo` is set, that is used as the downstream 1300 repository name. 1301 - If neither is present, the default repository name based on the target is 1302 used (i.e., the same value as the `repoDefault` variable). 1303 - The resulting downstream repository name is used to load the corresponding 1304 Repository object in the same namespace as the PackageVariantSet. 1305 1. The downstream Repository is added to the CEL environment. 1306 1. All other CEL expressions are evaluated. 1307 1. Note that if any of the resources (e.g., the upstream PackageRevision, or the 1308 downstream Repository) are not found our otherwise fail to load, processing 1309 stops and a failure condition is raised. Similarly, if a CEL expression 1310 cannot be properly evaluated due to syntax or other reasons, processing stops 1311 and a failure condition is raised. 1312 1313 #### Other Considerations 1314 It would appear convenient to automatically inject the PackageVariantSet 1315 targeting resource. However, it is better to require the package advertise 1316 the ways it accepts injections (i.e., the GVKs it understands), and only inject 1317 those. This keeps the separation of concerns cleaner; the package does not 1318 build in an awareness of the context in which it expects to be deployed. For 1319 example, a package should not accept a Porch Repository resource just because 1320 that happens to be the targeting mechanism. That would make the package unusable 1321 in other contexts. 1322 1323 #### PackageVariantSet Status 1324 1325 The PackageVariantSet status uses these conditions: 1326 - `Stalled` is set to True if there has been a failure that most likely 1327 requires user intervention. 1328 - `Ready` is set to True if the last reconciliation successfully reconciled 1329 all targeted PackageVariant resources. 1330 1331 ## Future Considerations 1332 - As an alternative to the floating tag proposal, we may instead want to have 1333 a separate tag tracking controller that can update PV and PVS resources to 1334 tweak their upstream as the tag moves. 1335 - Installing a collection of packages across a set of clusters, or performing 1336 the same mutations to each package in a collection, is only supported by 1337 creating multiple PackageVariant / PackageVariantSet resources. Options to 1338 consider for these use cases: 1339 - `upstreams` listing multiple packages. 1340 - Label selector against PackageRevisions. This does not seem that useful, as 1341 PackageRevisions are highly re-usable and would likely be composed in many 1342 different ways. 1343 - A PackageRevisionSet resource that simply contained a list of Upstream 1344 structures and could be used as an Upstream. This is functionally equivalent 1345 to the `upstreams` option, but that list is reusable across resources. 1346 - Listing multiple PackageRevisionSets in the upstream would be nice as well. 1347 - Any or all of these could be implemented in PackageVariant, 1348 PackageVariantSet, or both. 1349 1350 ## Footnotes 1351 [^porch17]: Implemented in Porch v0.0.17. 1352 [^porch18]: Coming in Porch v0.0.18. 1353 [^notimplemented]: Proposed here but not yet implemented as of Porch v0.0.18. 1354 [^setns]: As of this writing, the `set-namespace` function does not have a 1355 `create` option. This should be added to avoid the user needing to also use 1356 the `upsert-resource` function. Such common operation should be simple for 1357 users. 1358 [^pvsimpl]: This document describes PackageVariantSet `v1alpha2`, which will be 1359 available starting with Porch v0.0.18. In Porch v0.0.16 and 17, the `v1alpha1` 1360 implementation is available, but it is a somewhat different API, without 1361 support for CEL or any injection. It is focused only on fan out targeting, 1362 and uses a [slightly different targeting 1363 API](https://github.com/GoogleContainerTools/kpt/blob/main/porch/controllers/packagevariantsets/api/v1alpha1/packagevariantset_types.go). 1364 [^repo-pkg-expr]: This is not exactly correct. As we will see later in the 1365 `template` discussion, this the repository and package names listed actually 1366 are just defaults for the template; they can be further manipulated in the 1367 template to reference different downstream repositories and package names. 1368 The same is true for the repositories selected via the `repositorySelector` 1369 option. However, this can be ignored for now. 1370 [^multi-ns-reg]: Note that the same upstream repository can be registered in 1371 multiple namespaces without a problem. This simplifies access controls, 1372 avoiding the need for cross-namespace relationships between Repositories and 1373 other Porch resources.