@@ -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.
5053type 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.
301353func updateDesiredState (ctx context.Context , req * runtimehooksv1.GeneratePatchesRequest , blueprint * scope.ClusterBlueprint , desired * scope.ClusterState ) error {
0 commit comments