sigs.k8s.io/cluster-api/bootstrap/kubeadm@v0.0.0-20191016155141-23a891785b60/controllers/kubeadmconfig_controller_test.go (about)

     1  /*
     2  Copyright 2019 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 controllers
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"reflect"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/pkg/errors"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	fakeclient "k8s.io/client-go/kubernetes/fake"
    33  	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    34  	bootstrapapi "k8s.io/cluster-bootstrap/token/api"
    35  	"k8s.io/klog/klogr"
    36  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha2"
    37  	internalcluster "sigs.k8s.io/cluster-api/bootstrap/kubeadm/internal/cluster"
    38  	kubeadmv1beta1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/kubeadm/v1beta1"
    39  	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha2"
    40  	"sigs.k8s.io/cluster-api/util/secret"
    41  	ctrl "sigs.k8s.io/controller-runtime"
    42  	"sigs.k8s.io/controller-runtime/pkg/client"
    43  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    44  	"sigs.k8s.io/controller-runtime/pkg/handler"
    45  	"sigs.k8s.io/controller-runtime/pkg/runtime/log"
    46  )
    47  
    48  func setupScheme() *runtime.Scheme {
    49  	scheme := runtime.NewScheme()
    50  	if err := clusterv1.AddToScheme(scheme); err != nil {
    51  		panic(err)
    52  	}
    53  	if err := bootstrapv1.AddToScheme(scheme); err != nil {
    54  		panic(err)
    55  	}
    56  	if err := corev1.AddToScheme(scheme); err != nil {
    57  		panic(err)
    58  	}
    59  	return scheme
    60  }
    61  
    62  // MachineToBootstrapMapFunc return kubeadm bootstrap configref name when configref exists
    63  func TestKubeadmConfigReconciler_MachineToBootstrapMapFuncReturn(t *testing.T) {
    64  	cluster := newCluster("my-cluster")
    65  	objs := []runtime.Object{cluster}
    66  	machineObjs := []runtime.Object{}
    67  	var expectedConfigName string
    68  	for i := 0; i < 3; i++ {
    69  		m := newMachine(cluster, fmt.Sprintf("my-machine-%d", i))
    70  		configName := fmt.Sprintf("my-config-%d", i)
    71  		if i == 1 {
    72  			c := newKubeadmConfig(m, configName)
    73  			objs = append(objs, m, c)
    74  			expectedConfigName = configName
    75  		} else {
    76  			objs = append(objs, m)
    77  		}
    78  		machineObjs = append(machineObjs, m)
    79  	}
    80  	fakeClient := fake.NewFakeClientWithScheme(setupScheme(), objs...)
    81  	reconciler := &KubeadmConfigReconciler{
    82  		Log:    log.Log,
    83  		Client: fakeClient,
    84  	}
    85  	for i := 0; i < 3; i++ {
    86  		o := handler.MapObject{
    87  			Object: machineObjs[i],
    88  		}
    89  		configs := reconciler.MachineToBootstrapMapFunc(o)
    90  		if i == 1 {
    91  			if configs[0].Name != expectedConfigName {
    92  				t.Fatalf("unexpected config name: %s", configs[0].Name)
    93  			}
    94  		} else {
    95  			if configs[0].Name != "" {
    96  				t.Fatalf("unexpected config name: %s", configs[0].Name)
    97  			}
    98  		}
    99  	}
   100  }
   101  
   102  // Reconcile returns early if the kubeadm config is ready because it should never re-generate bootstrap data.
   103  func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfKubeadmConfigIsReady(t *testing.T) {
   104  	config := newKubeadmConfig(nil, "cfg")
   105  	config.Status.Ready = true
   106  
   107  	objects := []runtime.Object{
   108  		config,
   109  	}
   110  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   111  
   112  	k := &KubeadmConfigReconciler{
   113  		Log:    log.Log,
   114  		Client: myclient,
   115  	}
   116  
   117  	request := ctrl.Request{
   118  		NamespacedName: types.NamespacedName{
   119  			Namespace: "default",
   120  			Name:      "cfg",
   121  		},
   122  	}
   123  	result, err := k.Reconcile(request)
   124  	if err != nil {
   125  		t.Fatalf("Failed to reconcile:\n %+v", err)
   126  	}
   127  	if result.Requeue == true {
   128  		t.Fatal("did not expect to requeue")
   129  	}
   130  	if result.RequeueAfter != time.Duration(0) {
   131  		t.Fatal("did not expect to requeue after")
   132  	}
   133  }
   134  
   135  // Reconcile returns an error in this case because the owning machine should not go away before the things it owns.
   136  func TestKubeadmConfigReconciler_Reconcile_ReturnErrorIfReferencedMachineIsNotFound(t *testing.T) {
   137  	machine := newMachine(nil, "machine")
   138  	config := newKubeadmConfig(machine, "cfg")
   139  
   140  	objects := []runtime.Object{
   141  		// intentionally omitting machine
   142  		config,
   143  	}
   144  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   145  
   146  	k := &KubeadmConfigReconciler{
   147  		Log:    log.Log,
   148  		Client: myclient,
   149  	}
   150  
   151  	request := ctrl.Request{
   152  		NamespacedName: types.NamespacedName{
   153  			Namespace: "default",
   154  			Name:      "cfg",
   155  		},
   156  	}
   157  	_, err := k.Reconcile(request)
   158  	if err == nil {
   159  		t.Fatal("Expected error, got nil")
   160  	}
   161  }
   162  
   163  // If the machine has bootstrap data already then there is no need to generate more bootstrap data. The work is done.
   164  func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasBootstrapData(t *testing.T) {
   165  	machine := newMachine(nil, "machine")
   166  	machine.Spec.Bootstrap.Data = stringPtr("something")
   167  
   168  	config := newKubeadmConfig(machine, "cfg")
   169  	objects := []runtime.Object{
   170  		machine,
   171  		config,
   172  	}
   173  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   174  
   175  	k := &KubeadmConfigReconciler{
   176  		Log:    log.Log,
   177  		Client: myclient,
   178  	}
   179  
   180  	request := ctrl.Request{
   181  		NamespacedName: types.NamespacedName{
   182  			Namespace: "default",
   183  			Name:      "cfg",
   184  		},
   185  	}
   186  	result, err := k.Reconcile(request)
   187  	if err != nil {
   188  		t.Fatalf("Failed to reconcile:\n %+v", err)
   189  	}
   190  	if result.Requeue == true {
   191  		t.Fatal("did not expect to requeue")
   192  	}
   193  	if result.RequeueAfter != time.Duration(0) {
   194  		t.Fatal("did not expect to requeue after")
   195  	}
   196  }
   197  
   198  // Return early If the owning machine does not have an associated cluster
   199  func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasNoCluster(t *testing.T) {
   200  	machine := newMachine(nil, "machine") // Machine without a cluster
   201  	config := newKubeadmConfig(machine, "cfg")
   202  
   203  	objects := []runtime.Object{
   204  		machine,
   205  		config,
   206  	}
   207  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   208  
   209  	k := &KubeadmConfigReconciler{
   210  		Log:    log.Log,
   211  		Client: myclient,
   212  	}
   213  
   214  	request := ctrl.Request{
   215  		NamespacedName: types.NamespacedName{
   216  			Namespace: "default",
   217  			Name:      "cfg",
   218  		},
   219  	}
   220  	_, err := k.Reconcile(request)
   221  	if err != nil {
   222  		t.Fatalf("Not Expecting error, got an error: %+v", err)
   223  	}
   224  }
   225  
   226  // This does not expect an error, hoping the machine gets updated with a cluster
   227  func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfMachineDoesNotHaveAssociatedCluster(t *testing.T) {
   228  	machine := newMachine(nil, "machine") // intentionally omitting cluster
   229  	config := newKubeadmConfig(machine, "cfg")
   230  
   231  	objects := []runtime.Object{
   232  		machine,
   233  		config,
   234  	}
   235  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   236  
   237  	k := &KubeadmConfigReconciler{
   238  		Log:    log.Log,
   239  		Client: myclient,
   240  	}
   241  
   242  	request := ctrl.Request{
   243  		NamespacedName: types.NamespacedName{
   244  			Namespace: "default",
   245  			Name:      "cfg",
   246  		},
   247  	}
   248  	_, err := k.Reconcile(request)
   249  	if err != nil {
   250  		t.Fatal("Not Expecting error, got an error")
   251  	}
   252  }
   253  
   254  // This does not expect an error, hoping that the associated cluster will be created
   255  func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfAssociatedClusterIsNotFound(t *testing.T) {
   256  	cluster := newCluster("cluster")
   257  	machine := newMachine(cluster, "machine")
   258  	config := newKubeadmConfig(machine, "cfg")
   259  
   260  	objects := []runtime.Object{
   261  		// intentionally omitting cluster
   262  		machine,
   263  		config,
   264  	}
   265  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   266  
   267  	k := &KubeadmConfigReconciler{
   268  		Log:    log.Log,
   269  		Client: myclient,
   270  	}
   271  
   272  	request := ctrl.Request{
   273  		NamespacedName: types.NamespacedName{
   274  			Namespace: "default",
   275  			Name:      "cfg",
   276  		},
   277  	}
   278  	_, err := k.Reconcile(request)
   279  	if err != nil {
   280  		t.Fatal("Not Expecting error, got an error")
   281  	}
   282  }
   283  
   284  // If the control plane isn't initialized then there is no cluster for either a worker or control plane node to join.
   285  func TestKubeadmConfigReconciler_Reconcile_RequeueJoiningNodesIfControlPlaneNotInitialized(t *testing.T) {
   286  	cluster := newCluster("cluster")
   287  	cluster.Status.InfrastructureReady = true
   288  
   289  	workerMachine := newWorkerMachine(cluster)
   290  	workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine)
   291  
   292  	controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine")
   293  	controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg")
   294  
   295  	testcases := []struct {
   296  		name    string
   297  		request ctrl.Request
   298  		objects []runtime.Object
   299  	}{
   300  		{
   301  			name: "requeue worker when control plane is not yet initialiezd",
   302  			request: ctrl.Request{
   303  				NamespacedName: types.NamespacedName{
   304  					Namespace: workerJoinConfig.Namespace,
   305  					Name:      workerJoinConfig.Name,
   306  				},
   307  			},
   308  			objects: []runtime.Object{
   309  				cluster,
   310  				workerMachine,
   311  				workerJoinConfig,
   312  			},
   313  		},
   314  		{
   315  			name: "requeue a secondary control plane when the control plane is not yet initialized",
   316  			request: ctrl.Request{
   317  				NamespacedName: types.NamespacedName{
   318  					Namespace: controlPlaneJoinConfig.Namespace,
   319  					Name:      controlPlaneJoinConfig.Name,
   320  				},
   321  			},
   322  			objects: []runtime.Object{
   323  				cluster,
   324  				controlPlaneJoinMachine,
   325  				controlPlaneJoinConfig,
   326  			},
   327  		},
   328  	}
   329  	for _, tc := range testcases {
   330  		t.Run(tc.name, func(t *testing.T) {
   331  			myclient := fake.NewFakeClientWithScheme(setupScheme(), tc.objects...)
   332  
   333  			k := &KubeadmConfigReconciler{
   334  				Log:             log.Log,
   335  				Client:          myclient,
   336  				KubeadmInitLock: &myInitLocker{},
   337  			}
   338  
   339  			result, err := k.Reconcile(tc.request)
   340  			if err != nil {
   341  				t.Fatalf("Failed to reconcile:\n %+v", err)
   342  			}
   343  			if result.Requeue == true {
   344  				t.Fatal("did not expect to requeue")
   345  			}
   346  			if result.RequeueAfter != 30*time.Second {
   347  				t.Fatal("expected to requeue after 30s")
   348  			}
   349  		})
   350  	}
   351  }
   352  
   353  // This generates cloud-config data but does not test the validity of it.
   354  func TestKubeadmConfigReconciler_Reconcile_GenerateCloudConfigData(t *testing.T) {
   355  	cluster := newCluster("cluster")
   356  	cluster.Status.InfrastructureReady = true
   357  
   358  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
   359  	controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg")
   360  
   361  	objects := []runtime.Object{
   362  		cluster,
   363  		controlPlaneInitMachine,
   364  		controlPlaneInitConfig,
   365  	}
   366  	objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...)
   367  
   368  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   369  
   370  	k := &KubeadmConfigReconciler{
   371  		Log:             log.Log,
   372  		Client:          myclient,
   373  		KubeadmInitLock: &myInitLocker{},
   374  	}
   375  
   376  	request := ctrl.Request{
   377  		NamespacedName: types.NamespacedName{
   378  			Namespace: "default",
   379  			Name:      "control-plane-init-cfg",
   380  		},
   381  	}
   382  	result, err := k.Reconcile(request)
   383  	if err != nil {
   384  		t.Fatalf("Failed to reconcile:\n %+v", err)
   385  	}
   386  	if result.Requeue != false {
   387  		t.Fatal("did not expect to requeue")
   388  	}
   389  	if result.RequeueAfter != time.Duration(0) {
   390  		t.Fatal("did not expect to requeue after")
   391  	}
   392  
   393  	cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg")
   394  	if err != nil {
   395  		t.Fatalf("Failed to reconcile:\n %+v", err)
   396  	}
   397  	if cfg.Status.Ready != true {
   398  		t.Fatal("Expected status ready")
   399  	}
   400  	if cfg.Status.BootstrapData == nil {
   401  		t.Fatal("Expected generated bootstrap data")
   402  	}
   403  
   404  	// Ensure that we don't fail trying to refresh any bootstrap tokens
   405  	_, err = k.Reconcile(request)
   406  	if err != nil {
   407  		t.Fatalf("Failed to reconcile:\n %+v", err)
   408  	}
   409  }
   410  
   411  // If a control plane has no JoinConfiguration, then we will create a default and no error will occur
   412  func TestKubeadmConfigReconciler_Reconcile_ErrorIfJoiningControlPlaneHasInvalidConfiguration(t *testing.T) {
   413  	// TODO: extract this kind of code into a setup function that puts the state of objects into an initialized controlplane (implies secrets exist)
   414  	cluster := newCluster("cluster")
   415  	cluster.Status.InfrastructureReady = true
   416  	cluster.Status.ControlPlaneInitialized = true
   417  	cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}}
   418  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
   419  	controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg")
   420  
   421  	controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine")
   422  	controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg")
   423  	controlPlaneJoinConfig.Spec.JoinConfiguration.ControlPlane = nil // Makes controlPlaneJoinConfig invalid for a control plane machine
   424  
   425  	objects := []runtime.Object{
   426  		cluster,
   427  		controlPlaneJoinMachine,
   428  		controlPlaneJoinConfig,
   429  	}
   430  	objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...)
   431  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   432  
   433  	k := &KubeadmConfigReconciler{
   434  		Log:                  log.Log,
   435  		Client:               myclient,
   436  		SecretsClientFactory: newFakeSecretFactory(),
   437  		KubeadmInitLock:      &myInitLocker{},
   438  	}
   439  
   440  	request := ctrl.Request{
   441  		NamespacedName: types.NamespacedName{
   442  			Namespace: "default",
   443  			Name:      "control-plane-join-cfg",
   444  		},
   445  	}
   446  	_, err := k.Reconcile(request)
   447  	if err != nil {
   448  		t.Fatalf("Expected no error but got %v", err)
   449  	}
   450  }
   451  
   452  // If there is no APIEndpoint but everything is ready then requeue in hopes of a new APIEndpoint showing up eventually.
   453  func TestKubeadmConfigReconciler_Reconcile_RequeueIfControlPlaneIsMissingAPIEndpoints(t *testing.T) {
   454  	cluster := newCluster("cluster")
   455  	cluster.Status.InfrastructureReady = true
   456  	cluster.Status.ControlPlaneInitialized = true
   457  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
   458  	controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg")
   459  
   460  	workerMachine := newWorkerMachine(cluster)
   461  	workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine)
   462  
   463  	objects := []runtime.Object{
   464  		cluster,
   465  		workerMachine,
   466  		workerJoinConfig,
   467  	}
   468  	objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...)
   469  
   470  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   471  
   472  	k := &KubeadmConfigReconciler{
   473  		Log:             log.Log,
   474  		Client:          myclient,
   475  		KubeadmInitLock: &myInitLocker{},
   476  	}
   477  
   478  	request := ctrl.Request{
   479  		NamespacedName: types.NamespacedName{
   480  			Namespace: "default",
   481  			Name:      "worker-join-cfg",
   482  		},
   483  	}
   484  	result, err := k.Reconcile(request)
   485  	if err != nil {
   486  		t.Fatalf("Failed to reconcile:\n %+v", err)
   487  	}
   488  	if result.Requeue == true {
   489  		t.Fatal("did not expect to requeue")
   490  	}
   491  	if result.RequeueAfter != 10*time.Second {
   492  		t.Fatal("expected to requeue after 10s")
   493  	}
   494  }
   495  
   496  func TestReconcileIfJoinNodesAndControlPlaneIsReady(t *testing.T) {
   497  	cluster := newCluster("cluster")
   498  	cluster.Status.InfrastructureReady = true
   499  	cluster.Status.ControlPlaneInitialized = true
   500  	cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}}
   501  
   502  	var useCases = []struct {
   503  		name          string
   504  		machine       *clusterv1.Machine
   505  		configName    string
   506  		configBuilder func(*clusterv1.Machine, string) *bootstrapv1.KubeadmConfig
   507  	}{
   508  		{
   509  			name:       "Join a worker node with a fully compiled kubeadm config object",
   510  			machine:    newWorkerMachine(cluster),
   511  			configName: "worker-join-cfg",
   512  			configBuilder: func(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig {
   513  				return newWorkerJoinKubeadmConfig(machine)
   514  			},
   515  		},
   516  		{
   517  			name:          "Join a worker node  with an empty kubeadm config object (defaults apply)",
   518  			machine:       newWorkerMachine(cluster),
   519  			configName:    "worker-join-cfg",
   520  			configBuilder: newKubeadmConfig,
   521  		},
   522  		{
   523  			name:          "Join a control plane node with a fully compiled kubeadm config object",
   524  			machine:       newControlPlaneMachine(cluster, "control-plane-join-machine"),
   525  			configName:    "control-plane-join-cfg",
   526  			configBuilder: newControlPlaneJoinKubeadmConfig,
   527  		},
   528  		{
   529  			name:          "Join a control plane node with an empty kubeadm config object (defaults apply)",
   530  			machine:       newControlPlaneMachine(cluster, "control-plane-join-machine"),
   531  			configName:    "control-plane-join-cfg",
   532  			configBuilder: newKubeadmConfig,
   533  		},
   534  	}
   535  
   536  	for _, rt := range useCases {
   537  		rt := rt // pin!
   538  		t.Run(rt.name, func(t *testing.T) {
   539  			config := rt.configBuilder(rt.machine, rt.configName)
   540  
   541  			objects := []runtime.Object{
   542  				cluster,
   543  				rt.machine,
   544  				config,
   545  			}
   546  			objects = append(objects, createSecrets(t, cluster, config)...)
   547  			myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   548  			k := &KubeadmConfigReconciler{
   549  				Log:                  log.Log,
   550  				Client:               myclient,
   551  				SecretsClientFactory: newFakeSecretFactory(),
   552  				KubeadmInitLock:      &myInitLocker{},
   553  			}
   554  
   555  			request := ctrl.Request{
   556  				NamespacedName: types.NamespacedName{
   557  					Namespace: config.GetNamespace(),
   558  					Name:      rt.configName,
   559  				},
   560  			}
   561  			result, err := k.Reconcile(request)
   562  			if err != nil {
   563  				t.Fatal(fmt.Sprintf("Failed to reconcile:\n %+v", err))
   564  			}
   565  			if result.Requeue == true {
   566  				t.Fatal("did not expected to requeue")
   567  			}
   568  			if result.RequeueAfter != time.Duration(0) {
   569  				t.Fatal("did not expected to requeue after")
   570  			}
   571  
   572  			cfg, err := getKubeadmConfig(myclient, rt.configName)
   573  			if err != nil {
   574  				t.Fatal(fmt.Sprintf("Failed to reconcile:\n %+v", err))
   575  			}
   576  
   577  			if cfg.Status.Ready != true {
   578  				t.Fatal("Expected status ready")
   579  			}
   580  
   581  			if cfg.Status.BootstrapData == nil {
   582  				t.Fatal("Expected status ready")
   583  			}
   584  
   585  			myremoteclient, _ := k.SecretsClientFactory.NewSecretsClient(nil, nil)
   586  			l, err := myremoteclient.List(metav1.ListOptions{})
   587  			if err != nil {
   588  				t.Fatal(fmt.Sprintf("Failed to get secrets after reconcile:\n %+v", err))
   589  			}
   590  
   591  			if len(l.Items) != 1 {
   592  				t.Fatal("Failed to get bootstrap token secret")
   593  			}
   594  		})
   595  
   596  	}
   597  }
   598  
   599  func TestBootstrapTokenTTLExtension(t *testing.T) {
   600  	cluster := newCluster("cluster")
   601  	cluster.Status.InfrastructureReady = true
   602  	cluster.Status.ControlPlaneInitialized = true
   603  	cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}}
   604  
   605  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
   606  	initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-config")
   607  	workerMachine := newWorkerMachine(cluster)
   608  	workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine)
   609  	controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine")
   610  	controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg")
   611  	objects := []runtime.Object{
   612  		cluster,
   613  		workerMachine,
   614  		workerJoinConfig,
   615  		controlPlaneJoinMachine,
   616  		controlPlaneJoinConfig,
   617  	}
   618  
   619  	objects = append(objects, createSecrets(t, cluster, initConfig)...)
   620  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
   621  	k := &KubeadmConfigReconciler{
   622  		Log:                  log.Log,
   623  		Client:               myclient,
   624  		SecretsClientFactory: newFakeSecretFactory(),
   625  		KubeadmInitLock:      &myInitLocker{},
   626  	}
   627  	request := ctrl.Request{
   628  		NamespacedName: types.NamespacedName{
   629  			Namespace: "default",
   630  			Name:      "worker-join-cfg",
   631  		},
   632  	}
   633  	result, err := k.Reconcile(request)
   634  	if err != nil {
   635  		t.Fatalf("Failed to reconcile:\n %+v", err)
   636  	}
   637  	if result.Requeue == true {
   638  		t.Fatal("did not expect to requeue")
   639  	}
   640  	if result.RequeueAfter != time.Duration(0) {
   641  		t.Fatal("did not expect to requeue after")
   642  	}
   643  	cfg, err := getKubeadmConfig(myclient, "worker-join-cfg")
   644  	if err != nil {
   645  		t.Fatalf("Failed to reconcile:\n %+v", err)
   646  	}
   647  	if cfg.Status.Ready != true {
   648  		t.Fatal("Expected status ready")
   649  	}
   650  	if cfg.Status.BootstrapData == nil {
   651  		t.Fatal("Expected status ready")
   652  	}
   653  	request = ctrl.Request{
   654  		NamespacedName: types.NamespacedName{
   655  			Namespace: "default",
   656  			Name:      "control-plane-join-cfg",
   657  		},
   658  	}
   659  	result, err = k.Reconcile(request)
   660  	if err != nil {
   661  		t.Fatalf("Failed to reconcile:\n %+v", err)
   662  	}
   663  	if result.Requeue == true {
   664  		t.Fatal("did not expect to requeue")
   665  	}
   666  	if result.RequeueAfter != time.Duration(0) {
   667  		t.Fatal("did not expect to requeue after")
   668  	}
   669  	cfg, err = getKubeadmConfig(myclient, "control-plane-join-cfg")
   670  	if err != nil {
   671  		t.Fatalf("Failed to reconcile:\n %+v", err)
   672  	}
   673  	if cfg.Status.Ready != true {
   674  		t.Fatal("Expected status ready")
   675  	}
   676  	if cfg.Status.BootstrapData == nil {
   677  		t.Fatal("Expected status ready")
   678  	}
   679  
   680  	myremoteclient, _ := k.SecretsClientFactory.NewSecretsClient(nil, nil)
   681  	l, err := myremoteclient.List(metav1.ListOptions{})
   682  	if err != nil {
   683  		t.Fatalf("Failed to read secrets:\n %+v", err)
   684  	}
   685  
   686  	if len(l.Items) != 2 {
   687  		t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items))
   688  	}
   689  
   690  	// ensure that the token is refreshed...
   691  	tokenExpires := make([][]byte, len(l.Items))
   692  
   693  	for i, item := range l.Items {
   694  		tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey]
   695  	}
   696  
   697  	<-time.After(1 * time.Second)
   698  
   699  	for _, req := range []ctrl.Request{
   700  		{
   701  			NamespacedName: types.NamespacedName{
   702  				Namespace: "default",
   703  				Name:      "worker-join-cfg",
   704  			},
   705  		},
   706  		{
   707  			NamespacedName: types.NamespacedName{
   708  				Namespace: "default",
   709  				Name:      "control-plane-join-cfg",
   710  			},
   711  		},
   712  	} {
   713  
   714  		result, err := k.Reconcile(req)
   715  		if err != nil {
   716  			t.Fatalf("Failed to reconcile:\n %+v", err)
   717  		}
   718  		if result.RequeueAfter >= DefaultTokenTTL {
   719  			t.Fatal("expected a requeue duration less than the token TTL")
   720  		}
   721  	}
   722  
   723  	l, err = myremoteclient.List(metav1.ListOptions{})
   724  	if err != nil {
   725  		t.Fatalf("Failed to read secrets:\n %+v", err)
   726  	}
   727  
   728  	if len(l.Items) != 2 {
   729  		t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items))
   730  	}
   731  
   732  	for i, item := range l.Items {
   733  		if bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey]) {
   734  			t.Fatal("Reconcile should have refreshed bootstrap token's expiration until the infrastructure was ready")
   735  		}
   736  		tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey]
   737  	}
   738  
   739  	// ...until the infrastructure is marked "ready"
   740  	workerMachine.Status.InfrastructureReady = true
   741  	err = myclient.Update(context.Background(), workerMachine)
   742  	if err != nil {
   743  		t.Fatalf("unable to set machine infrastructure ready: %v", err)
   744  	}
   745  
   746  	controlPlaneJoinMachine.Status.InfrastructureReady = true
   747  	err = myclient.Update(context.Background(), controlPlaneJoinMachine)
   748  	if err != nil {
   749  		t.Fatalf("unable to set machine infrastructure ready: %v", err)
   750  	}
   751  
   752  	<-time.After(1 * time.Second)
   753  
   754  	for _, req := range []ctrl.Request{
   755  		{
   756  			NamespacedName: types.NamespacedName{
   757  				Namespace: "default",
   758  				Name:      "worker-join-cfg",
   759  			},
   760  		},
   761  		{
   762  			NamespacedName: types.NamespacedName{
   763  				Namespace: "default",
   764  				Name:      "control-plane-join-cfg",
   765  			},
   766  		},
   767  	} {
   768  
   769  		result, err := k.Reconcile(req)
   770  		if err != nil {
   771  			t.Fatalf("Failed to reconcile:\n %+v", err)
   772  		}
   773  		if result.Requeue == true {
   774  			t.Fatal("did not expect to requeue")
   775  		}
   776  		if result.RequeueAfter != time.Duration(0) {
   777  			t.Fatal("did not expect to requeue after")
   778  		}
   779  	}
   780  
   781  	l, err = myremoteclient.List(metav1.ListOptions{})
   782  	if err != nil {
   783  		t.Fatalf("Failed to read secrets:\n %+v", err)
   784  	}
   785  
   786  	if len(l.Items) != 2 {
   787  		t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items))
   788  	}
   789  
   790  	for i, item := range l.Items {
   791  		if !bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey]) {
   792  			t.Fatal("Reconcile should have let the bootstrap token expire after the infrastructure was ready")
   793  		}
   794  	}
   795  }
   796  
   797  // Ensure the discovery portion of the JoinConfiguration gets generated correctly.
   798  func TestKubeadmConfigReconciler_Reconcile_DisocveryReconcileBehaviors(t *testing.T) {
   799  	k := &KubeadmConfigReconciler{
   800  		Log:                  log.Log,
   801  		Client:               nil,
   802  		SecretsClientFactory: newFakeSecretFactory(),
   803  		KubeadmInitLock:      &myInitLocker{},
   804  	}
   805  
   806  	dummyCAHash := []string{"...."}
   807  	bootstrapToken := kubeadmv1beta1.Discovery{
   808  		BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{
   809  			CACertHashes: dummyCAHash,
   810  		},
   811  	}
   812  	goodcluster := &clusterv1.Cluster{
   813  		Status: clusterv1.ClusterStatus{
   814  			APIEndpoints: []clusterv1.APIEndpoint{
   815  				{
   816  					Host: "example.com",
   817  					Port: 6443,
   818  				},
   819  			},
   820  		},
   821  	}
   822  	testcases := []struct {
   823  		name              string
   824  		cluster           *clusterv1.Cluster
   825  		config            *bootstrapv1.KubeadmConfig
   826  		validateDiscovery func(*bootstrapv1.KubeadmConfig) error
   827  	}{
   828  		{
   829  			name:    "Automatically generate token if discovery not specified",
   830  			cluster: goodcluster,
   831  			config: &bootstrapv1.KubeadmConfig{
   832  				Spec: bootstrapv1.KubeadmConfigSpec{
   833  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   834  						Discovery: bootstrapToken,
   835  					},
   836  				},
   837  			},
   838  			validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error {
   839  				d := c.Spec.JoinConfiguration.Discovery
   840  				if d.BootstrapToken == nil {
   841  					return errors.Errorf("BootstrapToken expected, got nil")
   842  				}
   843  				if d.BootstrapToken.Token == "" {
   844  					return errors.Errorf(("BootstrapToken.Token expected, got empty string"))
   845  				}
   846  				if d.BootstrapToken.APIServerEndpoint != "example.com:6443" {
   847  					return errors.Errorf("BootstrapToken.APIServerEndpoint=example.com:6443 expected, got %q", d.BootstrapToken.APIServerEndpoint)
   848  				}
   849  				if d.BootstrapToken.UnsafeSkipCAVerification == true {
   850  					return errors.Errorf("BootstrapToken.UnsafeSkipCAVerification=false expected, got true")
   851  				}
   852  				return nil
   853  			},
   854  		},
   855  		{
   856  			name:    "Respect discoveryConfiguration.File",
   857  			cluster: goodcluster,
   858  			config: &bootstrapv1.KubeadmConfig{
   859  				Spec: bootstrapv1.KubeadmConfigSpec{
   860  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   861  						Discovery: kubeadmv1beta1.Discovery{
   862  							File: &kubeadmv1beta1.FileDiscovery{},
   863  						},
   864  					},
   865  				},
   866  			},
   867  			validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error {
   868  				d := c.Spec.JoinConfiguration.Discovery
   869  				if d.BootstrapToken != nil {
   870  					return errors.Errorf("BootstrapToken should not be created when DiscoveryFile is defined")
   871  				}
   872  				return nil
   873  			},
   874  		},
   875  		{
   876  			name:    "Respect discoveryConfiguration.BootstrapToken.APIServerEndpoint",
   877  			cluster: goodcluster,
   878  			config: &bootstrapv1.KubeadmConfig{
   879  				Spec: bootstrapv1.KubeadmConfigSpec{
   880  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   881  						Discovery: kubeadmv1beta1.Discovery{
   882  							BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{
   883  								CACertHashes:      dummyCAHash,
   884  								APIServerEndpoint: "bar.com:6443",
   885  							},
   886  						},
   887  					},
   888  				},
   889  			},
   890  			validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error {
   891  				d := c.Spec.JoinConfiguration.Discovery
   892  				if d.BootstrapToken.APIServerEndpoint != "bar.com:6443" {
   893  					return errors.Errorf("BootstrapToken.APIServerEndpoint=https://bar.com:6443 expected, got %s", d.BootstrapToken.APIServerEndpoint)
   894  				}
   895  				return nil
   896  			},
   897  		},
   898  		{
   899  			name:    "Respect discoveryConfiguration.BootstrapToken.Token",
   900  			cluster: goodcluster,
   901  			config: &bootstrapv1.KubeadmConfig{
   902  				Spec: bootstrapv1.KubeadmConfigSpec{
   903  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   904  						Discovery: kubeadmv1beta1.Discovery{
   905  							BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{
   906  								CACertHashes: dummyCAHash,
   907  								Token:        "abcdef.0123456789abcdef",
   908  							},
   909  						},
   910  					},
   911  				},
   912  			},
   913  			validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error {
   914  				d := c.Spec.JoinConfiguration.Discovery
   915  				if d.BootstrapToken.Token != "abcdef.0123456789abcdef" {
   916  					return errors.Errorf("BootstrapToken.Token=abcdef.0123456789abcdef expected, got %s", d.BootstrapToken.Token)
   917  				}
   918  				return nil
   919  			},
   920  		},
   921  		{
   922  			name:    "Respect discoveryConfiguration.BootstrapToken.CACertHashes",
   923  			cluster: goodcluster,
   924  			config: &bootstrapv1.KubeadmConfig{
   925  				Spec: bootstrapv1.KubeadmConfigSpec{
   926  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   927  						Discovery: kubeadmv1beta1.Discovery{
   928  							BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{
   929  								CACertHashes: dummyCAHash,
   930  							},
   931  						},
   932  					},
   933  				},
   934  			},
   935  			validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error {
   936  				d := c.Spec.JoinConfiguration.Discovery
   937  				if !reflect.DeepEqual(d.BootstrapToken.CACertHashes, dummyCAHash) {
   938  					return errors.Errorf("BootstrapToken.CACertHashes=%s expected, got %s", dummyCAHash, d.BootstrapToken.Token)
   939  				}
   940  				return nil
   941  			},
   942  		},
   943  	}
   944  
   945  	for _, tc := range testcases {
   946  		t.Run(tc.name, func(t *testing.T) {
   947  			err := k.reconcileDiscovery(tc.cluster, tc.config, internalcluster.Certificates{})
   948  			if err != nil {
   949  				t.Errorf("expected nil, got error %v", err)
   950  			}
   951  
   952  			if err := tc.validateDiscovery(tc.config); err != nil {
   953  				t.Fatal(err)
   954  			}
   955  		})
   956  	}
   957  }
   958  
   959  // Test failure cases for the discovery reconcile function.
   960  func TestKubeadmConfigReconciler_Reconcile_DisocveryReconcileFailureBehaviors(t *testing.T) {
   961  	k := &KubeadmConfigReconciler{
   962  		Log:    log.Log,
   963  		Client: nil,
   964  	}
   965  
   966  	testcases := []struct {
   967  		name    string
   968  		cluster *clusterv1.Cluster
   969  		config  *bootstrapv1.KubeadmConfig
   970  	}{
   971  		{
   972  			name:    "Fail if cluster has not APIEndpoints",
   973  			cluster: &clusterv1.Cluster{}, // cluster without endpoints
   974  			config: &bootstrapv1.KubeadmConfig{
   975  				Spec: bootstrapv1.KubeadmConfigSpec{
   976  					JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
   977  						Discovery: kubeadmv1beta1.Discovery{
   978  							BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{
   979  								CACertHashes: []string{"item"},
   980  							},
   981  						},
   982  					},
   983  				},
   984  			},
   985  		},
   986  	}
   987  
   988  	for _, tc := range testcases {
   989  		t.Run(tc.name, func(t *testing.T) {
   990  			err := k.reconcileDiscovery(tc.cluster, tc.config, internalcluster.Certificates{})
   991  			if err == nil {
   992  				t.Error("expected error, got nil")
   993  			}
   994  		})
   995  	}
   996  }
   997  
   998  // Set cluster configuration defaults based on dynamic values from the cluster object.
   999  func TestKubeadmConfigReconciler_Reconcile_DynamicDefaultsForClusterConfiguration(t *testing.T) {
  1000  	k := &KubeadmConfigReconciler{
  1001  		Log:    log.Log,
  1002  		Client: nil,
  1003  	}
  1004  
  1005  	testcases := []struct {
  1006  		name    string
  1007  		cluster *clusterv1.Cluster
  1008  		machine *clusterv1.Machine
  1009  		config  *bootstrapv1.KubeadmConfig
  1010  	}{
  1011  		{
  1012  			name: "Config settings have precedence",
  1013  			config: &bootstrapv1.KubeadmConfig{
  1014  				Spec: bootstrapv1.KubeadmConfigSpec{
  1015  					ClusterConfiguration: &kubeadmv1beta1.ClusterConfiguration{
  1016  						ClusterName:       "mycluster",
  1017  						KubernetesVersion: "myversion",
  1018  						Networking: kubeadmv1beta1.Networking{
  1019  							PodSubnet:     "myPodSubnet",
  1020  							ServiceSubnet: "myServiceSubnet",
  1021  							DNSDomain:     "myDNSDomain",
  1022  						},
  1023  						ControlPlaneEndpoint: "myControlPlaneEndpoint:6443",
  1024  					},
  1025  				},
  1026  			},
  1027  			cluster: &clusterv1.Cluster{
  1028  				ObjectMeta: metav1.ObjectMeta{
  1029  					Name: "OtherName",
  1030  				},
  1031  				Spec: clusterv1.ClusterSpec{
  1032  					ClusterNetwork: &clusterv1.ClusterNetwork{
  1033  						Services:      &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherServicesCidr"}},
  1034  						Pods:          &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherPodsCidr"}},
  1035  						ServiceDomain: "otherServiceDomain",
  1036  					},
  1037  				},
  1038  				Status: clusterv1.ClusterStatus{
  1039  					APIEndpoints: []clusterv1.APIEndpoint{{Host: "otherVersion", Port: 0}},
  1040  				},
  1041  			},
  1042  			machine: &clusterv1.Machine{
  1043  				Spec: clusterv1.MachineSpec{
  1044  					Version: stringPtr("otherVersion"),
  1045  				},
  1046  			},
  1047  		},
  1048  		{
  1049  			name: "Top level object settings are used in case config settings are missing",
  1050  			config: &bootstrapv1.KubeadmConfig{
  1051  				Spec: bootstrapv1.KubeadmConfigSpec{
  1052  					ClusterConfiguration: &kubeadmv1beta1.ClusterConfiguration{},
  1053  				},
  1054  			},
  1055  			cluster: &clusterv1.Cluster{
  1056  				ObjectMeta: metav1.ObjectMeta{
  1057  					Name: "mycluster",
  1058  				},
  1059  				Spec: clusterv1.ClusterSpec{
  1060  					ClusterNetwork: &clusterv1.ClusterNetwork{
  1061  						Services:      &clusterv1.NetworkRanges{CIDRBlocks: []string{"myServiceSubnet"}},
  1062  						Pods:          &clusterv1.NetworkRanges{CIDRBlocks: []string{"myPodSubnet"}},
  1063  						ServiceDomain: "myDNSDomain",
  1064  					},
  1065  				},
  1066  				Status: clusterv1.ClusterStatus{
  1067  					APIEndpoints: []clusterv1.APIEndpoint{{Host: "myControlPlaneEndpoint", Port: 6443}},
  1068  				},
  1069  			},
  1070  			machine: &clusterv1.Machine{
  1071  				Spec: clusterv1.MachineSpec{
  1072  					Version: stringPtr("myversion"),
  1073  				},
  1074  			},
  1075  		},
  1076  	}
  1077  
  1078  	for _, tc := range testcases {
  1079  		t.Run(tc.name, func(t *testing.T) {
  1080  			k.reconcileTopLevelObjectSettings(tc.cluster, tc.machine, tc.config)
  1081  
  1082  			if tc.config.Spec.ClusterConfiguration.ControlPlaneEndpoint != "myControlPlaneEndpoint:6443" {
  1083  				t.Errorf("expected ClusterConfiguration.ControlPlaneEndpoint %q, got %q", "myControlPlaneEndpoint:6443", tc.config.Spec.ClusterConfiguration.ControlPlaneEndpoint)
  1084  			}
  1085  			if tc.config.Spec.ClusterConfiguration.ClusterName != "mycluster" {
  1086  				t.Errorf("expected ClusterConfiguration.ClusterName %q, got %q", "mycluster", tc.config.Spec.ClusterConfiguration.ClusterName)
  1087  			}
  1088  			if tc.config.Spec.ClusterConfiguration.Networking.PodSubnet != "myPodSubnet" {
  1089  				t.Errorf("expected ClusterConfiguration.Networking.PodSubnet  %q, got %q", "myPodSubnet", tc.config.Spec.ClusterConfiguration.Networking.PodSubnet)
  1090  			}
  1091  			if tc.config.Spec.ClusterConfiguration.Networking.ServiceSubnet != "myServiceSubnet" {
  1092  				t.Errorf("expected ClusterConfiguration.Networking.ServiceSubnet  %q, got %q", "myServiceSubnet", tc.config.Spec.ClusterConfiguration.Networking.ServiceSubnet)
  1093  			}
  1094  			if tc.config.Spec.ClusterConfiguration.Networking.DNSDomain != "myDNSDomain" {
  1095  				t.Errorf("expected ClusterConfiguration.Networking.DNSDomain  %q, got %q", "myDNSDomain", tc.config.Spec.ClusterConfiguration.Networking.DNSDomain)
  1096  			}
  1097  			if tc.config.Spec.ClusterConfiguration.KubernetesVersion != "myversion" {
  1098  				t.Errorf("expected ClusterConfiguration.KubernetesVersion %q, got %q", "myversion", tc.config.Spec.ClusterConfiguration.KubernetesVersion)
  1099  			}
  1100  		})
  1101  	}
  1102  }
  1103  
  1104  // Allow users to skip CA Verification if they *really* want to.
  1105  func TestKubeadmConfigReconciler_Reconcile_AlwaysCheckCAVerificationUnlessRequestedToSkip(t *testing.T) {
  1106  	// Setup work for an initialized cluster
  1107  	clusterName := "my-cluster"
  1108  	cluster := newCluster(clusterName)
  1109  	cluster.Status.ControlPlaneInitialized = true
  1110  	cluster.Status.InfrastructureReady = true
  1111  	cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{
  1112  		{
  1113  			Host: "example.com",
  1114  			Port: 6443,
  1115  		},
  1116  	}
  1117  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "my-control-plane-init-machine")
  1118  	initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "my-control-plane-init-config")
  1119  
  1120  	controlPlaneMachineName := "my-machine"
  1121  	machine := newMachine(cluster, controlPlaneMachineName)
  1122  
  1123  	workerMachineName := "my-worker"
  1124  	workerMachine := newMachine(cluster, workerMachineName)
  1125  
  1126  	controlPlaneConfigName := "my-config"
  1127  	config := newKubeadmConfig(machine, controlPlaneConfigName)
  1128  
  1129  	objects := []runtime.Object{
  1130  		cluster, machine, workerMachine, config,
  1131  	}
  1132  	objects = append(objects, createSecrets(t, cluster, initConfig)...)
  1133  
  1134  	testcases := []struct {
  1135  		name               string
  1136  		discovery          *kubeadmv1beta1.BootstrapTokenDiscovery
  1137  		skipCAVerification bool
  1138  	}{
  1139  		{
  1140  			name:               "Do not skip CA verification by default",
  1141  			discovery:          &kubeadmv1beta1.BootstrapTokenDiscovery{},
  1142  			skipCAVerification: false,
  1143  		},
  1144  		{
  1145  			name: "Skip CA verification if requested by the user",
  1146  			discovery: &kubeadmv1beta1.BootstrapTokenDiscovery{
  1147  				UnsafeSkipCAVerification: true,
  1148  			},
  1149  			skipCAVerification: true,
  1150  		},
  1151  		{
  1152  			// skipCAVerification should be true since no Cert Hashes are provided, but reconcile will *always* get or create certs.
  1153  			// TODO: Certificate get/create behavior needs to be mocked to enable this test.
  1154  			name: "cannot test for defaulting behavior through the reconcile function",
  1155  			discovery: &kubeadmv1beta1.BootstrapTokenDiscovery{
  1156  				CACertHashes: []string{""},
  1157  			},
  1158  			skipCAVerification: false,
  1159  		},
  1160  	}
  1161  	for _, tc := range testcases {
  1162  		t.Run(tc.name, func(t *testing.T) {
  1163  			myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
  1164  			reconciler := KubeadmConfigReconciler{
  1165  				Client:               myclient,
  1166  				SecretsClientFactory: newFakeSecretFactory(),
  1167  				KubeadmInitLock:      &myInitLocker{},
  1168  				Log:                  klogr.New(),
  1169  			}
  1170  
  1171  			wc := newWorkerJoinKubeadmConfig(workerMachine)
  1172  			wc.Spec.JoinConfiguration.Discovery.BootstrapToken = tc.discovery
  1173  			key := types.NamespacedName{Namespace: wc.Namespace, Name: wc.Name}
  1174  			if err := myclient.Create(context.Background(), wc); err != nil {
  1175  				t.Fatal(err)
  1176  			}
  1177  			req := ctrl.Request{NamespacedName: key}
  1178  			if _, err := reconciler.Reconcile(req); err != nil {
  1179  				t.Fatalf("reconciled an error: %v", err)
  1180  			}
  1181  			cfg := &bootstrapv1.KubeadmConfig{}
  1182  			if err := myclient.Get(context.Background(), key, cfg); err != nil {
  1183  				t.Fatal(err)
  1184  			}
  1185  			if cfg.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification != tc.skipCAVerification {
  1186  				t.Fatalf("Expected skip CA verification: %v but was %v", tc.skipCAVerification, !tc.skipCAVerification)
  1187  			}
  1188  		})
  1189  	}
  1190  }
  1191  
  1192  // If a cluster object changes then all associated KubeadmConfigs should be re-reconciled.
  1193  // This allows us to not requeue a kubeadm config while we wait for InfrastructureReady.
  1194  func TestKubeadmConfigReconciler_ClusterToKubeadmConfigs(t *testing.T) {
  1195  	cluster := newCluster("my-cluster")
  1196  	objs := []runtime.Object{cluster}
  1197  	expectedNames := []string{}
  1198  	for i := 0; i < 3; i++ {
  1199  		m := newMachine(cluster, fmt.Sprintf("my-machine-%d", i))
  1200  		configName := fmt.Sprintf("my-config-%d", i)
  1201  		c := newKubeadmConfig(m, configName)
  1202  		expectedNames = append(expectedNames, configName)
  1203  		objs = append(objs, m, c)
  1204  	}
  1205  	fakeClient := fake.NewFakeClientWithScheme(setupScheme(), objs...)
  1206  	reconciler := &KubeadmConfigReconciler{
  1207  		Log:    log.Log,
  1208  		Client: fakeClient,
  1209  	}
  1210  	o := handler.MapObject{
  1211  		Object: cluster,
  1212  	}
  1213  	configs := reconciler.ClusterToKubeadmConfigs(o)
  1214  	names := make([]string, 3)
  1215  	for i := range configs {
  1216  		names[i] = configs[i].Name
  1217  	}
  1218  	for _, name := range expectedNames {
  1219  		found := false
  1220  		for _, foundName := range names {
  1221  			if foundName == name {
  1222  				found = true
  1223  			}
  1224  		}
  1225  		if !found {
  1226  			t.Fatalf("did not find %s in %v", name, names)
  1227  		}
  1228  	}
  1229  }
  1230  
  1231  // Reconcile should not fail if the Etcd CA Secret already exists
  1232  func TestKubeadmConfigReconciler_Reconcile_DoesNotFailIfCASecretsAlreadyExist(t *testing.T) {
  1233  	cluster := newCluster("my-cluster")
  1234  	cluster.Status.InfrastructureReady = true
  1235  	cluster.Status.ControlPlaneInitialized = false
  1236  	m := newControlPlaneMachine(cluster, "control-plane-machine")
  1237  	configName := "my-config"
  1238  	c := newControlPlaneInitKubeadmConfig(m, configName)
  1239  	scrt := &corev1.Secret{
  1240  		ObjectMeta: metav1.ObjectMeta{
  1241  			Name:      fmt.Sprintf("%s-%s", cluster.Name, internalcluster.EtcdCA),
  1242  			Namespace: "default",
  1243  		},
  1244  		Data: map[string][]byte{
  1245  			"tls.crt": []byte("hello world"),
  1246  			"tls.key": []byte("hello world"),
  1247  		},
  1248  	}
  1249  	fakec := fake.NewFakeClientWithScheme(setupScheme(), []runtime.Object{cluster, m, c, scrt}...)
  1250  	reconciler := &KubeadmConfigReconciler{
  1251  		Log:             log.Log,
  1252  		Client:          fakec,
  1253  		KubeadmInitLock: &myInitLocker{},
  1254  	}
  1255  	req := ctrl.Request{
  1256  		NamespacedName: types.NamespacedName{Namespace: "default", Name: configName},
  1257  	}
  1258  	if _, err := reconciler.Reconcile(req); err != nil {
  1259  		t.Fatal(err)
  1260  	}
  1261  }
  1262  
  1263  // Exactly one control plane machine initializes if there are multiple control plane machines defined
  1264  func TestKubeadmConfigReconciler_Reconcile_ExactlyOneControlPlaneMachineInitializes(t *testing.T) {
  1265  	cluster := newCluster("cluster")
  1266  	cluster.Status.InfrastructureReady = true
  1267  
  1268  	controlPlaneInitMachineFirst := newControlPlaneMachine(cluster, "control-plane-init-machine-first")
  1269  	controlPlaneInitConfigFirst := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineFirst, "control-plane-init-cfg-first")
  1270  
  1271  	controlPlaneInitMachineSecond := newControlPlaneMachine(cluster, "control-plane-init-machine-second")
  1272  	controlPlaneInitConfigSecond := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineSecond, "control-plane-init-cfg-second")
  1273  
  1274  	objects := []runtime.Object{
  1275  		cluster,
  1276  		controlPlaneInitMachineFirst,
  1277  		controlPlaneInitConfigFirst,
  1278  		controlPlaneInitMachineSecond,
  1279  		controlPlaneInitConfigSecond,
  1280  	}
  1281  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
  1282  	k := &KubeadmConfigReconciler{
  1283  		Log:                  log.Log,
  1284  		Client:               myclient,
  1285  		SecretsClientFactory: newFakeSecretFactory(),
  1286  		KubeadmInitLock:      &myInitLocker{},
  1287  	}
  1288  
  1289  	request := ctrl.Request{
  1290  		NamespacedName: types.NamespacedName{
  1291  			Namespace: "default",
  1292  			Name:      "control-plane-init-cfg-first",
  1293  		},
  1294  	}
  1295  	result, err := k.Reconcile(request)
  1296  	if err != nil {
  1297  		t.Fatalf("Failed to reconcile:\n %+v", err)
  1298  	}
  1299  	if result.Requeue == true {
  1300  		t.Fatal("did not expect to requeue")
  1301  	}
  1302  	if result.RequeueAfter != time.Duration(0) {
  1303  		t.Fatal("did not expect to requeue after")
  1304  	}
  1305  
  1306  	request = ctrl.Request{
  1307  		NamespacedName: types.NamespacedName{
  1308  			Namespace: "default",
  1309  			Name:      "control-plane-init-cfg-second",
  1310  		},
  1311  	}
  1312  	result, err = k.Reconcile(request)
  1313  	if err != nil {
  1314  		t.Fatalf("Failed to reconcile:\n %+v", err)
  1315  	}
  1316  	if result.Requeue == true {
  1317  		t.Fatal("did not expect to requeue")
  1318  	}
  1319  	if result.RequeueAfter != 30*time.Second {
  1320  		t.Fatal("expected to requeue after 30s")
  1321  	}
  1322  }
  1323  
  1324  // No patch should be applied if there is an error in reconcile
  1325  func TestKubeadmConfigReconciler_Reconcile_DoNotPatchWhenErrorOccurred(t *testing.T) {
  1326  	cluster := newCluster("cluster")
  1327  	cluster.Status.InfrastructureReady = true
  1328  
  1329  	controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine")
  1330  	controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg")
  1331  
  1332  	// set InitConfiguration as nil, we will check this to determine if the kubeadm config has been patched
  1333  	controlPlaneInitConfig.Spec.InitConfiguration = nil
  1334  
  1335  	objects := []runtime.Object{
  1336  		cluster,
  1337  		controlPlaneInitMachine,
  1338  		controlPlaneInitConfig,
  1339  	}
  1340  
  1341  	secrets := createSecrets(t, cluster, controlPlaneInitConfig)
  1342  	for _, obj := range secrets {
  1343  		s := obj.(*corev1.Secret)
  1344  		delete(s.Data, secret.TLSCrtDataName) // destroy the secrets, which will cause Reconcile to fail
  1345  		objects = append(objects, s)
  1346  	}
  1347  
  1348  	myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...)
  1349  	k := &KubeadmConfigReconciler{
  1350  		Log:                  log.Log,
  1351  		Client:               myclient,
  1352  		SecretsClientFactory: newFakeSecretFactory(),
  1353  		KubeadmInitLock:      &myInitLocker{},
  1354  	}
  1355  
  1356  	request := ctrl.Request{
  1357  		NamespacedName: types.NamespacedName{
  1358  			Namespace: "default",
  1359  			Name:      "control-plane-init-cfg",
  1360  		},
  1361  	}
  1362  
  1363  	result, err := k.Reconcile(request)
  1364  	if err == nil {
  1365  		t.Fatal("Expected error, got nil")
  1366  	}
  1367  	if result.Requeue != false {
  1368  		t.Fatal("did not expect to requeue")
  1369  	}
  1370  	if result.RequeueAfter != time.Duration(0) {
  1371  		t.Fatal("did not expect to requeue after")
  1372  	}
  1373  
  1374  	cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg")
  1375  	if err != nil {
  1376  		t.Fatalf("Failed to reconcile:\n %+v", err)
  1377  	}
  1378  
  1379  	// check if the kubeadm config has been patched
  1380  	if cfg.Spec.InitConfiguration != nil {
  1381  		t.Fatal("did not expect to patch the kubeadm config if there was an error in Reconcile")
  1382  	}
  1383  }
  1384  
  1385  // test utils
  1386  
  1387  // newCluster return a CAPI cluster object
  1388  func newCluster(name string) *clusterv1.Cluster {
  1389  	return &clusterv1.Cluster{
  1390  		TypeMeta: metav1.TypeMeta{
  1391  			Kind:       "Cluster",
  1392  			APIVersion: clusterv1.GroupVersion.String(),
  1393  		},
  1394  		ObjectMeta: metav1.ObjectMeta{
  1395  			Namespace: "default",
  1396  			Name:      name,
  1397  		},
  1398  	}
  1399  }
  1400  
  1401  // newMachine return a CAPI machine object; if cluster is not nil, the machine is linked to the cluster as well
  1402  func newMachine(cluster *clusterv1.Cluster, name string) *clusterv1.Machine {
  1403  	machine := &clusterv1.Machine{
  1404  		TypeMeta: metav1.TypeMeta{
  1405  			Kind:       "Machine",
  1406  			APIVersion: clusterv1.GroupVersion.String(),
  1407  		},
  1408  		ObjectMeta: metav1.ObjectMeta{
  1409  			Namespace: "default",
  1410  			Name:      name,
  1411  		},
  1412  		Spec: clusterv1.MachineSpec{
  1413  			Bootstrap: clusterv1.Bootstrap{
  1414  				ConfigRef: &corev1.ObjectReference{
  1415  					Kind:       "KubeadmConfig",
  1416  					APIVersion: bootstrapv1.GroupVersion.String(),
  1417  				},
  1418  			},
  1419  		},
  1420  	}
  1421  	if cluster != nil {
  1422  		machine.ObjectMeta.Labels = map[string]string{
  1423  			clusterv1.MachineClusterLabelName: cluster.Name,
  1424  		}
  1425  	}
  1426  	return machine
  1427  }
  1428  
  1429  func newWorkerMachine(cluster *clusterv1.Cluster) *clusterv1.Machine {
  1430  	return newMachine(cluster, "worker-machine") // machine by default is a worker node (not the bootstrapNode)
  1431  }
  1432  
  1433  func newControlPlaneMachine(cluster *clusterv1.Cluster, name string) *clusterv1.Machine {
  1434  	m := newMachine(cluster, name)
  1435  	m.Labels[clusterv1.MachineControlPlaneLabelName] = "true"
  1436  	return m
  1437  }
  1438  
  1439  // newKubeadmConfig return a CABPK KubeadmConfig object; if machine is not nil, the KubeadmConfig is linked to the machine as well
  1440  func newKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig {
  1441  	config := &bootstrapv1.KubeadmConfig{
  1442  		TypeMeta: metav1.TypeMeta{
  1443  			Kind:       "KubeadmConfig",
  1444  			APIVersion: bootstrapv1.GroupVersion.String(),
  1445  		},
  1446  		ObjectMeta: metav1.ObjectMeta{
  1447  			Namespace: "default",
  1448  			Name:      name,
  1449  		},
  1450  	}
  1451  	if machine != nil {
  1452  		config.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
  1453  			{
  1454  				Kind:       "Machine",
  1455  				APIVersion: clusterv1.GroupVersion.String(),
  1456  				Name:       machine.Name,
  1457  				UID:        types.UID(fmt.Sprintf("%s uid", machine.Name)),
  1458  			},
  1459  		}
  1460  		machine.Spec.Bootstrap.ConfigRef.Name = config.Name
  1461  		machine.Spec.Bootstrap.ConfigRef.Namespace = config.Namespace
  1462  	}
  1463  	return config
  1464  }
  1465  
  1466  func newWorkerJoinKubeadmConfig(machine *clusterv1.Machine) *bootstrapv1.KubeadmConfig {
  1467  	c := newKubeadmConfig(machine, "worker-join-cfg")
  1468  	c.Spec.JoinConfiguration = &kubeadmv1beta1.JoinConfiguration{
  1469  		ControlPlane: nil,
  1470  	}
  1471  	return c
  1472  }
  1473  
  1474  func newControlPlaneJoinKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig {
  1475  	c := newKubeadmConfig(machine, name)
  1476  	c.Spec.JoinConfiguration = &kubeadmv1beta1.JoinConfiguration{
  1477  		ControlPlane: &kubeadmv1beta1.JoinControlPlane{},
  1478  	}
  1479  	return c
  1480  }
  1481  
  1482  func newControlPlaneInitKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig {
  1483  	c := newKubeadmConfig(machine, name)
  1484  	c.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{}
  1485  	c.Spec.InitConfiguration = &kubeadmv1beta1.InitConfiguration{}
  1486  	return c
  1487  }
  1488  
  1489  func createSecrets(t *testing.T, cluster *clusterv1.Cluster, owner *bootstrapv1.KubeadmConfig) []runtime.Object {
  1490  	out := []runtime.Object{}
  1491  	if owner.Spec.ClusterConfiguration == nil {
  1492  		owner.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{}
  1493  	}
  1494  	certificates := internalcluster.NewCertificatesForInitialControlPlane(owner.Spec.ClusterConfiguration)
  1495  	if err := certificates.Generate(); err != nil {
  1496  		t.Fatal(err)
  1497  	}
  1498  	for _, certificate := range certificates {
  1499  		out = append(out, certificate.AsSecret(cluster, owner))
  1500  	}
  1501  	return out
  1502  }
  1503  
  1504  func stringPtr(s string) *string {
  1505  	return &s
  1506  }
  1507  
  1508  // TODO this is not a fake but an actual client whose behavior we cannot control.
  1509  // TODO remove this, probably when https://github.com/kubernetes-sigs/cluster-api-bootstrap-provider-kubeadm/issues/127 is closed.
  1510  func newFakeSecretFactory() FakeSecretFactory {
  1511  	return FakeSecretFactory{
  1512  		client: fakeclient.NewSimpleClientset().CoreV1().Secrets(metav1.NamespaceSystem),
  1513  	}
  1514  }
  1515  
  1516  type FakeSecretFactory struct {
  1517  	client typedcorev1.SecretInterface
  1518  }
  1519  
  1520  func (f FakeSecretFactory) NewSecretsClient(client client.Client, cluster *clusterv1.Cluster) (typedcorev1.SecretInterface, error) {
  1521  	return f.client, nil
  1522  }
  1523  
  1524  type myInitLocker struct {
  1525  	locked bool
  1526  }
  1527  
  1528  func (m *myInitLocker) Lock(_ context.Context, _ *clusterv1.Cluster, _ *clusterv1.Machine) bool {
  1529  	if !m.locked {
  1530  		m.locked = true
  1531  		return true
  1532  	}
  1533  	return false
  1534  }
  1535  
  1536  func (m *myInitLocker) Unlock(_ context.Context, _ *clusterv1.Cluster) bool {
  1537  	if m.locked {
  1538  		m.locked = false
  1539  	}
  1540  	return true
  1541  }