Skip to content

Commit eb10457

Browse files
committed
add new test for projection, fix ARS version projection
On-behalf-of: @SAP [email protected]
1 parent 0d976e3 commit eb10457

File tree

3 files changed

+138
-25
lines changed

3 files changed

+138
-25
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/evanphx/json-patch/v5 v5.9.0
88
github.com/go-logr/logr v1.4.2
99
github.com/go-logr/zapr v1.3.0
10+
github.com/google/go-cmp v0.6.0
1011
github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20240817110845-a9eb9752bfeb
1112
github.com/kcp-dev/client-go v0.0.0-20240912145314-f5949d81732a
1213
github.com/kcp-dev/code-generator/v2 v2.3.1
@@ -53,7 +54,6 @@ require (
5354
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
5455
github.com/golang/protobuf v1.5.4 // indirect
5556
github.com/google/gnostic-models v0.6.8 // indirect
56-
github.com/google/go-cmp v0.6.0 // indirect
5757
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect
5858
github.com/google/uuid v1.6.0 // indirect
5959
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect

internal/controller/apiresourceschema/controller.go

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
133133
}
134134

135135
// project the CRD
136-
projectedCRD := r.projectResourceNames(r.apiExportName, crd, pubResource.Spec.Projection)
136+
projectedCRD, err := r.applyProjection(r.apiExportName, crd, pubResource)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to apply projection rules: %w", err)
139+
}
137140

138141
// to prevent changing the source GVK e.g. from "apps/v1 Daemonset" to "core/v1 Pod",
139142
// we include the source GVK in hashed form in the final APIResourceSchema name.
@@ -147,7 +150,7 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
147150
err = r.kcpClient.Get(wsCtx, types.NamespacedName{Name: arsName}, ars, &ctrlruntimeclient.GetOptions{})
148151

