sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/blueprint_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cluster
    18  
    19  import (
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	. "github.com/onsi/gomega"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    28  	. "sigs.k8s.io/controller-runtime/pkg/envtest/komega"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api/exp/topology/scope"
    32  	"sigs.k8s.io/cluster-api/internal/test/builder"
    33  )
    34  
    35  func TestGetBlueprint(t *testing.T) {
    36  	crds := []client.Object{
    37  		builder.GenericInfrastructureClusterTemplateCRD,
    38  		builder.GenericInfrastructureMachineTemplateCRD,
    39  		builder.GenericInfrastructureMachineCRD,
    40  		builder.GenericInfrastructureMachinePoolTemplateCRD,
    41  		builder.GenericInfrastructureMachinePoolCRD,
    42  		builder.GenericControlPlaneTemplateCRD,
    43  		builder.GenericBootstrapConfigTemplateCRD,
    44  	}
    45  
    46  	// The following is a block creating a number of objects for use in the test cases.
    47  
    48  	infraClusterTemplate := builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infraclustertemplate1").
    49  		Build()
    50  	controlPlaneTemplate := builder.ControlPlaneTemplate(metav1.NamespaceDefault, "controlplanetemplate1").
    51  		Build()
    52  
    53  	controlPlaneInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "controlplaneinframachinetemplate1").
    54  		Build()
    55  	controlPlaneTemplateWithInfrastructureMachine := builder.ControlPlaneTemplate(metav1.NamespaceDefault, "controlplanetempaltewithinfrastructuremachine1").
    56  		WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
    57  		Build()
    58  
    59  	workerInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "workerinframachinetemplate1").
    60  		Build()
    61  	workerInfrastructureMachinePoolTemplate := builder.InfrastructureMachinePoolTemplate(metav1.NamespaceDefault, "workerinframachinepooltemplate1").
    62  		Build()
    63  	workerBootstrapTemplate := builder.BootstrapTemplate(metav1.NamespaceDefault, "workerbootstraptemplate1").
    64  		Build()
    65  	machineHealthCheck := &clusterv1.MachineHealthCheckClass{
    66  		NodeStartupTimeout: &metav1.Duration{
    67  			Duration: time.Duration(1)},
    68  	}
    69  
    70  	machineDeployment := builder.MachineDeploymentClass("workerclass1").
    71  		WithLabels(map[string]string{"foo": "bar"}).
    72  		WithAnnotations(map[string]string{"a": "b"}).
    73  		WithInfrastructureTemplate(workerInfrastructureMachineTemplate).
    74  		WithBootstrapTemplate(workerBootstrapTemplate).
    75  		WithMachineHealthCheckClass(machineHealthCheck).
    76  		Build()
    77  
    78  	mds := []clusterv1.MachineDeploymentClass{*machineDeployment}
    79  
    80  	machinePools := builder.MachinePoolClass("workerclass2").
    81  		WithLabels(map[string]string{"foo": "bar"}).
    82  		WithAnnotations(map[string]string{"a": "b"}).
    83  		WithInfrastructureTemplate(workerInfrastructureMachinePoolTemplate).
    84  		WithBootstrapTemplate(workerBootstrapTemplate).
    85  		Build()
    86  	mps := []clusterv1.MachinePoolClass{*machinePools}
    87  
    88  	// Define test cases.
    89  	tests := []struct {
    90  		name         string
    91  		clusterClass *clusterv1.ClusterClass
    92  		objects      []client.Object
    93  		want         *scope.ClusterBlueprint
    94  		wantErr      bool
    95  	}{
    96  		{
    97  			name: "Fails if ClusterClass does not have reference to the InfrastructureClusterTemplate",
    98  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass1").
    99  				// No InfrastructureClusterTemplate reference!
   100  				Build(),
   101  			wantErr: true,
   102  		},
   103  		{
   104  			name: "Fails if ClusterClass references an InfrastructureClusterTemplate that does not exist",
   105  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass1").
   106  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   107  				Build(),
   108  			objects: []client.Object{
   109  				// infraClusterTemplate is missing!
   110  			},
   111  			wantErr: true,
   112  		},
   113  		{
   114  			name: "Fails if ClusterClass does not have reference to the ControlPlaneTemplate",
   115  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   116  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   117  				// No ControlPlaneTemplate reference!
   118  				Build(),
   119  			objects: []client.Object{
   120  				infraClusterTemplate,
   121  			},
   122  			wantErr: true,
   123  		},
   124  		{
   125  			name: "Fails if ClusterClass does not have reference to the ControlPlaneTemplate",
   126  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   127  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   128  				WithControlPlaneTemplate(controlPlaneTemplate).
   129  				Build(),
   130  			objects: []client.Object{
   131  				infraClusterTemplate,
   132  				// ControlPlaneTemplate is missing!
   133  			},
   134  			wantErr: true,
   135  		},
   136  		{
   137  			name: "Should read a ClusterClass without worker classes",
   138  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   139  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   140  				WithControlPlaneTemplate(controlPlaneTemplate).
   141  				Build(),
   142  			objects: []client.Object{
   143  				infraClusterTemplate,
   144  				controlPlaneTemplate,
   145  			},
   146  			want: &scope.ClusterBlueprint{
   147  				ClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   148  					WithInfrastructureClusterTemplate(infraClusterTemplate).
   149  					WithControlPlaneTemplate(controlPlaneTemplate).
   150  					Build(),
   151  				InfrastructureClusterTemplate: infraClusterTemplate,
   152  				ControlPlane: &scope.ControlPlaneBlueprint{
   153  					Template: controlPlaneTemplate,
   154  				},
   155  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{},
   156  				MachinePools:       map[string]*scope.MachinePoolBlueprint{},
   157  			},
   158  		},
   159  		{
   160  			name: "Should read a ClusterClass referencing an InfrastructureMachineTemplate for the ControlPlane (but without any worker class)",
   161  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   162  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   163  				WithControlPlaneTemplate(controlPlaneTemplateWithInfrastructureMachine).
   164  				WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
   165  				Build(),
   166  			objects: []client.Object{
   167  				infraClusterTemplate,
   168  				controlPlaneTemplateWithInfrastructureMachine,
   169  				controlPlaneInfrastructureMachineTemplate,
   170  			},
   171  			want: &scope.ClusterBlueprint{
   172  				ClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   173  					WithInfrastructureClusterTemplate(infraClusterTemplate).
   174  					WithControlPlaneTemplate(controlPlaneTemplateWithInfrastructureMachine).
   175  					WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
   176  					Build(),
   177  				InfrastructureClusterTemplate: infraClusterTemplate,
   178  				ControlPlane: &scope.ControlPlaneBlueprint{
   179  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   180  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   181  				},
   182  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{},
   183  				MachinePools:       map[string]*scope.MachinePoolBlueprint{},
   184  			},
   185  		},
   186  		{
   187  			name: "Fails if ClusterClass references an InfrastructureMachineTemplate for the ControlPlane that does not exist",
   188  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   189  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   190  				WithControlPlaneTemplate(controlPlaneTemplate).
   191  				WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
   192  				Build(),
   193  			objects: []client.Object{
   194  				infraClusterTemplate,
   195  				controlPlaneTemplate,
   196  				// controlPlaneInfrastructureMachineTemplate is missing!
   197  			},
   198  			wantErr: true,
   199  		},
   200  		{
   201  			name: "Should read a ClusterClass with a MachineDeploymentClass",
   202  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   203  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   204  				WithControlPlaneTemplate(controlPlaneTemplate).
   205  				WithWorkerMachineDeploymentClasses(mds...).
   206  				Build(),
   207  			objects: []client.Object{
   208  				infraClusterTemplate,
   209  				controlPlaneTemplate,
   210  				workerInfrastructureMachineTemplate,
   211  				workerBootstrapTemplate,
   212  			},
   213  			want: &scope.ClusterBlueprint{
   214  				ClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   215  					WithInfrastructureClusterTemplate(infraClusterTemplate).
   216  					WithControlPlaneTemplate(controlPlaneTemplate).
   217  					WithWorkerMachineDeploymentClasses(mds...).
   218  					Build(),
   219  				InfrastructureClusterTemplate: infraClusterTemplate,
   220  				ControlPlane: &scope.ControlPlaneBlueprint{
   221  					Template: controlPlaneTemplate,
   222  				},
   223  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   224  					"workerclass1": {
   225  						Metadata: clusterv1.ObjectMeta{
   226  							Labels:      map[string]string{"foo": "bar"},
   227  							Annotations: map[string]string{"a": "b"},
   228  						},
   229  						InfrastructureMachineTemplate: workerInfrastructureMachineTemplate,
   230  						BootstrapTemplate:             workerBootstrapTemplate,
   231  						MachineHealthCheck:            machineHealthCheck,
   232  					},
   233  				},
   234  				MachinePools: map[string]*scope.MachinePoolBlueprint{},
   235  			},
   236  		},
   237  		{
   238  			name: "Fails if ClusterClass has a MachineDeploymentClass referencing a BootstrapConfigTemplate that does not exist",
   239  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   240  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   241  				WithControlPlaneTemplate(controlPlaneTemplate).
   242  				WithWorkerMachineDeploymentClasses(mds...).
   243  				Build(),
   244  			objects: []client.Object{
   245  				infraClusterTemplate,
   246  				controlPlaneTemplate,
   247  				workerInfrastructureMachineTemplate,
   248  				// workerBootstrapTemplate is missing!
   249  			},
   250  			wantErr: true,
   251  		},
   252  		{
   253  			name: "Fails if ClusterClass has a MachineDeploymentClass referencing a InfrastructureMachineTemplate that does not exist",
   254  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   255  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   256  				WithControlPlaneTemplate(controlPlaneTemplate).
   257  				WithWorkerMachineDeploymentClasses(mds...).
   258  				Build(),
   259  			objects: []client.Object{
   260  				infraClusterTemplate,
   261  				controlPlaneTemplate,
   262  				workerBootstrapTemplate,
   263  				// workerInfrastructureTemplate is missing!
   264  			},
   265  			wantErr: true,
   266  		},
   267  		{
   268  			name: "Should read a ClusterClass with a MachineHealthCheck in the ControlPlane",
   269  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   270  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   271  				WithControlPlaneTemplate(controlPlaneTemplate).
   272  				WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
   273  				WithControlPlaneMachineHealthCheck(machineHealthCheck).
   274  				Build(),
   275  			objects: []client.Object{
   276  				infraClusterTemplate,
   277  				controlPlaneTemplate,
   278  				controlPlaneInfrastructureMachineTemplate,
   279  			},
   280  			want: &scope.ClusterBlueprint{
   281  				ClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   282  					WithInfrastructureClusterTemplate(infraClusterTemplate).
   283  					WithControlPlaneTemplate(controlPlaneTemplate).
   284  					WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
   285  					WithControlPlaneMachineHealthCheck(machineHealthCheck).
   286  					Build(),
   287  				InfrastructureClusterTemplate: infraClusterTemplate,
   288  				ControlPlane: &scope.ControlPlaneBlueprint{
   289  					Template:                      controlPlaneTemplate,
   290  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   291  					MachineHealthCheck:            machineHealthCheck,
   292  				},
   293  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{},
   294  				MachinePools:       map[string]*scope.MachinePoolBlueprint{},
   295  			},
   296  		},
   297  		{
   298  			name: "Should read a ClusterClass with a MachinePoolClass",
   299  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   300  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   301  				WithControlPlaneTemplate(controlPlaneTemplate).
   302  				WithWorkerMachinePoolClasses(mps...).
   303  				Build(),
   304  			objects: []client.Object{
   305  				infraClusterTemplate,
   306  				controlPlaneTemplate,
   307  				workerInfrastructureMachinePoolTemplate,
   308  				workerBootstrapTemplate,
   309  			},
   310  			want: &scope.ClusterBlueprint{
   311  				ClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   312  					WithInfrastructureClusterTemplate(infraClusterTemplate).
   313  					WithControlPlaneTemplate(controlPlaneTemplate).
   314  					WithWorkerMachinePoolClasses(mps...).
   315  					Build(),
   316  				InfrastructureClusterTemplate: infraClusterTemplate,
   317  				ControlPlane: &scope.ControlPlaneBlueprint{
   318  					Template: controlPlaneTemplate,
   319  				},
   320  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{},
   321  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   322  					"workerclass2": {
   323  						Metadata: clusterv1.ObjectMeta{
   324  							Labels:      map[string]string{"foo": "bar"},
   325  							Annotations: map[string]string{"a": "b"},
   326  						},
   327  						InfrastructureMachinePoolTemplate: workerInfrastructureMachinePoolTemplate,
   328  						BootstrapTemplate:                 workerBootstrapTemplate,
   329  					},
   330  				},
   331  			},
   332  		},
   333  		{
   334  			name: "Fails if ClusterClass has a MachinePoolClass referencing a BootstrapConfigTemplate that does not exist",
   335  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   336  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   337  				WithControlPlaneTemplate(controlPlaneTemplate).
   338  				WithWorkerMachinePoolClasses(mps...).
   339  				Build(),
   340  			objects: []client.Object{
   341  				infraClusterTemplate,
   342  				controlPlaneTemplate,
   343  				workerInfrastructureMachinePoolTemplate,
   344  				// workerBootstrapTemplate is missing!
   345  			},
   346  			wantErr: true,
   347  		},
   348  		{
   349  			name: "Fails if ClusterClass has a MachinePoolClass referencing a InfrastructureMachinePoolTemplate that does not exist",
   350  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   351  				WithInfrastructureClusterTemplate(infraClusterTemplate).
   352  				WithControlPlaneTemplate(controlPlaneTemplate).
   353  				WithWorkerMachinePoolClasses(mps...).
   354  				Build(),
   355  			objects: []client.Object{
   356  				infraClusterTemplate,
   357  				controlPlaneTemplate,
   358  				workerBootstrapTemplate,
   359  				// workerInfrastructureMachinePoolTemplate is missing!
   360  			},
   361  			wantErr: true,
   362  		},
   363  	}
   364  	for _, tt := range tests {
   365  		t.Run(tt.name, func(t *testing.T) {
   366  			g := NewWithT(t)
   367  
   368  			// Set up a cluster using the ClusterClass, if any.
   369  			cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
   370  				WithTopology(
   371  					builder.ClusterTopology().
   372  						WithClass("class1").
   373  						Build()).
   374  				Build()
   375  
   376  			// If no clusterClass is defined in the test case fill in a dummy value "foo".
   377  			if tt.clusterClass == nil {
   378  				cluster.Spec.Topology.Class = "foo"
   379  			}
   380  
   381  			// Sets up the fakeClient for the test case.
   382  			objs := []client.Object{}
   383  			objs = append(objs, crds...)
   384  			objs = append(objs, tt.objects...)
   385  			if tt.clusterClass != nil {
   386  				objs = append(objs, tt.clusterClass)
   387  			}
   388  			fakeClient := fake.NewClientBuilder().
   389  				WithScheme(fakeScheme).
   390  				WithObjects(objs...).
   391  				Build()
   392  
   393  			// Calls getBlueprint.
   394  			r := &Reconciler{
   395  				Client:                    fakeClient,
   396  				patchHelperFactory:        dryRunPatchHelperFactory(fakeClient),
   397  				UnstructuredCachingClient: fakeClient,
   398  			}
   399  			got, err := r.getBlueprint(ctx, scope.New(cluster).Current.Cluster, tt.clusterClass)
   400  
   401  			// Checks the return error.
   402  			if tt.wantErr {
   403  				g.Expect(err).To(HaveOccurred())
   404  			} else {
   405  				g.Expect(err).ToNot(HaveOccurred())
   406  			}
   407  
   408  			if tt.want == nil {
   409  				g.Expect(got).To(BeNil())
   410  				return
   411  			}
   412  
   413  			// Use EqualObject where an object is created and passed through the fake client. Elsewhere the Equal method
   414  			// is enough to establish inequality.
   415  			g.Expect(tt.want.ClusterClass).To(EqualObject(got.ClusterClass, IgnoreAutogeneratedMetadata))
   416  			g.Expect(tt.want.InfrastructureClusterTemplate).To(EqualObject(got.InfrastructureClusterTemplate), cmp.Diff(got.InfrastructureClusterTemplate, tt.want.InfrastructureClusterTemplate))
   417  			g.Expect(got.ControlPlane).To(BeComparableTo(tt.want.ControlPlane), cmp.Diff(got.ControlPlane, tt.want.ControlPlane))
   418  			g.Expect(tt.want.MachineDeployments).To(BeComparableTo(got.MachineDeployments), cmp.Diff(got.MachineDeployments, tt.want.MachineDeployments))
   419  			g.Expect(tt.want.MachinePools).To(BeComparableTo(got.MachinePools), cmp.Diff(got.MachinePools, tt.want.MachinePools))
   420  		})
   421  	}
   422  }