Skip to content

Commit cbce4fc

Browse files
authored
Merge pull request #62 from kcp-dev/api-lifecycle
Handle API lifecycle
2 parents 6a26fa4 + 58721d6 commit cbce4fc

File tree

26 files changed

+1747
-321
lines changed

26 files changed

+1747
-321
lines changed

cmd/api-syncagent/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/kcp-dev/logicalcluster/v3"
2828
"github.com/spf13/pflag"
2929
"go.uber.org/zap"
30+
reconcilerlog "k8c.io/reconciler/pkg/log"
3031

3132
"github.com/kcp-dev/api-syncagent/internal/controller/apiexport"
3233
"github.com/kcp-dev/api-syncagent/internal/controller/apiresourceschema"
@@ -80,6 +81,7 @@ func main() {
8081

8182
// set the logger used by sigs.k8s.io/controller-runtime
8283
ctrlruntimelog.SetLogger(zapr.NewLogger(log.WithOptions(zap.AddCallerSkip(1))))
84+
reconcilerlog.SetLogger(sugar)
8385

8486
if err := run(ctx, sugar, opts); err != nil {
8587
sugar.Fatalw("Sync Agent has encountered an error", zap.Error(err))

cmd/crd-puller/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/crd-puller
2+
*.yaml

cmd/crd-puller/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# CRD Puller
22

33
The `crd-puller` can be used for testing and development in order to export a
4-
CustomResourceDefinition for any Group/Version/Kind (GVK) in a Kubernetes cluster.
4+
CustomResourceDefinition for any Group/Kind (GK) in a Kubernetes cluster.
55

66
The main difference between this and kcp's own `crd-puller` is that this one
7-
works based on GVKs and not resources (i.e. on `apps/v1 Deployment` instead of
7+
works based on GKs and not resources (i.e. on `apps/Deployment` instead of
88
`apps.deployments`). This is more useful since a PublishedResource publishes a
9-
specific Kind and version.
9+
specific Kind and version. Also, this puller pulls all available versions, not
10+
just the preferred version.
1011

1112
## Usage
1213

1314
```shell
1415
export KUBECONFIG=/path/to/kubeconfig
1516

16-
./crd-puller Deployment.v1.apps.k8s.io
17+
./crd-puller Deployment.apps.k8s.io
1718
```

cmd/crd-puller/main.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,10 @@ func main() {
4141
pflag.Parse()
4242

4343
if pflag.NArg() == 0 {
44-
log.Fatal("No argument given. Please specify a GVK in the form 'Kind.version.apigroup.com' to pull.")
44+
log.Fatal("No argument given. Please specify a GroupKind in the form 'Kind.apigroup.com' (case-sensitive) to pull.")
4545
}
4646

47-
gvk, _ := schema.ParseKindArg(pflag.Arg(0))
48-
if gvk == nil {
49-
log.Fatal("Invalid GVK, please use the format 'Kind.version.apigroup.com'.")
50-
}
47+
gk := schema.ParseGroupKind(pflag.Arg(0))
5148

5249
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
5350
loadingRules.ExplicitPath = kubeconfigPath
@@ -67,7 +64,7 @@ func main() {
6764
log.Fatalf("Failed to create discovery client: %v.", err)
6865
}
6966

70-
crd, err := discoveryClient.RetrieveCRD(ctx, *gvk)
67+
crd, err := discoveryClient.RetrieveCRD(ctx, gk)
7168
if err != nil {
7269
log.Fatalf("Failed to pull CRD: %v.", err)
7370
}

deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ spec:
286286
type: string
287287
type: array
288288
group:
289-
description: The API group, for example "myservice.example.com".
289+
description: The API group, for example "myservice.example.com". Leave empty to not modify the API group.
290290
type: string
291291
kind:
292292
description: |-
@@ -316,8 +316,20 @@ spec:
316316
type: string
317317
type: array
318318
version:
319-
description: The API version, for example "v1beta1".
319+
description: |-
320+
The API version, for example "v1beta1". Leave empty to not modify the version.
321+
322+
This field must not be set when multiple versions have been selected.
323+
324+
Deprecated: Use .versions instead.
320325
type: string
326+
versions:
327+
additionalProperties:
328+
type: string
329+
description: |-
330+
Versions allows to map API versions onto new values in kcp. Leave empty to not modify the
331+
versions.
332+
type: object
321333
type: object
322334
related:
323335
items:
@@ -674,12 +686,23 @@ spec:
674686
description: The resource Kind, for example "Database".
675687
type: string
676688
version:
677-
description: The API version, for example "v1beta1".
689+
description: |-
690+
The API version, for example "v1beta1". Setting this field will only publish
691+
the given version, otherwise all versions for the group/kind will be
692+
published.
693+
694+
Deprecated: Use .versions instead.
678695
type: string
696+
versions:
697+
description: |-
698+
Versions allows to select a subset of versions to publish. Leave empty
699+
to publish all available versions.
700+
items:
701+
type: string
702+
type: array
679703
required:
680704
- apiGroup
681705
- kind
682-
- version
683706
type: object
684707
required:
685708
- resource

docs/content/api-lifecycle.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# API Lifecycle
2+
3+
In only the rarest of cases will the first version of a CRD be also its final version. Instead usually
4+
CRDs evolve over time and Kubernetes has strong, though sometimes hard to use, support for managing
5+
different versions of CRDs and their resources.
6+
7+
To understand how CRDs work in the context of the Sync Agent, it's important to first get familiar
8+
with the [regular Kubernetes behaviour](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/)
9+
regarding CRD versioning.
10+
11+
## Basics
12+
13+
The Sync Agent will, whenever a published CRD changes (this can also happen when the projection rules
14+
inside a `PublishedResource` are updated), create a new `APIResourceSchema` (ARS) in kcp. The name and
15+
version of this ARS are based on a hash of the projected CRD. Undoing a change would make the agent
16+
re-use the previously created ARS (ARS are immutable).
17+
18+
After every reconciliation, the list of latest resource schemas in the configured `APIExport` is
19+
updated. For this the agent will find all ARS that belong to it (based on an ownership label) and
20+
then merge them into the `APIExport`. Resource schemas for unknown group/resource combinations are
21+
left untouched, so admins are free to add additional resource schemas to an `APIExport`.
22+
23+
This means that every change to a CRD on the service cluster is applied practically immediately in
24+
each workspace that consumes the `APIExport`. Administrators are wise to act carefully when working
25+
with their CRDs on their service cluster. Sometimes it can make sense to turn-off the agent before
26+
testing new CRDs, even though this will temporarily suspend the synchronization.
27+
28+
## Single-Version CRDs
29+
30+
A very common scenario is to only ever have a single version inside each CRD and keeping this version
31+
perpetually backwards-compatible. As long as all consumers are aware that certain fields might not
32+
be set yet in older objects, this scheme works out generally fine.
33+
34+
The agent will handle this scenario just fine by itself. Whenever a CRD is updated, it will reflect
35+
those changes back into a new `APIResourceSchema` and update the `APIExport`, making the changes
36+
immediately available to all consumers. Since the agent itself doesn't much care for the contents of
37+
objects, it itself is not affected by any structural changes in CRDs, as long as it is able to apply
38+
them on the underlying Kubernetes cluster.
39+
40+
## Multi-Version CRDs
41+
42+
Having multiple versions in a single CRD is immediately much more work, since in Kubernetes all
43+
versions of a CRD must be _losslessly_ convertible to every other version. Without CEL expressions
44+
or a dedicated conversion webhook this is practically impossible to achieve.
45+
46+
At the moment kcp does not support CEL-based conversions, and there is no support for configuring a
47+
conversion webhook inside the Sync Agent either. This is because such a webhook would need to run
48+
very close to the kcp shards and it's simply out of scope for such a component to be described and
49+
deployed by the Sync Agent, let alone a trust nightmare for the kcp operators who would have to run
50+
foreign webhooks on their cluster.
51+
52+
Since both conversion mechanisms are not usable in the current state of kcp and the Sync Agent,
53+
having multiple versions in a CRD can be difficult to manage.
54+
55+
Generally the Sync Agent itself does not care much about the schemas of each CRD version or the
56+
convertibility between them. The synchronization works by using unstructured clients to the storage
57+
versison of the CRD on both sides (in kcp and on the service cluster). Which version is the storage
58+
version is up to the CRD author.
59+
60+
When publishing multiple versions of a CRD
61+
62+
* only those versions marked as `served` can be picked and
63+
* if no `storage` version is picked, the latest (highest) version will be chosen automatically as
64+
the storage version in kcp.

docs/content/faq.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ Only if you have distinct API groups (and therefore also distinct `PublishedReso
1717
You cannot currently publish the same API group onto multiple kcp setups. See issue #13 for more
1818
information.
1919

20-
## What happens when CRDs are updated?
21-
22-
At the moment, nothing. `APIResourceSchemas` in kcp are immutable and the Sync Agent currently does
23-
not attempt to update existing schemas in an `APIExport`. If you add a _new_ CRD that you want to
24-
publish, that's fine, it will be added to the `APIExport`. But changes to existing CRDs require
25-
manual work.
26-
27-
To trigger an update:
28-
29-
* remove the `APIResourceSchema` from the `latestResourceSchemas`,
30-
* delete the `APIResourceSchema` object in kcp,
31-
* restart the api-syncagent
20+
## Can I have additional resources in APIExports, unmanaged by the Sync Agent?
21+
22+
Yes, you can. The agent will only ever change those resourceSchemas that match group/resource of
23+
the configured `PublishedResources`. So if you configure the agent to publish
24+
`cert-manager.io/Certificate`, this would "claim" all resource schemas ending in
25+
`.certificates.cert-manager.io`. When updating the `APIExport`, the agent will only touch schemas
26+
with this suffix and leave all others alone.
27+
28+
This is also used when a `PublishedResource` is deleted: Since the `APIResourceSchema` remains in kcp,
29+
but is no longer configured in the agent, the agent will simply ignore the schema in the `APIExport`.
30+
This allows for async cleanup processes to happen before an admin ultimately removes the old
31+
schema from the `APIExport`.
3232

3333
## Does the Sync Agent handle permission claims?
3434

docs/content/publish-resources.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ For each of the CRDs on the service cluster that should be published, the servic
1818
`PublishedResource` object, which will contain both which CRD to publish, as well as numerous other
1919
important settings that influence the behaviour around handling the CRD.
2020

21-
When publishing a resource (CRD), exactly one version is published. All others are ignored from the
22-
standpoint of the resource synchronization logic.
21+
When publishing a resource (CRD), service owners can choose to restrict it to a subset of available
22+
versions and even change API group, versions and names in transit (for example published a v1 from
23+
the service cluster as v1beta1 within kcp). This process of changing the identity of a CRD is called
24+
"projection" in the agent.
2325

2426
All published resources together form the APIExport. When a service is enabled in a workspace
2527
(i.e. it is bound to it), users can manage objects for the projected resources described by the
@@ -46,11 +48,18 @@ spec:
4648
resource:
4749
kind: Certificate
4850
apiGroup: cert-manager.io
49-
version: v1
51+
versions: [v1]
5052
```
5153
5254
However, you will most likely apply more configuration and use features described below.
5355
56+
You always have to select at least one version, and all selected versions must be marked as `served`
57+
on the service cluster. If the storage version is selected to be published, it stays the storage
58+
version in kcp. If no storage version is selected, the latest selected version becomes the storage
59+
version.
60+
61+
For more information refer to the [API lifecycle](api-lifecycle.md).
62+
5463
### Filtering
5564

5665
The Sync Agent can be instructed to only work on a subset of resources in kcp. This can be restricted
@@ -70,16 +79,18 @@ spec:
7079
foo: bar
7180
```
7281

82+
The configuration above would mean the agent only synchronizes objects from `my-app` namespaces (in
83+
each of the kcp workspaces) that also have a `foo=bar` label on them.
84+
7385
### Schema
7486

75-
**Warning:** The actual CRD schema is always copied verbatim. All projections <!--, mutations -->
76-
etc. have to take into account that the resource contents must be expressible without changes to the
77-
schema, so you cannot define entirely new fields in an object that are not defined by the original
78-
CRD.
87+
**Warning:** The actual CRD schema is always copied verbatim. All projections, mutations etc. have
88+
to take into account that the resource contents must be expressible without changes to the schema,
89+
so you cannot define entirely new fields in an object that are not defined by the original CRD.
7990

8091
### Projection
8192

82-
For stronger separation of concerns and to enable whitelabelling of services, the type meta for
93+
For stronger separation of concerns and to enable whitelabelling of services, the type meta for CRDs
8394
can be projected, i.e. changed between the local service cluster and kcp. You could for example
8495
rename `Certificate` from cert-manager to `Sertifikat` inside kcp.
8596

@@ -103,18 +114,22 @@ metadata:
103114
spec:
104115
resource: ...
105116
projection:
106-
version: v1beta1
117+
# all of these options are optional
107118
kind: Sertifikat
108119
plural: Sertifikater
109120
shortNames: [serts]
121+
versions:
122+
# old version => new version;
123+
# this must not map multiple versions to the same new version.
124+
v1: v1beta1
110125
# categories: [management]
111126
# scope: Namespaced # change only when you know what you're doing
112127
```
113128

114129
Consumers (end users) in kcp would then ultimately see projected names only. Note that GVK
115130
projection applies only to the synced object itself and has no effect on the contents of these
116131
objects. To change the contents, use external solutions like Crossplane to transform objects.
117-
<!-- To change the contents, use *Mutations*. -->
132+
To change the contents, use *Mutations*.
118133

119134
### (Re-)Naming
120135

@@ -274,7 +289,7 @@ spec:
274289
resource:
275290
kind: Certificate
276291
apiGroup: cert-manager.io
277-
version: v1
292+
versions: [v1]
278293
279294
naming:
280295
# this is where our CA and Issuer live in this example
@@ -360,7 +375,7 @@ spec:
360375
resource:
361376
kind: Certificate
362377
apiGroup: cert-manager.io
363-
version: v1
378+
versions: [v1]
364379
365380
naming:
366381
namespace: kube-system
@@ -445,7 +460,7 @@ spec:
445460
resource:
446461
kind: Certificate
447462
apiGroup: cert-manager.io
448-
version: v1
463+
versions: [v1]
449464
450465
naming:
451466
# this is where our CA and Issuer live in this example

docs/mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
1615
site_name: api-syncagent
1716
repo_url: https://github.com/kcp-dev/api-syncagent
1817
repo_name: kcp-dev/api-syncagent
@@ -24,6 +23,7 @@ nav:
2423
- Getting Started: getting-started.md
2524
- Publishing Resources: publish-resources.md
2625
- Consuming Services: consuming-services.md
26+
- API Lifecycle: api-lifecycle.md
2727
- FAQ: faq.md
2828
- Release Process: releasing.md
2929

internal/controller/apiexport/controller.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package apiexport
1919
import (
2020
"context"
2121
"fmt"
22+
"slices"
2223

2324
"github.com/kcp-dev/logicalcluster/v3"
2425
"go.uber.org/zap"
@@ -121,12 +122,9 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
121122
}
122123

123124
// filter out those PRs that have not yet been processed into an ARS
124-
filteredPubResources := []syncagentv1alpha1.PublishedResource{}
125-
for i, pubResource := range pubResources.Items {
126-
if pubResource.Status.ResourceSchemaName != "" {
127-
filteredPubResources = append(filteredPubResources, pubResources.Items[i])
128-
}
129-
}
125+
filteredPubResources := slices.DeleteFunc(pubResources.Items, func(pr syncagentv1alpha1.PublishedResource) bool {
126+
return pr.Status.ResourceSchemaName == ""
127+
})
130128

131129
// for each PR, we note down the created ARS and also the GVKs of related resources
132130
arsList := sets.New[string]()

0 commit comments

Comments
 (0)