Skip to content

Commit 51f25b5

Browse files
authored
Merge pull request #44 from kcp-dev/label-selector-related-resources
⚠️ Re-define how related resources are configuring/found, add support for label selectors
2 parents 31f565a + ed17a44 commit 51f25b5

22 files changed

+1868
-441
lines changed

Diff for: deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml

+221-18
Large diffs are not rendered by default.

Diff for: docs/publish-resources.md

+142-31
Original file line numberDiff line numberDiff line change
@@ -226,25 +226,42 @@ Likewise it's possible for auxiliary resources having to be created by the user,
226226
the user has to provide credentials.
227227

228228
To handle these cases, a `PublishedResource` can define multiple "related resources". Each related
229-
resource currently represents exactly one object to synchronize between user workspace and service
230-
cluster (i.e. you cannot express "sync all Secrets"). While the main published resource sync is
231-
always workspace->service cluster, related resources can originate on either side and so either can
232-
work as the source of truth.
229+
resource represents usually one, but can be multiple objects to synchronize between user workspace
230+
and service cluster. While the main published resource sync is always workspace->service cluster,
231+
related resources can originate on either side and so either can work as the source of truth.
233232

234233
At the moment, only `ConfigMaps` and `Secrets` are allowed related resource kinds.
235234

236-
For each related resource, the Sync Agent needs to be told their name/namespace. This is done by
237-
selecting a field in the main resource (for a `Certificate` this would mean `spec.secretName`). Both
238-
name and namespace need to be part of the main object (or be fixed values, like a hardcoded
239-
`kube-system` namespace).
235+
For each related resource, the Sync Agent needs to be told how to find the object on the origin side
236+
and where to create it on the destination side. There are multiple options that you can choose from.
240237

241-
The path expressions for name and namespace are evaluated against the main object on either side
242-
to determine their values. So if you had a `Certificate` in your workspace with
243-
`spec.secretName = "my-cert"` and after syncing it down, the copy on the service cluster has a
244-
rewritten/mutated `spec.secretName = "jk23h4wz47329rz2r72r92-cert"` (e.g. to prevent naming
245-
collisions), the expression `spec.secretName` would yield `"my-cert"` for the name in the workspace
246-
and `"jk...."` as the name on the service cluster. Once the object exists with that name on the
247-
originating side, the Sync Agent will begin to sync it to the other side.
238+
By default all related objects live in the same namespace as the primary object (their owner/parent).
239+
If the primary object is cluster scoped, admins must configure additional rules to specify what
240+
namespace the ConfigMap/Secret shall be read from and created in.
241+
242+
Related resources are always optional. Even if references (see below) are used and their path
243+
expression points to a non-existing field in the primary object (e.g. `spec.secretName` is configured,
244+
but that field does not exist in Certificate object), this will simply be treated as "not _yet_
245+
existing" and not create an error.
246+
247+
#### References
248+
249+
A reference is a JSONPath-like expression that are evaluated on both sides of the synchronization.
250+
You configure a single path expression (like `spec.secretName`) and the sync agent will evaluate it
251+
in the original primary object (in kcp) and again in the copied primary object (on the service
252+
cluster). Since the primary object has already been mutated, the `spec.secretName` is already
253+
rewritten/adjusted to work on the service cluster (for example it was changed from `my-secret` to
254+
`jk23h4wz47329rz2r72r92-secret` on the service cluster side). By doing it this way, admins only have
255+
to think about mutations and rewrites once (when configuring the primary object in the
256+
PublishedResource) and the path will yield 2 ready to use values (`my-secret` and the computed value).
257+
258+
The value selected by the path expression must be a string (or number, but it will be coalesced into
259+
a string) and can then be further adjusted by applying a regular expression to it.
260+
261+
References can only ever select 1 related object. Their upside is that they are simple to understand
262+
and easy to use, but require a "link" in the primary object that would point to the related object.
263+
264+
Here's an example on how to use references to locate the related object.
248265