149152
if apierrors.IsNotFound(err) {
150-
if err := r.createAPIResourceSchema(wsCtx, log, r.apiExportName, projectedCRD, arsName, pubResource.Spec.Resource.Version); err != nil {
153+
if err := r.createAPIResourceSchema(wsCtx, log, r.apiExportName, projectedCRD, arsName); err != nil {
151154
return nil, fmt.Errorf("failed to create APIResourceSchema: %w", err)
152155
}
153156
} else if err != nil {
@@ -170,25 +173,7 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR
170173
return nil, nil
171174
}
172175

173-
func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.SugaredLogger, apigroup string, projectedCRD *apiextensionsv1.CustomResourceDefinition, arsName string, selectedVersion string) error {
174-
// At this moment we ignore every non-selected version in the CRD, as we have not fully
175-
// decided on how to support the API version lifecycle yet. Having multiple versions in
176-
// the CRD will make kcp require a `conversion` to also be configured. Since we cannot
177-
// enforce that and want to instead work with existing CRDs as best as we can, we chose
178-
// this option (instead of error'ing out if a conversion is missing).
179-
projectedCRD.Spec.Conversion = nil
180-
projectedCRD.Spec.Versions = slices.DeleteFunc(projectedCRD.Spec.Versions, func(v apiextensionsv1.CustomResourceDefinitionVersion) bool {
181-
return v.Name != selectedVersion
182-
})
183-
184-
if len(projectedCRD.Spec.Versions) != 1 {
185-
// This should never happen because of checks earlier in the reconciler.
186-
return fmt.Errorf("invalid CRD: cannot find selected version %q", selectedVersion)
187-
}
188-
189-
projectedCRD.Spec.Versions[0].Served = true
190-
projectedCRD.Spec.Versions[0].Storage = true
191-
176+
func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.SugaredLogger, apigroup string, projectedCRD *apiextensionsv1.CustomResourceDefinition, arsName string) error {
192177
// prefix is irrelevant as the reconciling framework will use arsName anyway
193178
converted, err := kcpdevv1alpha1.CRDToAPIResourceSchema(projectedCRD, "irrelevant")
194179
if err != nil {
@@ -214,12 +199,35 @@ func (r *Reconciler) createAPIResourceSchema(ctx context.Context, log *zap.Sugar
214199
return r.kcpClient.Create(ctx, ars)
215200
}
216201

217-
func (r *Reconciler) projectResourceNames(apiGroup string, crd *apiextensionsv1.CustomResourceDefinition, projection *syncagentv1alpha1.ResourceProjection) *apiextensionsv1.CustomResourceDefinition {
202+
func (r *Reconciler) applyProjection(apiGroup string, crd *apiextensionsv1.CustomResourceDefinition, pr *syncagentv1alpha1.PublishedResource) (*apiextensionsv1.CustomResourceDefinition, error) {
218203
result := crd.DeepCopy()
219204
result.Spec.Group = apiGroup
220205

206+
// At this moment we ignore every non-selected version in the CRD, as we have not fully
207+
// decided on how to support the API version lifecycle yet. Having multiple versions in
208+
// the CRD will make kcp require a `conversion` to also be configured. Since we cannot
209+
// enforce that and want to instead work with existing CRDs as best as we can, we chose
210+
// this option (instead of error'ing out if a conversion is missing).
211+
result.Spec.Conversion = nil
212+
result.Spec.Versions = slices.DeleteFunc(result.Spec.Versions, func(v apiextensionsv1.CustomResourceDefinitionVersion) bool {
213+
return v.Name != pr.Spec.Resource.Version
214+
})
215+
216+
if len(result.Spec.Versions) != 1 {
217+
// This should never happen because of checks earlier in the reconciler.
218+
return nil, fmt.Errorf("invalid CRD: cannot find selected version %q", pr.Spec.Resource.Version)
219+
}
220+
221+
result.Spec.Versions[0].Served = true
222+
result.Spec.Versions[0].Storage = true
223+
224+
projection := pr.Spec.Projection
221225
if projection == nil {
222-
return result
226+
return result, nil
227+
}
228+
229+
if projection.Version != "" {
230+
result.Spec.Versions[0].Name = projection.Version
223231
}
224232

225233
if projection.Kind != "" {
@@ -246,7 +254,7 @@ func (r *Reconciler) projectResourceNames(apiGroup string, crd *apiextensionsv1.
246254
result.Spec.Names.ShortNames = projection.ShortNames
247255
}
248256

249-
return result
257+
return result, nil
250258
}
251259

252260
// getAPIResourceSchemaName generates the name for the ARS in kcp. Note that

test/e2e/apiresourceschema/apiresourceschema_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ import (
2424
"time"
2525

2626
"github.com/go-logr/logr"
27+
"github.com/google/go-cmp/cmp"
2728

2829
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
2930
"github.com/kcp-dev/api-syncagent/test/utils"
3031

3132
kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
3233

34+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3335
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3436
"k8s.io/apimachinery/pkg/types"
3537
"k8s.io/apimachinery/pkg/util/wait"
@@ -272,3 +274,106 @@ func TestARSDropsAllVersionsExceptTheSelectedOne(t *testing.T) {
272274
t.Fatalf("Expected ARS to contain %q, but contains %q.", theVersion, name)
273275
}
274276
}
277+
278+
func TestProjection(t *testing.T) {
279+
const (
280+
apiExportName = "example.com"
281+
originalVersion = "v1"
282+
)
283+
284+
ctx := context.Background()
285+
ctrlruntime.SetLogger(logr.Discard())
286+
287+
// setup a test environment in kcp
288+
orgKubconfig := utils.CreateOrganization(t, ctx, "ars-projections", apiExportName)
289+
290+
// start a service cluster
291+
envtestKubeconfig, envtestClient, _ := utils.RunEnvtest(t, []string{
292+
"test/crds/crontab.yaml",
293+
})
294+
295+
// publish Crontabs
296+
t.Logf("Publishing CronTabs…")
297+
pr := &syncagentv1alpha1.PublishedResource{
298+
ObjectMeta: metav1.ObjectMeta{
299+
Name: "publish-crontabs",
300+
},
301+
Spec: syncagentv1alpha1.PublishedResourceSpec{
302+
Resource: syncagentv1alpha1.SourceResourceDescriptor{
303+
APIGroup: "example.com",
304+
Version: originalVersion,
305+
Kind: "CronTab",
306+
},
307+
Projection: &syncagentv1alpha1.ResourceProjection{
308+
Version: "v6",
309+
Scope: syncagentv1alpha1.ClusterScoped,
310+
Kind: "CronusTabulatus",
311+
Plural: "cronustabulati",
312+
ShortNames: []string{"cront"},
313+
},
314+
},
315+
}
316+
317+
if err := envtestClient.Create(ctx, pr); err != nil {
318+
t.Fatalf("Failed to create PublishedResource: %v", err)
319+
}
320+
321+
// let the agent do its thing
322+
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName)
323+
324+
// wait for the APIExport to be updated
325+
t.Logf("Waiting for APIExport to be updated…")
326+
orgClient := utils.GetClient(t, orgKubconfig)
327+
apiExportKey := types.NamespacedName{Name: apiExportName}
328+
329+
var arsName string
330+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 1*time.Minute, false, func(ctx context.Context) (done bool, err error) {
331+
apiExport := &kcpapisv1alpha1.APIExport{}
332+
err = orgClient.Get(ctx, apiExportKey, apiExport)
333+
if err != nil {
334+
return false, err
335+
}
336+
337+
if len(apiExport.Spec.LatestResourceSchemas) == 0 {
338+
return false, nil
339+
}
340+
341+
arsName = apiExport.Spec.LatestResourceSchemas[0]
342+
343+
return true, nil
344+
})
345+
if err != nil {
346+
t.Fatalf("Failed to wait for APIExport to be updated: %v", err)
347+
}
348+
349+
// check the APIResourceSchema
350+
ars := &kcpapisv1alpha1.APIResourceSchema{}
351+
err = orgClient.Get(ctx, types.NamespacedName{Name: arsName}, ars)
352+
if err != nil {
353+
t.Fatalf("APIResourceSchema does not exist: %v", err)
354+
}
355+
356+
if len(ars.Spec.Versions) != 1 {
357+
t.Fatalf("Expected only one version to remain in ARS, but found %d.", len(ars.Spec.Versions))
358+
}
359+
360+
if name := ars.Spec.Versions[0].Name; name != pr.Spec.Projection.Version {
361+
t.Errorf("Expected ARS to contain version %q, but contains %q.", pr.Spec.Projection.Version, name)
362+
}
363+
364+
if ars.Spec.Scope != apiextensionsv1.ResourceScope(pr.Spec.Projection.Scope) {
365+
t.Errorf("Expected ARS to be of scope %q, but is %q.", pr.Spec.Projection.Scope, ars.Spec.Scope)
366+
}
367+
368+
if ars.Spec.Names.Kind != pr.Spec.Projection.Kind {
369+
t.Errorf("Expected ARS to be kind %q, but is %q.", pr.Spec.Projection.Kind, ars.Spec.Names.Kind)
370+
}
371+
372+
if ars.Spec.Names.Plural != pr.Spec.Projection.Plural {
373+
t.Errorf("Expected ARS to have plural name %q, but has %q.", pr.Spec.Projection.Plural, ars.Spec.Names.Plural)
374+
}
375+
376+
if !cmp.Equal(ars.Spec.Names.ShortNames, pr.Spec.Projection.ShortNames) {
377+
t.Errorf("Expected ARS to have short names %v, but has %v.", pr.Spec.Projection.ShortNames, ars.Spec.Names.ShortNames)
378+
}
379+
}

0 commit comments

Comments
 (0)