sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/cluster/cluster_controller_phases_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 cluster
    18  
    19  import (
    20  	"testing"
    21  	"time"
    22  
    23  	. "github.com/onsi/gomega"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/client-go/tools/record"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    31  
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	capierrors "sigs.k8s.io/cluster-api/errors"
    34  	"sigs.k8s.io/cluster-api/internal/test/builder"
    35  )
    36  
    37  func TestClusterReconcilePhases(t *testing.T) {
    38  	t.Run("reconcile infrastructure", func(t *testing.T) {
    39  		cluster := &clusterv1.Cluster{
    40  			ObjectMeta: metav1.ObjectMeta{
    41  				Name:      "test-cluster",
    42  				Namespace: "test-namespace",
    43  			},
    44  			Status: clusterv1.ClusterStatus{
    45  				InfrastructureReady: true,
    46  			},
    47  			Spec: clusterv1.ClusterSpec{
    48  				ControlPlaneEndpoint: clusterv1.APIEndpoint{
    49  					Host: "1.2.3.4",
    50  					Port: 8443,
    51  				},
    52  				InfrastructureRef: &corev1.ObjectReference{
    53  					APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
    54  					Kind:       "GenericInfrastructureMachine",
    55  					Name:       "test",
    56  				},
    57  			},
    58  		}
    59  
    60  		tests := []struct {
    61  			name         string
    62  			cluster      *clusterv1.Cluster
    63  			infraRef     map[string]interface{}
    64  			expectErr    bool
    65  			expectResult ctrl.Result
    66  		}{
    67  			{
    68  				name:      "returns no error if infrastructure ref is nil",
    69  				cluster:   &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster", Namespace: "test-namespace"}},
    70  				expectErr: false,
    71  			},
    72  			{
    73  				name:         "returns error if unable to reconcile infrastructure ref",
    74  				cluster:      cluster,
    75  				expectErr:    false,
    76  				expectResult: ctrl.Result{RequeueAfter: 30 * time.Second},
    77  			},
    78  			{
    79  				name:    "returns no error if infra config is marked for deletion",
    80  				cluster: cluster,
    81  				infraRef: map[string]interface{}{
    82  					"kind":       "GenericInfrastructureMachine",
    83  					"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
    84  					"metadata": map[string]interface{}{
    85  						"name":              "test",
    86  						"namespace":         "test-namespace",
    87  						"deletionTimestamp": "sometime",
    88  					},
    89  				},
    90  				expectErr: false,
    91  			},
    92  			{
    93  				name:    "returns no error if infrastructure is marked ready on cluster",
    94  				cluster: cluster,
    95  				infraRef: map[string]interface{}{
    96  					"kind":       "GenericInfrastructureMachine",
    97  					"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
    98  					"metadata": map[string]interface{}{
    99  						"name":              "test",
   100  						"namespace":         "test-namespace",
   101  						"deletionTimestamp": "sometime",
   102  					},
   103  				},
   104  				expectErr: false,
   105  			},
   106  			{
   107  				name:    "returns error if infrastructure has the paused annotation",
   108  				cluster: cluster,
   109  				infraRef: map[string]interface{}{
   110  					"kind":       "GenericInfrastructureMachine",
   111  					"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
   112  					"metadata": map[string]interface{}{
   113  						"name":      "test",
   114  						"namespace": "test-namespace",
   115  						"annotations": map[string]interface{}{
   116  							"cluster.x-k8s.io/paused": "true",
   117  						},
   118  					},
   119  				},
   120  				expectErr: false,
   121  			},
   122  		}
   123  
   124  		for _, tt := range tests {
   125  			t.Run(tt.name, func(t *testing.T) {
   126  				g := NewWithT(t)
   127  
   128  				var c client.Client
   129  				if tt.infraRef != nil {
   130  					infraConfig := &unstructured.Unstructured{Object: tt.infraRef}
   131  					c = fake.NewClientBuilder().
   132  						WithObjects(builder.GenericInfrastructureMachineCRD.DeepCopy(), tt.cluster, infraConfig).
   133  						Build()
   134  				} else {
   135  					c = fake.NewClientBuilder().
   136  						WithObjects(builder.GenericInfrastructureMachineCRD.DeepCopy(), tt.cluster).
   137  						Build()
   138  				}
   139  				r := &Reconciler{
   140  					Client:                    c,
   141  					UnstructuredCachingClient: c,
   142  					recorder:                  record.NewFakeRecorder(32),
   143  				}
   144  
   145  				res, err := r.reconcileInfrastructure(ctx, tt.cluster)
   146  				g.Expect(res).To(BeComparableTo(tt.expectResult))
   147  				if tt.expectErr {
   148  					g.Expect(err).To(HaveOccurred())
   149  				} else {
   150  					g.Expect(err).ToNot(HaveOccurred())
   151  				}
   152  			})
   153  		}
   154  	})
   155  
   156  	t.Run("reconcile kubeconfig", func(t *testing.T) {
   157  		cluster := &clusterv1.Cluster{
   158  			ObjectMeta: metav1.ObjectMeta{
   159  				Name: "test-cluster",
   160  			},
   161  			Spec: clusterv1.ClusterSpec{
   162  				ControlPlaneEndpoint: clusterv1.APIEndpoint{
   163  					Host: "1.2.3.4",
   164  					Port: 8443,
   165  				},
   166  			},
   167  		}
   168  
   169  		tests := []struct {
   170  			name        string
   171  			cluster     *clusterv1.Cluster
   172  			secret      *corev1.Secret
   173  			wantErr     bool
   174  			wantRequeue bool
   175  		}{
   176  			{
   177  				name:    "cluster not provisioned, apiEndpoint is not set",
   178  				cluster: &clusterv1.Cluster{},
   179  				wantErr: false,
   180  			},
   181  			{
   182  				name:    "kubeconfig secret found",
   183  				cluster: cluster,
   184  				secret: &corev1.Secret{
   185  					ObjectMeta: metav1.ObjectMeta{
   186  						Name: "test-cluster-kubeconfig",
   187  					},
   188  				},
   189  				wantErr: false,
   190  			},
   191  			{
   192  				name:        "kubeconfig secret not found, should requeue",
   193  				cluster:     cluster,
   194  				wantErr:     false,
   195  				wantRequeue: true,
   196  			},
   197  			{
   198  				name:    "invalid ca secret, should return error",
   199  				cluster: cluster,
   200  				secret: &corev1.Secret{
   201  					ObjectMeta: metav1.ObjectMeta{
   202  						Name: "test-cluster-ca",
   203  					},
   204  				},
   205  				wantErr: true,
   206  			},
   207  		}
   208  		for _, tt := range tests {
   209  			t.Run(tt.name, func(t *testing.T) {
   210  				g := NewWithT(t)
   211  
   212  				c := fake.NewClientBuilder().
   213  					WithObjects(tt.cluster).
   214  					Build()
   215  				if tt.secret != nil {
   216  					c = fake.NewClientBuilder().
   217  						WithObjects(tt.cluster, tt.secret).
   218  						Build()
   219  				}
   220  				r := &Reconciler{
   221  					Client:                    c,
   222  					UnstructuredCachingClient: c,
   223  					recorder:                  record.NewFakeRecorder(32),
   224  				}
   225  				res, err := r.reconcileKubeconfig(ctx, tt.cluster)
   226  				if tt.wantErr {
   227  					g.Expect(err).To(HaveOccurred())
   228  				} else {
   229  					g.Expect(err).ToNot(HaveOccurred())
   230  				}
   231  
   232  				if tt.wantRequeue {
   233  					g.Expect(res.RequeueAfter).To(BeNumerically(">=", 0))
   234  				}
   235  			})
   236  		}
   237  	})
   238  }
   239  
   240  func TestClusterReconciler_reconcilePhase(t *testing.T) {
   241  	cluster := &clusterv1.Cluster{
   242  		ObjectMeta: metav1.ObjectMeta{
   243  			Name: "test-cluster",
   244  		},
   245  		Status: clusterv1.ClusterStatus{},
   246  		Spec:   clusterv1.ClusterSpec{},
   247  	}
   248  	createClusterError := capierrors.CreateClusterError
   249  	failureMsg := "Create failed"
   250  
   251  	tests := []struct {
   252  		name      string
   253  		cluster   *clusterv1.Cluster
   254  		wantPhase clusterv1.ClusterPhase
   255  	}{
   256  		{
   257  			name:      "cluster not provisioned",
   258  			cluster:   cluster,
   259  			wantPhase: clusterv1.ClusterPhasePending,
   260  		},
   261  		{
   262  			name: "cluster has infrastructureRef",
   263  			cluster: &clusterv1.Cluster{
   264  				ObjectMeta: metav1.ObjectMeta{
   265  					Name: "test-cluster",
   266  				},
   267  				Status: clusterv1.ClusterStatus{},
   268  				Spec: clusterv1.ClusterSpec{
   269  					InfrastructureRef: &corev1.ObjectReference{},
   270  				},
   271  			},
   272  
   273  			wantPhase: clusterv1.ClusterPhaseProvisioning,
   274  		},
   275  		{
   276  			name: "cluster infrastructure is ready",
   277  			cluster: &clusterv1.Cluster{
   278  				ObjectMeta: metav1.ObjectMeta{
   279  					Name: "test-cluster",
   280  				},
   281  				Status: clusterv1.ClusterStatus{
   282  					InfrastructureReady: true,
   283  				},
   284  				Spec: clusterv1.ClusterSpec{
   285  					InfrastructureRef: &corev1.ObjectReference{},
   286  				},
   287  			},
   288  
   289  			wantPhase: clusterv1.ClusterPhaseProvisioning,
   290  		},
   291  		{
   292  			name: "cluster infrastructure is ready and ControlPlaneEndpoint is set",
   293  			cluster: &clusterv1.Cluster{
   294  				ObjectMeta: metav1.ObjectMeta{
   295  					Name: "test-cluster",
   296  				},
   297  				Spec: clusterv1.ClusterSpec{
   298  					InfrastructureRef: &corev1.ObjectReference{},
   299  					ControlPlaneEndpoint: clusterv1.APIEndpoint{
   300  						Host: "1.2.3.4",
   301  						Port: 8443,
   302  					},
   303  				},
   304  				Status: clusterv1.ClusterStatus{
   305  					InfrastructureReady: true,
   306  				},
   307  			},
   308  
   309  			wantPhase: clusterv1.ClusterPhaseProvisioned,
   310  		},
   311  		{
   312  			name: "cluster status has FailureReason",
   313  			cluster: &clusterv1.Cluster{
   314  				ObjectMeta: metav1.ObjectMeta{
   315  					Name: "test-cluster",
   316  				},
   317  				Status: clusterv1.ClusterStatus{
   318  					InfrastructureReady: true,
   319  					FailureReason:       &createClusterError,
   320  				},
   321  				Spec: clusterv1.ClusterSpec{
   322  					InfrastructureRef: &corev1.ObjectReference{},
   323  				},
   324  			},
   325  
   326  			wantPhase: clusterv1.ClusterPhaseFailed,
   327  		},
   328  		{
   329  			name: "cluster status has FailureMessage",
   330  			cluster: &clusterv1.Cluster{
   331  				ObjectMeta: metav1.ObjectMeta{
   332  					Name: "test-cluster",
   333  				},
   334  				Status: clusterv1.ClusterStatus{
   335  					InfrastructureReady: true,
   336  					FailureMessage:      &failureMsg,
   337  				},
   338  				Spec: clusterv1.ClusterSpec{
   339  					InfrastructureRef: &corev1.ObjectReference{},
   340  				},
   341  			},
   342  
   343  			wantPhase: clusterv1.ClusterPhaseFailed,
   344  		},
   345  		{
   346  			name: "cluster has deletion timestamp",
   347  			cluster: &clusterv1.Cluster{
   348  				ObjectMeta: metav1.ObjectMeta{
   349  					Name:              "test-cluster",
   350  					DeletionTimestamp: &metav1.Time{Time: time.Now().UTC()},
   351  					Finalizers:        []string{clusterv1.ClusterFinalizer},
   352  				},
   353  				Status: clusterv1.ClusterStatus{
   354  					InfrastructureReady: true,
   355  				},
   356  				Spec: clusterv1.ClusterSpec{
   357  					InfrastructureRef: &corev1.ObjectReference{},
   358  				},
   359  			},
   360  
   361  			wantPhase: clusterv1.ClusterPhaseDeleting,
   362  		},
   363  	}
   364  	for _, tt := range tests {
   365  		t.Run(tt.name, func(t *testing.T) {
   366  			g := NewWithT(t)
   367  
   368  			c := fake.NewClientBuilder().
   369  				WithObjects(tt.cluster).
   370  				Build()
   371  
   372  			r := &Reconciler{
   373  				Client:                    c,
   374  				UnstructuredCachingClient: c,
   375  				recorder:                  record.NewFakeRecorder(32),
   376  			}
   377  			r.reconcilePhase(ctx, tt.cluster)
   378  			g.Expect(tt.cluster.Status.GetTypedPhase()).To(Equal(tt.wantPhase))
   379  		})
   380  	}
   381  }
   382  
   383  func TestClusterReconcilePhases_reconcileFailureDomains(t *testing.T) {
   384  	cluster := &clusterv1.Cluster{
   385  		ObjectMeta: metav1.ObjectMeta{
   386  			Name:      "test-cluster",
   387  			Namespace: "test-namespace",
   388  		},
   389  		Status: clusterv1.ClusterStatus{
   390  			InfrastructureReady: true,
   391  		},
   392  		Spec: clusterv1.ClusterSpec{
   393  			ControlPlaneEndpoint: clusterv1.APIEndpoint{
   394  				Host: "1.2.3.4",
   395  				Port: 8443,
   396  			},
   397  			InfrastructureRef: &corev1.ObjectReference{
   398  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   399  				Kind:       "GenericInfrastructureCluster",
   400  				Name:       "test",
   401  			},
   402  		},
   403  	}
   404  
   405  	newFailureDomain := clusterv1.FailureDomains{
   406  		"newdomain": clusterv1.FailureDomainSpec{
   407  			ControlPlane: false,
   408  			Attributes: map[string]string{
   409  				"attribute1": "value1",
   410  			},
   411  		},
   412  	}
   413  
   414  	newFailureDomainUpdated := clusterv1.FailureDomains{
   415  		"newdomain": clusterv1.FailureDomainSpec{
   416  			ControlPlane: false,
   417  			Attributes: map[string]string{
   418  				"attribute2": "value2",
   419  			},
   420  		},
   421  	}
   422  
   423  	clusterWithNewFailureDomainUpdated := cluster.DeepCopy()
   424  	clusterWithNewFailureDomainUpdated.Status.FailureDomains = newFailureDomainUpdated
   425  
   426  	oldFailureDomain := clusterv1.FailureDomains{
   427  		"olddomain": clusterv1.FailureDomainSpec{
   428  			ControlPlane: false,
   429  			Attributes: map[string]string{
   430  				"attribute1": "value1",
   431  			},
   432  		},
   433  	}
   434  
   435  	clusterWithOldFailureDomain := cluster.DeepCopy()
   436  	clusterWithOldFailureDomain.Status.FailureDomains = oldFailureDomain
   437  
   438  	tests := []struct {
   439  		name                 string
   440  		cluster              *clusterv1.Cluster
   441  		infraRef             map[string]interface{}
   442  		expectFailureDomains clusterv1.FailureDomains
   443  	}{
   444  		{
   445  			name:    "expect no failure domain if infrastructure ref is nil",
   446  			cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster", Namespace: "test-namespace"}},
   447  		},
   448  		{
   449  			name:                 "expect no failure domain if infra config does not have failure domain",
   450  			cluster:              cluster.DeepCopy(),
   451  			infraRef:             generateInfraRef(false),
   452  			expectFailureDomains: clusterv1.FailureDomains{},
   453  		},
   454  		{
   455  			name:                 "expect cluster failure domain to be reset to empty if infra config does not have failure domain",
   456  			cluster:              clusterWithOldFailureDomain.DeepCopy(),
   457  			infraRef:             generateInfraRef(false),
   458  			expectFailureDomains: clusterv1.FailureDomains{},
   459  		},
   460  		{
   461  			name:                 "expect failure domain to remain same if infra config have same failure domain",
   462  			cluster:              cluster.DeepCopy(),
   463  			infraRef:             generateInfraRef(true),
   464  			expectFailureDomains: newFailureDomain,
   465  		},
   466  		{
   467  			name:                 "expect failure domain to be updated if infra config has updates to failure domain",
   468  			cluster:              clusterWithNewFailureDomainUpdated.DeepCopy(),
   469  			infraRef:             generateInfraRef(true),
   470  			expectFailureDomains: newFailureDomain,
   471  		},
   472  		{
   473  			name:                 "expect failure domain to be reset if infra config have different failure domain",
   474  			cluster:              clusterWithOldFailureDomain.DeepCopy(),
   475  			infraRef:             generateInfraRef(true),
   476  			expectFailureDomains: newFailureDomain,
   477  		},
   478  	}
   479  
   480  	for _, tt := range tests {
   481  		t.Run(tt.name, func(t *testing.T) {
   482  			g := NewWithT(t)
   483  
   484  			objs := []client.Object{builder.GenericInfrastructureClusterCRD.DeepCopy(), tt.cluster}
   485  			if tt.infraRef != nil {
   486  				objs = append(objs, &unstructured.Unstructured{Object: tt.infraRef})
   487  			}
   488  
   489  			c := fake.NewClientBuilder().WithObjects(objs...).Build()
   490  			r := &Reconciler{
   491  				Client:                    c,
   492  				UnstructuredCachingClient: c,
   493  				recorder:                  record.NewFakeRecorder(32),
   494  			}
   495  
   496  			_, err := r.reconcileInfrastructure(ctx, tt.cluster)
   497  			g.Expect(err).ToNot(HaveOccurred())
   498  			g.Expect(tt.cluster.Status.FailureDomains).To(BeEquivalentTo(tt.expectFailureDomains))
   499  		})
   500  	}
   501  }
   502  
   503  func generateInfraRef(withFailureDomain bool) map[string]interface{} {
   504  	infraRef := map[string]interface{}{
   505  		"kind":       "GenericInfrastructureCluster",
   506  		"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
   507  		"metadata": map[string]interface{}{
   508  			"name":              "test",
   509  			"namespace":         "test-namespace",
   510  			"deletionTimestamp": "sometime",
   511  		},
   512  		"status": map[string]interface{}{
   513  			"ready": true,
   514  		},
   515  	}
   516  
   517  	if withFailureDomain {
   518  		infraRef["status"] = map[string]interface{}{
   519  			"failureDomains": map[string]interface{}{
   520  				"newdomain": map[string]interface{}{
   521  					"controlPlane": false,
   522  					"attributes": map[string]interface{}{
   523  						"attribute1": "value1",
   524  					},
   525  				},
   526  			},
   527  			"ready": true,
   528  		}
   529  	}
   530  
   531  	return infraRef
   532  }