249266
```yaml
250267
apiVersion: syncagent.kcp.io/v1alpha1
@@ -277,10 +294,11 @@ spec:
277294
# there is no GVK projection for related resources
278295
kind: Secret
279296
280-
# configure where in the parent object we can find
281-
# the name/namespace of the related resource (the child)
282-
reference:
283-
name:
297+
# configure where in the parent object we can find the child object
298+
object:
299+
# Object can use either reference, labelSelector or expressions. In this
300+
# example we use references.
301+
reference:
284302
# This path is evaluated in both the local and remote objects, to figure out
285303
# the local and remote names for the related object. This saves us from having
286304
# to remember mutated fields before their mutation (similar to the last-known
@@ -289,22 +307,115 @@ spec:
289307
290308
# namespace part is optional; if not configured,
291309
# Sync Agent assumes the same namespace as the owning resource
292-
#
293310
# namespace:
294-
# path: spec.secretName
295-
# regex:
296-
# pattern: '...'
297-
# replacement: '...'
298-
#
299-
# to inject static values, select a meaningless string value
300-
# and leave the pattern empty
301-
#
311+
# reference:
312+
# path: spec.secretName
313+
# regex:
314+
# pattern: '...'
315+
# replacement: '...'
316+
```
317+
318+
#### Label Selectors
319+
320+
In some cases, the primary object does not have a link to its child/children objects. In these cases,
321+
a label selector can be used. This allows to configure the labels that any related object must have
322+
to be included.
323+
324+
Notably, this allows for _multiple_ objects that are synced for a single configured related resource.
325+
The sync agent will not prevent misconfigurations, so great care must be taken when configuring
326+
selectors to not accidentally include too many objects.
327+
328+
Additionally, it is assumed that
329+
330+
* Primary objects synced from kcp to a service cluster will be renamed, to prevent naming collisions.
331+
* The renamed objects on the service cluster might contain private, sensitive information that should
332+
not be leaked into kcp workspaces.
333+
* When there is no explicit name being requested (like by setting `spec.secretName`), it can be
334+
assumed that the operator on the service cluster that is actually processing the primary object
335+
will use the primary object's name (at least in parts) to construct the names of related objects,
336+
for example a Certificate `yaddasupersecretyadda` might automatically get a Secret created named
337+
`yaddasupersecretyadda-secret`.
338+
339+
Since the name of the related object must not leak into a kcp workspace, admins who configure a
340+
label selector also always have to provide a naming scheme for the copies of the related objects on
341+
the destination side.
342+
343+
Namespaces work the same as with references, i.e. by default the same namespace as the primary object
344+
is assumed. However you can actually also use label selectors to find the origin _namespaces_
345+
dynamically. So you can configure two label selectors, and then agent will first use the namespace
346+
selector to find all applicable namespaces, and then use the other label selector _in each of the
347+
applicable namespaces_ to finally locate the related objects. How useful this is depends a lot on
348+
how crazy the underlying operators on the service clusters are.
349+
350+
Here is an example on how to use label selectors:
351+
352+
```yaml
353+
apiVersion: syncagent.kcp.io/v1alpha1
354+
kind: PublishedResource
355+
metadata:
356+
name: publish-certmanager-certs
357+
spec:
358+
resource:
359+
kind: Certificate
360+
apiGroup: cert-manager.io
361+
version: v1
362+
363+
naming:
364+
namespace: kube-system
365+
name: "$remoteClusterName-$remoteNamespaceHash-$remoteNameHash"
366+
367+
related:
368+
- identifier: tls-secrets
369+
370+
# "service" or "kcp"
371+
origin: service
372+
373+
# for now, only "Secret" and "ConfigMap" are supported;
374+
# there is no GVK projection for related resources
375+
kind: Secret
376+
377+
# configure where in the parent object we can find the child object
378+
object:
379+
# A selector is a standard Kubernetes label selector, supporting
380+
# matchLabels and matchExpressions.
381+
selector:
382+
matchLabels:
383+
my-key: my-value
384+
another: pair
385+
386+
# You also need to provide rules on how objects found by this selector
387+
# should be named on the destination side of the sync.
388+
# Rewrites are either using regular expressions or templated strings,
389+
# never both.
390+
# The rewrite config is applied to each individual found object.
391+
rewrite:
392+
regex:
393+
pattern: "foo-(.+)"
394+
replacement: "bar-\\1"
395+
396+
# or
397+
template:
398+
template: "{{ .Name }}-foo"
399+
400+
# Like with references, the namespace can (or must) be configured explicitly.
401+
# You do not need to also use label selectors here, you can mix and match
402+
# freely.
302403
# namespace:
303-
# path: metadata.uid
304-
# regex:
305-
# replacement: kube-system
404+
# reference:
405+
# path: metadata.namespace
406+
# regex:
407+
# pattern: '...'
408+
# replacement: '...'
306409
```
307410

411+
#### Templates
412+
413+
The third option to configure how to find/create related objects are templates. These are simple
414+
Go template strings (like `{{ .Variable }}`) that allow to easily configure static values with a
415+
sprinkling of dynamic values.
416+
417+
This feature has not been fully implemented yet.
418+
308419
## Examples
309420

310421
### Provide Certificates

Diff for: internal/controller/syncmanager/lifecycle/cluster.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ var apiRegex = regexp.MustCompile(`(/api/|/apis/)`)
105105

106106
// generatePath formats the request path to target the specified cluster.
107107
func generatePath(originalPath string, workspacePath logicalcluster.Path) string {
108-
// If the originalPath already has cluster.Path() then the path was already modifed and no change needed
108+
// If the originalPath already has cluster.Path() then the path was already modified and no change needed
109109
if strings.Contains(originalPath, workspacePath.RequestPath()) {
110110
return originalPath
111111
}

Diff for: internal/sync/state_store_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func TestStateStoreBasics(t *testing.T) {
144144
assertObjectsEqual(t, "RemoteThing", firstObject, result)
145145

146146
///////////////////////////////////////
147-
// strip subresoures
147+
// strip subresources
148148

149149
thirdObject := newUnstructured(&dummyv1alpha1.ThingWithStatusSubresource{
150150
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)