Skip to content

Commit 447bc5b

Browse files
authored
Merge pull request kubernetes-sigs#6623 from sbueringer/pr-topology-mutation-external-only
✨ Topology Mutation Hook: Implement external patching
2 parents 72b68bf + 998bc05 commit 447bc5b

File tree

16 files changed

+510
-55
lines changed

16 files changed

+510
-55
lines changed

api/v1beta1/clusterclass_types.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,12 @@ type ClusterClassPatch struct {
338338

339339
// Definitions define the patches inline.
340340
// Note: Patches will be applied in the order of the array.
341-
Definitions []PatchDefinition `json:"definitions"`
341+
// +optional
342+
Definitions []PatchDefinition `json:"definitions,omitempty"`
343+
344+
// External defines an external patch.
345+
// +optional
346+
External *ExternalPatchDefinition `json:"external,omitempty"`
342347
}
343348

344349
// PatchDefinition defines a patch which is applied to customize the referenced templates.
@@ -441,6 +446,17 @@ type JSONPatchValue struct {
441446
Template *string `json:"template,omitempty"`
442447
}
443448

449+
// ExternalPatchDefinition defines an external patch.
450+
type ExternalPatchDefinition struct {
451+
// GenerateExtension references an extension which is called to generate patches.
452+
// +optional
453+
GenerateExtension *string `json:"generateExtension,omitempty"`
454+
455+
// ValidateExtension references an extension which is called to validate the topology.
456+
// +optional
457+
ValidateExtension *string `json:"validateExtension,omitempty"`
458+
}
459+
444460
// LocalObjectTemplate defines a template for a topology Class.
445461
type LocalObjectTemplate struct {
446462
// Ref is a required reference to a custom resource

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta1/zz_generated.openapi.go

Lines changed: 36 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,11 +794,22 @@ spec:
794794
will be disabled. If EnabledIf is not set, the patch will
795795
be enabled per default.
796796
type: string
797+
external:
798+
description: External defines an external patch.
799+
properties:
800+
generateExtension:
801+
description: GenerateExtension references an extension which
802+
is called to generate patches.
803+
type: string
804+
validateExtension:
805+
description: ValidateExtension references an extension which
806+
is called to validate the topology.
807+
type: string
808+
type: object
797809
name:
798810
description: Name of the patch.
799811
type: string
800812
required:
801-
- definitions
802813
- name
803814
type: object
804815
type: array

internal/controllers/topology/cluster/cluster_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
111111
r.externalTracker = external.ObjectTracker{
112112
Controller: c,
113113
}
114-
r.patchEngine = patches.NewEngine()
114+
r.patchEngine = patches.NewEngine(r.RuntimeClient)
115115
r.recorder = mgr.GetEventRecorderFor("topology/cluster")
116116
if r.patchHelperFactory == nil {
117117
r.patchHelperFactory = serverSideApplyPatchHelperFactory(r.Client)
@@ -121,7 +121,7 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
121121

122122
// SetupForDryRun prepares the Reconciler for a dry run execution.
123123
func (r *Reconciler) SetupForDryRun(recorder record.EventRecorder) {
124-
r.patchEngine = patches.NewEngine()
124+
r.patchEngine = patches.NewEngine(r.RuntimeClient)
125125
r.recorder = recorder
126126
r.patchHelperFactory = dryRunPatchHelperFactory(r.Client)
127127
}

internal/controllers/topology/cluster/patches/api/interface.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ package api
2424
import (
2525
"context"
2626

27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
2729
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
2830
)
2931

@@ -32,5 +34,13 @@ type Generator interface {
3234
// Generate generates patches for templates.
3335
// GeneratePatchesRequest contains templates and the corresponding variables.
3436
// GeneratePatchesResponse contains the patches which should be applied to the templates of the GenerateRequest.
35-
Generate(context.Context, *runtimehooksv1.GeneratePatchesRequest) *runtimehooksv1.GeneratePatchesResponse
37+
Generate(context.Context, client.Object, *runtimehooksv1.GeneratePatchesRequest) (*runtimehooksv1.GeneratePatchesResponse, error)
38+
}
39+
40+
// Validator defines a component that can validate ClusterClass templates.
41+
type Validator interface {
42+
// Validate validates templates..
43+
// ValidateTopologyRequest contains templates and the corresponding variables.
44+
// ValidateTopologyResponse contains the validation response.
45+
Validate(context.Context, client.Object, *runtimehooksv1.ValidateTopologyRequest) (*runtimehooksv1.ValidateTopologyResponse, error)
3646
}

internal/controllers/topology/cluster/patches/engine.go

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ import (
2626

2727
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2828
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
29+
"sigs.k8s.io/cluster-api/feature"
2930
"sigs.k8s.io/cluster-api/internal/contract"
3031
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/api"
32+
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/external"
3133
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/inline"
3234
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
3335
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/scope"
3436
tlog "sigs.k8s.io/cluster-api/internal/log"
37+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
3538
)
3639

3740
// Engine is a patch engine which applies patches defined in a ClusterBlueprint to a ClusterState.
@@ -40,18 +43,15 @@ type Engine interface {
4043
}
4144

4245
// NewEngine creates a new patch engine.
43-
func NewEngine() Engine {
46+
func NewEngine(runtimeClient runtimeclient.Client) Engine {
4447
return &engine{
45-
createPatchGenerator: createPatchGenerator,
48+
runtimeClient: runtimeClient,
4649
}
4750
}
4851

4952
// engine implements the Engine interface.
5053
type engine struct {
51-
// createPatchGenerator is the func which returns a patch generator
52-
// based on a ClusterClassPatch.
53-
// Note: This field is also used to inject patches in unit tests.
54-
createPatchGenerator func(patch *clusterv1.ClusterClassPatch) (api.Generator, error)
54+
runtimeClient runtimeclient.Client
5555
}
5656

5757
// Apply applies patches to the desired state according to the patches from the ClusterClass, variables from the Cluster
@@ -83,7 +83,7 @@ func (e *engine) Apply(ctx context.Context, blueprint *scope.ClusterBlueprint, d
8383
log.V(5).Infof("Applying patch to templates")
8484

8585
// Create patch generator for the current patch.
86-
generator, err := e.createPatchGenerator(&clusterClassPatch)
86+
generator, err := createPatchGenerator(e.runtimeClient, &clusterClassPatch)
8787
if err != nil {
8888
return err
8989
}
@@ -92,9 +92,9 @@ func (e *engine) Apply(ctx context.Context, blueprint *scope.ClusterBlueprint, d
9292
// NOTE: All the partial patches accumulate on top of the request, so the
9393
// patch generator in the next iteration of the loop will get the modified
9494
// version of the request (including the patched version of the templates).
95-
resp := generator.Generate(ctx, req)
96-
if resp.Status == runtimehooksv1.ResponseStatusFailure {
97-
return errors.Errorf("failed to generate patches for patch %q: %v", clusterClassPatch.Name, resp.Message)
95+
resp, err := generator.Generate(ctx, desired.Cluster, req)
96+
if err != nil {
97+
return errors.Wrapf(err, "failed to generate patches for patch %q", clusterClassPatch.Name)
9898
}
9999

100100
// Apply patches to the request.
@@ -103,6 +103,30 @@ func (e *engine) Apply(ctx context.Context, blueprint *scope.ClusterBlueprint, d
103103
}
104104
}
105105

106+
// Convert request to validation request.
107+
validationRequest := convertToValidationRequest(req)
108+
109+
// Loop over patches in ClusterClass and validate topology,
110+
// respecting the order in which they are defined.
111+
for i := range blueprint.ClusterClass.Spec.Patches {
112+
clusterClassPatch := blueprint.ClusterClass.Spec.Patches[i]
113+
114+
if clusterClassPatch.External == nil || clusterClassPatch.External.ValidateExtension == nil {
115+
continue
116+
}
117+
118+
ctx, log = log.WithValues("patch", clusterClassPatch.Name).Into(ctx)
119+
120+
log.V(5).Infof("Validating topology")
121+
122+
validator := external.NewValidator(e.runtimeClient, &clusterClassPatch)
123+
124+
_, err := validator.Validate(ctx, desired.Cluster, validationRequest)
125+
if err != nil {
126+
return errors.Wrapf(err, "validation of patch %q failed", clusterClassPatch.Name)
127+
}
128+
}
129+
106130
// Use patched templates to update the desired state objects.
107131
log.V(5).Infof("Applying patched templates to desired state")
108132
if err := updateDesiredState(ctx, req, blueprint, desired); err != nil {
@@ -234,10 +258,20 @@ func lookupMDTopology(topology *clusterv1.Topology, mdTopologyName string) (*clu
234258
// createPatchGenerator creates a patch generator for the given patch.
235259
// NOTE: Currently only inline JSON patches are supported; in the future we will add
236260
// external patches as well.
237-
func createPatchGenerator(patch *clusterv1.ClusterClassPatch) (api.Generator, error) {
261+
func createPatchGenerator(runtimeClient runtimeclient.Client, patch *clusterv1.ClusterClassPatch) (api.Generator, error) {
238262
// Return a jsonPatchGenerator if there are PatchDefinitions in the patch.
239263
if len(patch.Definitions) > 0 {
240-
return inline.New(patch), nil
264+
return inline.NewGenerator(patch), nil
265+
}
266+
// Return an externalPatchGenerator if there is an external configuration in the patch.
267+
if patch.External != nil && patch.External.GenerateExtension != nil {
268+
if !feature.Gates.Enabled(feature.RuntimeSDK) {
269+
return nil, errors.Errorf("can not use external patch %q if RuntimeSDK feature flag is disabled", patch.Name)
270+
}
271+
if runtimeClient == nil {
272+
return nil, errors.Errorf("failed to create patch generator for patch %q: runtimeClient is not set up", patch.Name)
273+
}
274+
return external.NewGenerator(runtimeClient, patch), nil
241275
}
242276

243277
return nil, errors.Errorf("failed to create patch generator for patch %q", patch.Name)
@@ -296,6 +330,24 @@ func applyPatchesToRequest(ctx context.Context, req *runtimehooksv1.GeneratePatc
296330
return nil
297331
}
298332

333+
// convertToValidationRequest converts a GeneratePatchesRequest to a ValidateTopologyRequest.
334+
func convertToValidationRequest(generateRequest *runtimehooksv1.GeneratePatchesRequest) *runtimehooksv1.ValidateTopologyRequest {
335+
validationRequest := &runtimehooksv1.ValidateTopologyRequest{}
336+
validationRequest.Variables = generateRequest.Variables
337+
338+
for i := range generateRequest.Items {
339+
item := generateRequest.Items[i]
340+
341+
validationRequest.Items = append(validationRequest.Items, &runtimehooksv1.ValidateTopologyRequestItem{
342+
HolderReference: item.HolderReference,
343+
Object: item.Object,
344+
Variables: item.Variables,
345+
})
346+
}
347+
348+
return validationRequest
349+
}
350+
299351
// updateDesiredState uses the patched templates of a GeneratePatchesRequest to update the desired state.
300352
// NOTE: This func should be called after all the patches have been applied to the GeneratePatchesRequest.
301353
func updateDesiredState(ctx context.Context, req *runtimehooksv1.GeneratePatchesRequest, blueprint *scope.ClusterBlueprint, desired *scope.ClusterState) error {

internal/controllers/topology/cluster/patches/engine_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ import (
2929
"k8s.io/utils/pointer"
3030

3131
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
32+
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
33+
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
3234
"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/scope"
35+
runtimecatalog "sigs.k8s.io/cluster-api/internal/runtime/catalog"
36+
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
37+
runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
3338
"sigs.k8s.io/cluster-api/internal/test/builder"
3439
. "sigs.k8s.io/cluster-api/internal/test/matchers"
3540
)
@@ -361,7 +366,17 @@ func TestApply(t *testing.T) {
361366
blueprint, desired := setupTestObjects()
362367

363368
// If there are patches, set up patch generators.
364-
patchEngine := NewEngine()
369+
// FIXME(sbueringer) implement test for external patches
370+
cat := runtimecatalog.New()
371+
g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed())
372+
373+
registry := runtimeregistry.New()
374+
g.Expect(registry.WarmUp(&runtimev1.ExtensionConfigList{})).To(Succeed())
375+
runtimeClient := runtimeclient.New(runtimeclient.Options{
376+
Catalog: cat,
377+
Registry: registry,
378+
})
379+
patchEngine := NewEngine(runtimeClient)
365380
if len(tt.patches) > 0 {
366381
// Add the patches.
367382
blueprint.ClusterClass.Spec.Patches = tt.patches

0 commit comments

Comments
 (0)