Skip to content

Commit

Permalink
Implement configuration injection in PackageVariant (#3939)
Browse files Browse the repository at this point in the history
* Add injection API

* Add validation for config injectors

* Add updated CRD

* Partial implementation of config injection

* Test and fix setInjectionPointConditionsAndGates

* Fix PVS test due to changed hash

* Initial complete injection implementation

Note that ensurePackageVariant needed some fixes. When I introduced
applyMutations, I didn't properly update it to account for subsequent
reconciliations; if an error happened in the first reconciation, it was
never retried and rectified in later reconciliations. So, this change
also includes that fix.

Still more tests to do.

* Additional tests, cleanup, bug fixes

* Address review comments

* Add TODO about watches for injected objects, fix nit
  • Loading branch information
johnbelamaric authored May 10, 2023
1 parent 4b107b7 commit 190dc23
Show file tree
Hide file tree
Showing 10 changed files with 1,588 additions and 48 deletions.
39 changes: 20 additions & 19 deletions docs/design-docs/08-package-variant.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ application pipeline allows it, as seen in *Figure 3*.[^setns]
| :---: |
| *Figure 3: Kptfile Function Pipeline Editing * |

### Configuration Injection[^notimplemented]
### Configuration Injection
Adding values to the package context or functions to the pipeline works
for configuration that is under the control of the creator of the PackageVariant
resource. However, in more advanced use cases, we may need to specialize the
Expand All @@ -202,14 +202,15 @@ instance of the package to lookup a resource in the Porch cluster, and copy that
information into the package. Of course, the package has to be ready to receive
this information. So, there is a protocol for facilitating this dance:
- Packages may contain resources annotated with `kpt.dev/config-injection`
- Usually, these will also be `config.kubernetes.io/local-config` resources, as
- Often, these will also be `config.kubernetes.io/local-config` resources, as
they are likely just used by local functions as input. But this is not
mandatory.
- The package variant controller will look for any resource in the Kubernetes
cluster matching the Group, Version, and Kind of the package resource, and
satisfying the *injection selector*.
- The package variant controller will copy the `spec` field from the matching
in-cluster resource to the in-package resource.
in-cluster resource to the in-package resource, or the `data` field in the
case of a ConfigMap.

| ![Figure 4: Configuration Injection](packagevariant-config-injection.png) |
| :---: |
Expand Down Expand Up @@ -610,18 +611,14 @@ With that understanding, the injection process works as follows:
`readinessGates` gating publishing the package to a *deployment* repository,
but not gating publishing to a blueprint repository.
1. The injection processing will proceed as follows. For each injection point:
- If the resource schema of the injection point is not available in the
cluster, then the injection point ConditionType will be set to `False`,
with a message indicating that the schema is missing, and processing should
proceed to the next injection point. Note that for `optional` injection
points, not having the schema may be intentional and not an error.
- If the resource schema of the injection point does not contain a `spec`
field, then the injection point ConditionType will be set to `False`, with
a message explaining the error, and processing should proceed to the next
injection point.
- The controller will now identify all in-cluster objects in the same
- The controller will identify all in-cluster objects in the same
namespace as the PackageVariant resource, with GVK matching the injection
point (the in-package resource).
point (the in-package resource). If the controller is unable to load this
objects (e.g., there are none and the CRD is not installed), the injection
point ConditionType will be set to `False`, with a message indicating that
the error, and processing proceeds to the next injection point. Note that
for `optional` injection this may be an acceptable outcome, so it does not
interfere with overall generation of the Draft.
- The controller will look through the list of injection selectors in
order and checking if any of the in-cluster objects match the selector. If
so, that in-cluster object is selected, and processing of the list of
Expand All @@ -636,15 +633,19 @@ With that understanding, the injection process works as follows:
found, and processing proceeds to the next injection point.
- If a matching in-cluster object is selected, then it is injected as
follows:
- The `spec` field from the in-cluster resource is copied to the `spec`
field of the in-package resource (the injection point), overwriting it.
- For ConfigMap resources, the `data` field from the in-cluster resource is
copied to the `data` field of the in-package resource (the injection
point), overwriting it.
- For other resource types, the `spec` field from the in-cluster resource
is copied to the `spec` field of the in-package resource (the injection
point), overwriting it.
- An annotation with name `kpt.dev/injected-resource-name` and value set to
the name of the in-cluster resource is added (or overwritten) in the
in-package resource.

If the injection cannot be completed for some reason, or if any of the below
problems exist in the upstream package, it is considered an error and should
prevent generation of the Draft:
If the the overall injection cannot be completed for some reason, or if any of
the below problems exist in the upstream package, it is considered an error and
should prevent generation of the Draft:
- There is a resource annotated as an injection point but having an invalid
annotation value (i.e., other than `required` or `optional`).
- There are ambiguous condition types due to conflicting GVK and name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ spec:
repo:
type: string
type: object
injectors:
items:
description: InjectionSelector specifies how to select in-cluster
objects for resolving injection points.
properties:
group:
type: string
kind:
type: string
name:
type: string
version:
type: string
required:
- name
type: object
type: array
labels:
additionalProperties:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ type PackageVariantSpec struct {
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`

PackageContext *PackageContext `json:"packageContext,omitempty"`
PackageContext *PackageContext `json:"packageContext,omitempty"`
Injectors []InjectionSelector `json:"injectors,omitempty"`
}

type Upstream struct {
Expand All @@ -84,6 +85,15 @@ type PackageContext struct {
RemoveKeys []string `json:"removeKeys,omitempty"`
}

// InjectionSelector specifies how to select in-cluster objects for
// resolving injection points.
type InjectionSelector struct {
Group *string `json:"group,omitempty"`
Version *string `json:"version,omitempty"`
Kind *string `json:"kind,omitempty"`
Name string `json:"name"`
}

// PackageVariantStatus defines the observed state of PackageVariant
type PackageVariantStatus struct {
// Conditions describes the reconciliation state of the object.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 The kpt Authors
// Copyright 2023 The kpt Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,9 @@ import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)

type fakeClient struct {
Expand All @@ -42,3 +44,70 @@ func (f *fakeClient) Update(_ context.Context, obj client.Object, _ ...client.Up
f.output = append(f.output, fmt.Sprintf("updating object: %s", obj.GetName()))
return nil
}

func (f *fakeClient) List(_ context.Context, obj client.ObjectList, _ ...client.ListOption) error {
cmList := `apiVersion: v1
kind: ConfigMapList
metadata:
name: my-cm-list
items:
- apiVersion: v1
kind: ConfigMap
metadata:
name: us-east1-endpoints
data:
db: db.us-east1.example.com
- apiVersion: v1
kind: ConfigMap
metadata:
name: us-east2-endpoints
data:
db: db.us-east2.example.com
- apiVersion: v1
kind: ConfigMap
metadata:
name: us-east3-endpoints
data:
db: db.us-east3.example.com`

teamList := `apiVersion: v1
kind: TeamList
metadata:
name: my-team-list
items:
- apiVersion: hr.example.com/v1alpha1
kind: Team
metadata:
name: dev-team-alpha
spec:
chargeCode: ab
- apiVersion: hr.example.com/v1alpha1
kind: Team
metadata:
name: dev-team-beta
spec:
chargeCode: cd
- apiVersion: hr.example.com/v1alpha1
kind: Team
metadata:
name: prod-team
spec:
chargeCode: ef`

var err error
switch v := obj.(type) {
case *unstructured.UnstructuredList:
gvk := v.GroupVersionKind()
switch gvk.Kind {
case "Team":
err = yaml.Unmarshal([]byte(teamList), v)
case "ConfigMap":
err = yaml.Unmarshal([]byte(cmList), v)
default:
return fmt.Errorf("unsupported kind")
}
default:
return fmt.Errorf("unsupported type")
}
return err
}
Loading

0 comments on commit 190dc23

Please sign in to comment.