sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/topology_test.go (about) 1 /* 2 Copyright 2022 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 "context" 21 _ "embed" 22 "fmt" 23 "strings" 24 "testing" 25 26 . "github.com/onsi/gomega" 27 "github.com/onsi/gomega/types" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" 32 utilyaml "sigs.k8s.io/cluster-api/util/yaml" 33 ) 34 35 var ( 36 //go:embed assets/topology-test/new-clusterclass-and-cluster.yaml 37 newClusterClassAndClusterYAML []byte 38 39 //go:embed assets/topology-test/mock-CRDs.yaml 40 mockCRDsYAML []byte 41 42 //go:embed assets/topology-test/my-cluster-class.yaml 43 existingMyClusterClassYAML []byte 44 45 //go:embed assets/topology-test/existing-my-cluster.yaml 46 existingMyClusterYAML []byte 47 48 //go:embed assets/topology-test/existing-my-second-cluster.yaml 49 existingMySecondClusterYAML []byte 50 51 // modifiedClusterYAML changes the control plane replicas from 1 to 3. 52 //go:embed assets/topology-test/modified-my-cluster.yaml 53 modifiedMyClusterYAML []byte 54 55 // modifiedDockerMachineTemplateYAML adds metadat to the docker machine used by the control plane template.. 56 //go:embed assets/topology-test/modified-CP-dockermachinetemplate.yaml 57 modifiedDockerMachineTemplateYAML []byte 58 59 //go:embed assets/topology-test/objects-in-different-namespaces.yaml 60 objsInDifferentNamespacesYAML []byte 61 ) 62 63 func Test_topologyClient_Plan(t *testing.T) { 64 type args struct { 65 in *TopologyPlanInput 66 } 67 type item struct { 68 kind string 69 namespace string 70 namePrefix string 71 } 72 type out struct { 73 affectedClusters []client.ObjectKey 74 affectedClusterClasses []client.ObjectKey 75 reconciledCluster *client.ObjectKey 76 created []item 77 modified []item 78 deleted []item 79 } 80 tests := []struct { 81 name string 82 existingObjects []*unstructured.Unstructured 83 args args 84 want out 85 wantErr bool 86 }{ 87 { 88 name: "Input with new ClusterClass and new Cluster", 89 args: args{ 90 in: &TopologyPlanInput{ 91 Objs: mustToUnstructured(newClusterClassAndClusterYAML), 92 }, 93 }, 94 want: out{ 95 created: []item{ 96 {kind: "DockerCluster", namespace: "default", namePrefix: "my-cluster-"}, 97 {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-md-0-"}, 98 {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-md-1-"}, 99 {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, 100 {kind: "KubeadmConfigTemplate", namespace: "default", namePrefix: "my-cluster-md-0-"}, 101 {kind: "KubeadmConfigTemplate", namespace: "default", namePrefix: "my-cluster-md-1-"}, 102 {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, 103 {kind: "MachineDeployment", namespace: "default", namePrefix: "my-cluster-md-0-"}, 104 {kind: "MachineDeployment", namespace: "default", namePrefix: "my-cluster-md-1-"}, 105 }, 106 modified: []item{ 107 {kind: "Cluster", namespace: "default", namePrefix: "my-cluster"}, 108 }, 109 affectedClusters: func() []client.ObjectKey { 110 cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} 111 return []client.ObjectKey{cluster} 112 }(), 113 affectedClusterClasses: func() []client.ObjectKey { 114 cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} 115 return []client.ObjectKey{cc} 116 }(), 117 reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, 118 }, 119 wantErr: false, 120 }, 121 { 122 name: "Modifying an existing Cluster", 123 existingObjects: mustToUnstructured( 124 mockCRDsYAML, 125 existingMyClusterClassYAML, 126 existingMyClusterYAML, 127 ), 128 args: args{ 129 in: &TopologyPlanInput{ 130 Objs: mustToUnstructured(modifiedMyClusterYAML), 131 }, 132 }, 133 want: out{ 134 affectedClusters: func() []client.ObjectKey { 135 cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} 136 return []client.ObjectKey{cluster} 137 }(), 138 affectedClusterClasses: []client.ObjectKey{}, 139 modified: []item{ 140 {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, 141 }, 142 reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, 143 }, 144 wantErr: false, 145 }, 146 { 147 name: "Modifying an existing DockerMachineTemplate. Template used by Control Plane of an existing Cluster.", 148 existingObjects: mustToUnstructured( 149 mockCRDsYAML, 150 existingMyClusterClassYAML, 151 existingMyClusterYAML, 152 ), 153 args: args{ 154 in: &TopologyPlanInput{ 155 Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), 156 }, 157 }, 158 want: out{ 159 affectedClusters: func() []client.ObjectKey { 160 cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} 161 return []client.ObjectKey{cluster} 162 }(), 163 affectedClusterClasses: func() []client.ObjectKey { 164 cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} 165 return []client.ObjectKey{cc} 166 }(), 167 modified: []item{ 168 {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, 169 }, 170 created: []item{ 171 // Modifying the DockerClusterTemplate will result in template rotation. A new template will be created 172 // and used by KCP. 173 {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, 174 }, 175 reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, 176 }, 177 wantErr: false, 178 }, 179 { 180 name: "Modifying an existing DockerMachineTemplate. Affects multiple clusters. Target Cluster not specified.", 181 existingObjects: mustToUnstructured( 182 mockCRDsYAML, 183 existingMyClusterClassYAML, 184 existingMyClusterYAML, 185 existingMySecondClusterYAML, 186 ), 187 args: args{ 188 in: &TopologyPlanInput{ 189 Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), 190 }, 191 }, 192 want: out{ 193 affectedClusters: func() []client.ObjectKey { 194 cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} 195 cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} 196 return []client.ObjectKey{cluster, cluster2} 197 }(), 198 affectedClusterClasses: func() []client.ObjectKey { 199 cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} 200 return []client.ObjectKey{cc} 201 }(), 202 modified: []item{}, 203 created: []item{}, 204 reconciledCluster: nil, 205 }, 206 wantErr: false, 207 }, 208 { 209 name: "Modifying an existing DockerMachineTemplate. Affects multiple clusters. Target Cluster specified.", 210 existingObjects: mustToUnstructured( 211 mockCRDsYAML, 212 existingMyClusterClassYAML, 213 existingMyClusterYAML, 214 existingMySecondClusterYAML, 215 ), 216 args: args{ 217 in: &TopologyPlanInput{ 218 Objs: mustToUnstructured(modifiedDockerMachineTemplateYAML), 219 TargetClusterName: "my-cluster", 220 }, 221 }, 222 want: out{ 223 affectedClusters: func() []client.ObjectKey { 224 cluster := client.ObjectKey{Namespace: "default", Name: "my-cluster"} 225 cluster2 := client.ObjectKey{Namespace: "default", Name: "my-second-cluster"} 226 return []client.ObjectKey{cluster, cluster2} 227 }(), 228 affectedClusterClasses: func() []client.ObjectKey { 229 cc := client.ObjectKey{Namespace: "default", Name: "my-cluster-class"} 230 return []client.ObjectKey{cc} 231 }(), 232 modified: []item{ 233 {kind: "KubeadmControlPlane", namespace: "default", namePrefix: "my-cluster-"}, 234 }, 235 created: []item{ 236 // Modifying the DockerClusterTemplate will result in template rotation. A new template will be created 237 // and used by KCP. 238 {kind: "DockerMachineTemplate", namespace: "default", namePrefix: "my-cluster-"}, 239 }, 240 reconciledCluster: &client.ObjectKey{Namespace: "default", Name: "my-cluster"}, 241 }, 242 wantErr: false, 243 }, 244 { 245 name: "Input with objects in different namespaces should return error", 246 args: args{ 247 in: &TopologyPlanInput{ 248 Objs: mustToUnstructured(objsInDifferentNamespacesYAML), 249 }, 250 }, 251 wantErr: true, 252 }, 253 { 254 name: "Input with TargetNamespace different from objects in input should return error", 255 args: args{ 256 in: &TopologyPlanInput{ 257 Objs: mustToUnstructured(newClusterClassAndClusterYAML), 258 TargetNamespace: "different-namespace", 259 }, 260 }, 261 wantErr: true, 262 }, 263 } 264 for _, tt := range tests { 265 t.Run(tt.name, func(t *testing.T) { 266 g := NewWithT(t) 267 268 ctx := context.Background() 269 270 existingObjects := []client.Object{} 271 for _, o := range tt.existingObjects { 272 existingObjects = append(existingObjects, o) 273 } 274 proxy := test.NewFakeProxy().WithClusterAvailable(true).WithFakeCAPISetup().WithObjs(existingObjects...) 275 inventoryClient := newInventoryClient(proxy, nil) 276 tc := newTopologyClient( 277 proxy, 278 inventoryClient, 279 ) 280 281 res, err := tc.Plan(ctx, tt.args.in) 282 if tt.wantErr { 283 g.Expect(err).To(HaveOccurred()) 284 return 285 } 286 // The plan should function should not return any error. 287 g.Expect(err).ToNot(HaveOccurred()) 288 289 // Check affected ClusterClasses. 290 g.Expect(res.ClusterClasses).To(HaveLen(len(tt.want.affectedClusterClasses))) 291 for _, cc := range tt.want.affectedClusterClasses { 292 g.Expect(res.ClusterClasses).To(ContainElement(cc)) 293 } 294 295 // Check affected Clusters. 296 g.Expect(res.Clusters).To(HaveLen(len(tt.want.affectedClusters))) 297 for _, cluster := range tt.want.affectedClusters { 298 g.Expect(res.Clusters).To(ContainElement(cluster)) 299 } 300 301 // Check the reconciled cluster. 302 if tt.want.reconciledCluster == nil { 303 g.Expect(res.ReconciledCluster).To(BeNil()) 304 } else { 305 g.Expect(res.ReconciledCluster).NotTo(BeNil()) 306 g.Expect(*res.ReconciledCluster).To(BeComparableTo(*tt.want.reconciledCluster)) 307 } 308 309 // Check the created objects. 310 for _, created := range tt.want.created { 311 g.Expect(res.Created).To(ContainElement(MatchTopologyPlanOutputItem(created.kind, created.namespace, created.namePrefix))) 312 } 313 314 // Check the modified objects. 315 actualModifiedObjs := []*unstructured.Unstructured{} 316 for _, m := range res.Modified { 317 actualModifiedObjs = append(actualModifiedObjs, m.After) 318 } 319 for _, modified := range tt.want.modified { 320 g.Expect(actualModifiedObjs).To(ContainElement(MatchTopologyPlanOutputItem(modified.kind, modified.namespace, modified.namePrefix))) 321 } 322 323 // Check the deleted objects. 324 for _, deleted := range tt.want.deleted { 325 g.Expect(res.Deleted).To(ContainElement(MatchTopologyPlanOutputItem(deleted.kind, deleted.namespace, deleted.namePrefix))) 326 } 327 }) 328 } 329 } 330 331 func MatchTopologyPlanOutputItem(kind, namespace, namePrefix string) types.GomegaMatcher { 332 return &topologyPlanOutputItemMatcher{kind, namespace, namePrefix} 333 } 334 335 type topologyPlanOutputItemMatcher struct { 336 kind string 337 namespace string 338 namePrefix string 339 } 340 341 func (m *topologyPlanOutputItemMatcher) Match(actual interface{}) (bool, error) { 342 obj := actual.(*unstructured.Unstructured) 343 if obj.GetKind() != m.kind { 344 return false, nil 345 } 346 if obj.GetNamespace() != m.namespace { 347 return false, nil 348 } 349 if !strings.HasPrefix(obj.GetName(), m.namePrefix) { 350 return false, nil 351 } 352 return true, nil 353 } 354 355 func (m *topologyPlanOutputItemMatcher) FailureMessage(_ interface{}) string { 356 return fmt.Sprintf("Expected item Kind=%s, Namespace=%s, Name(prefix)=%s to be present", m.kind, m.namespace, m.namePrefix) 357 } 358 359 func (m *topologyPlanOutputItemMatcher) NegatedFailureMessage(_ interface{}) string { 360 return fmt.Sprintf("Expected item Kind=%s, Namespace=%s, Name(prefix)=%s not to be present", m.kind, m.namespace, m.namePrefix) 361 } 362 363 func convertToPtrSlice(objs []unstructured.Unstructured) []*unstructured.Unstructured { 364 res := []*unstructured.Unstructured{} 365 for i := range objs { 366 res = append(res, &objs[i]) 367 } 368 return res 369 } 370 371 func mustToUnstructured(rawyamls ...[]byte) []*unstructured.Unstructured { 372 objects := []unstructured.Unstructured{} 373 for _, raw := range rawyamls { 374 objs, err := utilyaml.ToUnstructured(raw) 375 if err != nil { 376 panic(err) 377 } 378 objects = append(objects, objs...) 379 } 380 return convertToPtrSlice(objects) 381 }