Skip to content

Commit e6be33c

Browse files
authored
Merge pull request #52 from kcp-dev/multi-group
Remove 1 APIExport = 1 API group limitation
2 parents 51f25b5 + c88b073 commit e6be33c

File tree

16 files changed

+98
-76
lines changed

16 files changed

+98
-76
lines changed

cmd/api-syncagent/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {
117117
return fmt.Errorf("failed to resolve APIExport: %w", err)
118118
}
119119

120-
log.Infow("Resolved APIExport", "apigroup", opts.APIExportRef, "workspace", lcPath, "logicalcluster", lcName)
120+
log.Infow("Resolved APIExport", "workspace", lcPath, "logicalcluster", lcName)
121121

122122
// init the "permanent" kcp cluster connection
123123
kcpCluster, err := setupKcpCluster(kcpRestConfig, opts)
@@ -131,7 +131,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {
131131
return fmt.Errorf("failed to add kcp cluster runnable: %w", err)
132132
}
133133

134-
if err := apiresourceschema.Add(mgr, kcpCluster, lcName, log, 4, opts.AgentName, opts.APIExportRef, opts.PublishedResourceSelector); err != nil {
134+
if err := apiresourceschema.Add(mgr, kcpCluster, lcName, log, 4, opts.AgentName, opts.PublishedResourceSelector); err != nil {
135135
return fmt.Errorf("failed to add apiresourceschema controller: %w", err)
136136
}
137137

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,9 @@ spec:
285285
items:
286286
type: string
287287
type: array
288+
group:
289+
description: The API group, for example "myservice.example.com".
290+
type: string
288291
kind:
289292
description: |-
290293
The resource Kind, for example "Database". Setting this field will also overwrite

internal/controller/apiexport/reconciler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
func (r *Reconciler) createAPIExportReconciler(availableResourceSchemas sets.Set[string], claimedResourceKinds sets.Set[string], agentName string, apiExportName string) reconciling.NamedAPIExportReconcilerFactory {
3535
return func() (string, reconciling.APIExportReconciler) {
3636
return apiExportName, func(existing *kcpdevv1alpha1.APIExport) (*kcpdevv1alpha1.APIExport, error) {
37-
known := sets.New[string](existing.Spec.LatestResourceSchemas...)
37+
known := sets.New(existing.Spec.LatestResourceSchemas...)
3838

3939
if existing.Annotations == nil {
4040
existing.Annotations = map[string]string{}

internal/controller/apiresourceschema/controller.go

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,13 @@ const (
5454
)
5555

5656
type Reconciler struct {
57-
localClient ctrlruntimeclient.Client
58-
kcpClient ctrlruntimeclient.Client
59-
restConfig *rest.Config
60-
log *zap.SugaredLogger
61-
recorder record.EventRecorder
62-
lcName logicalcluster.Name
63-
agentName string
64-
apiExportName string
57+
localClient ctrlruntimeclient.Client
58+
kcpClient ctrlruntimeclient.Client
59+
restConfig *rest.Config
60+
log *zap.SugaredLogger
61+
recorder record.EventRecorder
62+
lcName logicalcluster.Name
63+
agentName string
6564
}
6665

6766
// Add creates a new controller and adds it to the given manager.
@@ -72,18 +71,16 @@ func Add(
7271
log *zap.SugaredLogger,
7372
numWorkers int,
7473
agentName string,
75-
apiExportName string,
7674
prFilter labels.Selector,
7775
) error {
7876
reconciler := &Reconciler{
79-
localClient: mgr.GetClient(),
80-
kcpClient: kcpCluster.GetClient(),
81-
restConfig: mgr.GetConfig(),
82-
lcName: lcName,
83-
log: log.Named(ControllerName),
84-
recorder: mgr.GetEventRecorderFor(ControllerName),
85-
agentName: agentName,
86-
apiExportName: apiExportName,
77+
localClient: mgr.GetClient(),
78+
kcpClient: kcpCluster.GetClient(),
79+
restConfig: mgr.GetConfig(),
80+
lcName: lcName,
81+
log: log.Named(ControllerName),
82+
recorder: mgr.GetEventRecorderFor(ControllerName),
83+
agentName: agentName,
8784
}
8885

8986
_, err := builder.ControllerManagedBy(mgr).
@@ -138,14 +135,14 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
138135
}
139136

140137
// project the CRD
141-
projectedCRD, err := r.applyProjection(r.apiExportName, crd, pubResource)
138+
projectedCRD, err := r.applyProjection(crd, pubResource)
142139
if err != nil {
143140
return nil, fmt.Errorf("failed to apply projection rules: %w", err)
144141
}
145142

146143
// to prevent changing the source GVK e.g. from "apps/v1 Daemonset" to "core/v1 Pod",
147144
// we include the source GVK in hashed form in the final APIResourceSchema name.
148-
arsName := r.getAPIResourceSchemaName(r.apiExportName, projectedCRD)
145+
arsName := r.getAPIResourceSchemaName(projectedCRD)
149146

150147
// ARS'es cannot be updated, their entire spec is immutable. For now we do not care about
151148
// CRDs being updated on the service cluster, but in the future (TODO) we must allow
@@ -155,7 +152,7 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
155152
err = r.kcpClient.Get(wsCtx, types.NamespacedName{Name: arsName}, ars, &ctrlruntimeclient.GetOptions{})
156153

157154
if apierrors.IsNotFound(err) {
158-
if err := r.createAPIResourceSchema(wsCtx, log, r.apiExportName, projectedCRD, arsName); err != nil {
155+
if err := r.createAPIResourceSchema(wsCtx, log, projectedCRD, arsName); err != nil {
159156
return nil, fmt.Errorf("failed to create APIResourceSchema: %w", err)
160157
}
161158
} else if err != nil {
@@ -178,7 +175,7 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
178175
return nil, nil
179176
}
180177

181-
func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.SugaredLogger, apigroup string, projectedCRD *apiextensionsv1.CustomResourceDefinition, arsName string) error {
178+
func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.SugaredLogger, projectedCRD *apiextensionsv1.CustomResourceDefinition, arsName string) error {
182179
// prefix is irrelevant as the reconciling framework will use arsName anyway
183180
converted, err := kcpdevv1alpha1.CRDToAPIResourceSchema(projectedCRD, "irrelevant")
184181
if err != nil {
@@ -191,9 +188,6 @@ func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.Sugar
191188
syncagentv1alpha1.SourceGenerationAnnotation: fmt.Sprintf("%d", projectedCRD.Generation),
192189
syncagentv1alpha1.AgentNameAnnotation: r.agentName,
193190
}
194-
ars.Labels = map[string]string{
195-
syncagentv1alpha1.APIGroupLabel: apigroup,
196-
}
197191
ars.Spec.Group = converted.Spec.Group
198192
ars.Spec.Names = converted.Spec.Names
199193
ars.Spec.Scope = converted.Spec.Scope
@@ -204,9 +198,8 @@ func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.Sugar
204198
return r.kcpClient.Create(ctx, ars)
205199
}
206200

207-
func (r *Reconciler) applyProjection(apiGroup string, crd *apiextensionsv1.CustomResourceDefinition, pr *syncagentv1alpha1.PublishedResource) (*apiextensionsv1.CustomResourceDefinition, error) {
201+
func (r *Reconciler) applyProjection(crd *apiextensionsv1.CustomResourceDefinition, pr *syncagentv1alpha1.PublishedResource) (*apiextensionsv1.CustomResourceDefinition, error) {
208202
result := crd.DeepCopy()
209-
result.Spec.Group = apiGroup
210203

211204
// Currently CRDs generated by our discovery mechanism already set these to true, but that's just
212205
// because it doesn't care to set them correctly; we keep this code here because from here on,
@@ -219,6 +212,10 @@ func (r *Reconciler) applyProjection(apiGroup string, crd *apiextensionsv1.Custo
219212
return result, nil
220213
}
221214

215+
if projection.Group != "" {
216+
result.Spec.Group = projection.Group
217+
}
218+
222219
if projection.Version != "" {
223220
result.Spec.Versions[0].Name = projection.Version
224221
}
@@ -252,9 +249,9 @@ func (r *Reconciler) applyProjection(apiGroup string, crd *apiextensionsv1.Custo
252249

253250
// getAPIResourceSchemaName generates the name for the ARS in kcp. Note that
254251
// kcp requires, just like CRDs, that ARS are named following a specific pattern.
255-
func (r *Reconciler) getAPIResourceSchemaName(apiGroup string, crd *apiextensionsv1.CustomResourceDefinition) string {
252+
func (r *Reconciler) getAPIResourceSchemaName(crd *apiextensionsv1.CustomResourceDefinition) string {
256253
checksum := crypto.Hash(crd.Spec.Names)
257254

258255
// include a leading "v" to prevent SHA-1 hashes with digits to break the name
259-
return fmt.Sprintf("v%s.%s.%s", checksum[:8], crd.Spec.Names.Plural, apiGroup)
256+
return fmt.Sprintf("v%s.%s.%s", checksum[:8], crd.Spec.Names.Plural, crd.Spec.Group)
260257
}

internal/controller/sync/controller.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ func Create(
7171
virtualWorkspaceCluster cluster.Cluster,
7272
pubRes *syncagentv1alpha1.PublishedResource,
7373
discoveryClient *discovery.Client,
74-
apiExportName string,
7574
stateNamespace string,
7675
agentName string,
7776
log *zap.SugaredLogger,
@@ -85,7 +84,7 @@ func Create(
8584
localDummy.SetGroupVersionKind(localGVK)
8685

8786
// create a dummy unstructured object with the projected GVK inside the workspace
88-
remoteGVK := projection.PublishedResourceProjectedGVK(pubRes, apiExportName)
87+
remoteGVK := projection.PublishedResourceProjectedGVK(pubRes)
8988
remoteDummy := &unstructured.Unstructured{}
9089
remoteDummy.SetGroupVersionKind(remoteGVK)
9190

@@ -97,7 +96,7 @@ func Create(
9796

9897
// create the syncer that holds the meat&potatoes of the synchronization logic
9998
mutator := mutation.NewMutator(pubRes.Spec.Mutation)
100-
syncer, err := sync.NewResourceSyncer(log, localManager.GetClient(), virtualWorkspaceCluster.GetClient(), pubRes, localCRD, apiExportName, mutator, stateNamespace, agentName)
99+
syncer, err := sync.NewResourceSyncer(log, localManager.GetClient(), virtualWorkspaceCluster.GetClient(), pubRes, localCRD, mutator, stateNamespace, agentName)
101100
if err != nil {
102101
return nil, fmt.Errorf("failed to create syncer: %w", err)
103102
}

internal/controller/syncmanager/controller.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ func (r *Reconciler) ensureSyncControllers(ctx context.Context, log *zap.Sugared
286286
r.vwCluster.GetCluster(),
287287
&pubRes,
288288
r.discoveryClient,
289-
r.apiExport.Name,
290289
r.stateNamespace,
291290
r.agentName,
292291
r.log,

internal/projection/projection.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,16 @@ func PublishedResourceSourceGVK(pubRes *syncagentv1alpha1.PublishedResource) sch
3434

3535
// PublishedResourceProjectedGVK returns the effective GVK after the projection
3636
// rules have been applied according to the PublishedResource.
37-
func PublishedResourceProjectedGVK(pubRes *syncagentv1alpha1.PublishedResource, kcpAPIGroup string) schema.GroupVersionKind {
37+
func PublishedResourceProjectedGVK(pubRes *syncagentv1alpha1.PublishedResource) schema.GroupVersionKind {
38+
apiGroup := pubRes.Spec.Resource.APIGroup
3839
apiVersion := pubRes.Spec.Resource.Version
3940
kind := pubRes.Spec.Resource.Kind
4041

4142
if projection := pubRes.Spec.Projection; projection != nil {
43+
if v := projection.Group; v != "" {
44+
apiGroup = v
45+
}
46+
4247
if v := projection.Version; v != "" {
4348
apiVersion = v
4449
}
@@ -49,7 +54,7 @@ func PublishedResourceProjectedGVK(pubRes *syncagentv1alpha1.PublishedResource,
4954
}
5055

5156
return schema.GroupVersionKind{
52-
Group: kcpAPIGroup,
57+
Group: apiGroup,
5358
Version: apiVersion,
5459
Kind: kind,
5560
}

internal/projection/projection_test.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ func TestPublishedResourceSourceGVK(t *testing.T) {
5858

5959
func TestPublishedResourceProjectedGVK(t *testing.T) {
6060
const (
61-
apiGroup = "testgroup"
62-
overrideAPIGroup = "newgroup"
63-
version = "v1"
64-
kind = "test"
61+
apiGroup = "testgroup"
62+
version = "v1"
63+
kind = "test"
6564
)
6665

6766
pubRes := &syncagentv1alpha1.PublishedResource{
@@ -82,22 +81,27 @@ func TestPublishedResourceProjectedGVK(t *testing.T) {
8281
{
8382
name: "no projection",
8483
projection: nil,
85-
expected: schema.GroupVersionKind{Group: overrideAPIGroup, Version: version, Kind: kind},
84+
expected: schema.GroupVersionKind{Group: apiGroup, Version: version, Kind: kind},
8685
},
8786
{
8887
name: "override version",
8988
projection: &syncagentv1alpha1.ResourceProjection{Version: "v2"},
90-
expected: schema.GroupVersionKind{Group: overrideAPIGroup, Version: "v2", Kind: kind},
89+
expected: schema.GroupVersionKind{Group: apiGroup, Version: "v2", Kind: kind},
9190
},
9291
{
9392
name: "override kind",
9493
projection: &syncagentv1alpha1.ResourceProjection{Kind: "dummy"},
95-
expected: schema.GroupVersionKind{Group: overrideAPIGroup, Version: version, Kind: "dummy"},
94+
expected: schema.GroupVersionKind{Group: apiGroup, Version: version, Kind: "dummy"},
9695
},
9796
{
9897
name: "override both",
9998
projection: &syncagentv1alpha1.ResourceProjection{Version: "v2", Kind: "dummy"},
100-
expected: schema.GroupVersionKind{Group: overrideAPIGroup, Version: "v2", Kind: "dummy"},
99+
expected: schema.GroupVersionKind{Group: apiGroup, Version: "v2", Kind: "dummy"},
100+
},
101+
{
102+
name: "override group",
103+
projection: &syncagentv1alpha1.ResourceProjection{Group: "projected.com"},
104+
expected: schema.GroupVersionKind{Group: "projected.com", Version: version, Kind: kind},
101105
},
102106
}
103107

@@ -106,7 +110,7 @@ func TestPublishedResourceProjectedGVK(t *testing.T) {
106110
pr := pubRes.DeepCopy()
107111
pr.Spec.Projection = testcase.projection
108112

109-
gvk := PublishedResourceProjectedGVK(pr, overrideAPIGroup)
113+
gvk := PublishedResourceProjectedGVK(pr)
110114

111115
if gvk.Group != testcase.expected.Group {
112116
t.Errorf("Expected API group to be %q, but got %q.", testcase.expected.Group, gvk.Group)

internal/sync/syncer.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ func NewResourceSyncer(
5858
remoteClient ctrlruntimeclient.Client,
5959
pubRes *syncagentv1alpha1.PublishedResource,
6060
localCRD *apiextensionsv1.CustomResourceDefinition,
61-
remoteAPIGroup string,
6261
mutator mutation.Mutator,
6362
stateNamespace string,
6463
agentName string,
@@ -69,7 +68,7 @@ func NewResourceSyncer(
6968
localDummy.SetGroupVersionKind(localGVK)
7069

7170
// create a dummy unstructured object with the projected GVK inside the workspace
72-
remoteGVK := projection.PublishedResourceProjectedGVK(pubRes, remoteAPIGroup)
71+
remoteGVK := projection.PublishedResourceProjectedGVK(pubRes)
7372

7473
// determine whether the CRD has a status subresource in the relevant version
7574
subresources := []string{}

0 commit comments

Comments
 (0)