k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/componentconfigs/fakeconfig_test.go (about) 1 /* 2 Copyright 2020 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 componentconfigs 18 19 import ( 20 "crypto/sha256" 21 "fmt" 22 "reflect" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/lithammer/dedent" 28 29 v1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime" 32 clientset "k8s.io/client-go/kubernetes" 33 clientsetfake "k8s.io/client-go/kubernetes/fake" 34 35 kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" 36 kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" 37 kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" 38 outputapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha3" 39 "k8s.io/kubernetes/cmd/kubeadm/app/constants" 40 kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" 41 ) 42 43 // All tests in this file use an alternative set of `known` component configs. 44 // In this case it's just one known config and it's kubeadm's very own ClusterConfiguration. 45 // ClusterConfiguration is normally not managed by this package. It's only used, because of the following: 46 // - It's a versioned API that is under the control of kubeadm maintainers. This enables us to test 47 // the componentconfigs package more thoroughly without having to have full and always up to date 48 // knowledge about the config of another component. 49 // - Other components often introduce new fields in their configs without bumping up the config version. 50 // This, often times, requires that the PR that introduces such new fields to touch kubeadm test code. 51 // Doing so, requires more work on the part of developers and reviewers. When kubeadm moves out of k/k 52 // this would allow for more sporadic breaks in kubeadm tests as PRs that merge in k/k and introduce 53 // new fields won't be able to fix the tests in kubeadm. 54 // - If we implement tests for all common functionality using the config of another component and it gets 55 // deprecated and/or we stop supporting it in production, we'll have to focus on a massive test refactoring 56 // or just continue importing this config just for test use. 57 // 58 // Thus, to reduce maintenance costs without sacrificing test coverage, we introduce this mini-framework 59 // and set of tests here which replace the normal component configs with a single one (ClusterConfiguration) 60 // and test the component config independent logic of this package. 61 62 // clusterConfigHandler is the handler instance for the latest supported ClusterConfiguration to be used in tests 63 var clusterConfigHandler = handler{ 64 GroupVersion: kubeadmapiv1.SchemeGroupVersion, 65 AddToScheme: kubeadmapiv1.AddToScheme, 66 CreateEmpty: func() kubeadmapi.ComponentConfig { 67 return &clusterConfig{ 68 configBase: configBase{ 69 GroupVersion: kubeadmapiv1.SchemeGroupVersion, 70 }, 71 } 72 }, 73 fromCluster: clusterConfigFromCluster, 74 } 75 76 func clusterConfigFromCluster(h *handler, clientset clientset.Interface, _ *kubeadmapi.ClusterConfiguration) (kubeadmapi.ComponentConfig, error) { 77 return h.fromConfigMap(clientset, constants.KubeadmConfigConfigMap, constants.ClusterConfigurationConfigMapKey, true) 78 } 79 80 type clusterConfig struct { 81 configBase 82 config kubeadmapiv1.ClusterConfiguration 83 } 84 85 func (cc *clusterConfig) DeepCopy() kubeadmapi.ComponentConfig { 86 result := &clusterConfig{} 87 cc.configBase.DeepCopyInto(&result.configBase) 88 cc.config.DeepCopyInto(&result.config) 89 return result 90 } 91 92 func (cc *clusterConfig) Marshal() ([]byte, error) { 93 return cc.configBase.Marshal(&cc.config) 94 } 95 96 func (cc *clusterConfig) Unmarshal(docmap kubeadmapi.DocumentMap) error { 97 return cc.configBase.Unmarshal(docmap, &cc.config) 98 } 99 100 func (cc *clusterConfig) Get() interface{} { 101 return &cc.config 102 } 103 104 func (cc *clusterConfig) Set(cfg interface{}) { 105 cc.config = *cfg.(*kubeadmapiv1.ClusterConfiguration) 106 } 107 108 func (cc *clusterConfig) Default(_ *kubeadmapi.ClusterConfiguration, _ *kubeadmapi.APIEndpoint, _ *kubeadmapi.NodeRegistrationOptions) { 109 cc.config.ClusterName = "foo" 110 cc.config.KubernetesVersion = "bar" 111 } 112 113 func (cc *clusterConfig) Mutate() error { 114 return nil 115 } 116 117 // fakeKnown replaces temporarily during the execution of each test here known (in configset.go) 118 var fakeKnown = []*handler{ 119 &clusterConfigHandler, 120 } 121 122 // fakeKnownContext is the func that houses the fake component config context. 123 // NOTE: It does not support concurrent test execution! 124 func fakeKnownContext(f func()) { 125 // Save the real values 126 realKnown := known 127 realScheme := Scheme 128 realCodecs := Codecs 129 130 // Replace the context with the fake context 131 known = fakeKnown 132 Scheme = kubeadmscheme.Scheme 133 Codecs = kubeadmscheme.Codecs 134 135 // Upon function exit, restore the real values 136 defer func() { 137 known = realKnown 138 Scheme = realScheme 139 Codecs = realCodecs 140 }() 141 142 // Call f in the fake context 143 f() 144 } 145 146 // testClusterConfigMap is a short hand for creating and possibly signing a test config map. 147 // This produces config maps that can be loaded by clusterConfigFromCluster 148 func testClusterConfigMap(yaml string, signIt bool) *v1.ConfigMap { 149 cm := &v1.ConfigMap{ 150 ObjectMeta: metav1.ObjectMeta{ 151 Name: constants.KubeadmConfigConfigMap, 152 Namespace: metav1.NamespaceSystem, 153 }, 154 Data: map[string]string{ 155 constants.ClusterConfigurationConfigMapKey: dedent.Dedent(yaml), 156 }, 157 } 158 159 if signIt { 160 SignConfigMap(cm) 161 } 162 163 return cm 164 } 165 166 // oldClusterConfigVersion is used as an old unsupported version in tests throughout this file 167 const oldClusterConfigVersion = "v1alpha1" 168 169 var ( 170 // currentClusterConfigVersion represents the current actively supported version of ClusterConfiguration 171 currentClusterConfigVersion = kubeadmapiv1.SchemeGroupVersion.Version 172 173 // currentFooClusterConfig is a minimal currently supported ClusterConfiguration 174 // with a well known value of clusterName (in this case `foo`) 175 currentFooClusterConfig = fmt.Sprintf(` 176 apiVersion: %s 177 kind: ClusterConfiguration 178 clusterName: foo 179 `, kubeadmapiv1.SchemeGroupVersion) 180 181 // oldFooClusterConfig is a minimal unsupported ClusterConfiguration 182 // with a well known value of clusterName (in this case `foo`) 183 oldFooClusterConfig = fmt.Sprintf(` 184 apiVersion: %s/%s 185 kind: ClusterConfiguration 186 clusterName: foo 187 `, kubeadmapiv1.GroupName, oldClusterConfigVersion) 188 189 // This is the "minimal" valid config that can be unmarshalled to and from YAML. 190 // Due to same static defaulting it's not exactly small in size. 191 validUnmarshallableClusterConfig = struct { 192 yaml string 193 obj kubeadmapiv1.ClusterConfiguration 194 }{ 195 yaml: dedent.Dedent(fmt.Sprintf(` 196 apiServer: 197 timeoutForControlPlane: 4m 198 apiVersion: %s 199 certificatesDir: /etc/kubernetes/pki 200 clusterName: LeCluster 201 controllerManager: {} 202 etcd: 203 local: 204 dataDir: /var/lib/etcd 205 imageRepository: registry.k8s.io 206 kind: ClusterConfiguration 207 kubernetesVersion: 1.2.3 208 networking: 209 dnsDomain: cluster.local 210 serviceSubnet: 10.96.0.0/12 211 scheduler: {} 212 `, kubeadmapiv1.SchemeGroupVersion.String())), 213 obj: kubeadmapiv1.ClusterConfiguration{ 214 TypeMeta: metav1.TypeMeta{ 215 APIVersion: kubeadmapiv1.SchemeGroupVersion.String(), 216 Kind: "ClusterConfiguration", 217 }, 218 ClusterName: "LeCluster", 219 KubernetesVersion: "1.2.3", 220 CertificatesDir: "/etc/kubernetes/pki", 221 ImageRepository: "registry.k8s.io", 222 Networking: kubeadmapiv1.Networking{ 223 DNSDomain: "cluster.local", 224 ServiceSubnet: "10.96.0.0/12", 225 }, 226 Etcd: kubeadmapiv1.Etcd{ 227 Local: &kubeadmapiv1.LocalEtcd{ 228 DataDir: "/var/lib/etcd", 229 }, 230 }, 231 APIServer: kubeadmapiv1.APIServer{ 232 TimeoutForControlPlane: &metav1.Duration{ 233 Duration: 4 * time.Minute, 234 }, 235 }, 236 }, 237 } 238 ) 239 240 func TestConfigBaseMarshal(t *testing.T) { 241 fakeKnownContext(func() { 242 cfg := &clusterConfig{ 243 configBase: configBase{ 244 GroupVersion: kubeadmapiv1.SchemeGroupVersion, 245 }, 246 config: kubeadmapiv1.ClusterConfiguration{ 247 TypeMeta: metav1.TypeMeta{ 248 APIVersion: kubeadmapiv1.SchemeGroupVersion.String(), 249 Kind: "ClusterConfiguration", 250 }, 251 ClusterName: "LeCluster", 252 KubernetesVersion: "1.2.3", 253 }, 254 } 255 256 b, err := cfg.Marshal() 257 if err != nil { 258 t.Fatalf("Marshal failed: %v", err) 259 } 260 261 got := strings.TrimSpace(string(b)) 262 expected := strings.TrimSpace(dedent.Dedent(fmt.Sprintf(` 263 apiServer: {} 264 apiVersion: %s 265 clusterName: LeCluster 266 controllerManager: {} 267 dns: {} 268 etcd: {} 269 kind: ClusterConfiguration 270 kubernetesVersion: 1.2.3 271 networking: {} 272 scheduler: {} 273 `, kubeadmapiv1.SchemeGroupVersion.String()))) 274 275 if expected != got { 276 t.Fatalf("Missmatch between expected and got:\nExpected:\n%s\n---\nGot:\n%s", expected, got) 277 } 278 }) 279 } 280 281 func TestConfigBaseUnmarshal(t *testing.T) { 282 fakeKnownContext(func() { 283 expected := &clusterConfig{ 284 configBase: configBase{ 285 GroupVersion: kubeadmapiv1.SchemeGroupVersion, 286 }, 287 config: validUnmarshallableClusterConfig.obj, 288 } 289 290 gvkmap, err := kubeadmutil.SplitYAMLDocuments([]byte(validUnmarshallableClusterConfig.yaml)) 291 if err != nil { 292 t.Fatalf("unexpected failure of SplitYAMLDocuments: %v", err) 293 } 294 295 got := &clusterConfig{ 296 configBase: configBase{ 297 GroupVersion: kubeadmapiv1.SchemeGroupVersion, 298 }, 299 } 300 if err = got.Unmarshal(gvkmap); err != nil { 301 t.Fatalf("unexpected failure of Unmarshal: %v", err) 302 } 303 304 if !reflect.DeepEqual(got, expected) { 305 t.Fatalf("Missmatch between expected and got:\nExpected:\n%v\n---\nGot:\n%v", expected, got) 306 } 307 }) 308 } 309 310 func TestGeneratedConfigFromCluster(t *testing.T) { 311 fakeKnownContext(func() { 312 testYAML := dedent.Dedent(fmt.Sprintf(` 313 apiVersion: %s 314 kind: ClusterConfiguration 315 `, kubeadmapiv1.SchemeGroupVersion.String())) 316 testYAMLHash := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(testYAML))) 317 // The SHA256 sum of "The quick brown fox jumps over the lazy dog" 318 const mismatchHash = "sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592" 319 tests := []struct { 320 name string 321 hash string 322 userSupplied bool 323 }{ 324 { 325 name: "Matching hash means generated config", 326 hash: testYAMLHash, 327 }, 328 { 329 name: "Missmatching hash means user supplied config", 330 hash: mismatchHash, 331 userSupplied: true, 332 }, 333 { 334 name: "No hash means user supplied config", 335 userSupplied: true, 336 }, 337 } 338 for _, test := range tests { 339 t.Run(test.name, func(t *testing.T) { 340 configMap := testClusterConfigMap(testYAML, false) 341 if test.hash != "" { 342 configMap.Annotations = map[string]string{ 343 constants.ComponentConfigHashAnnotationKey: test.hash, 344 } 345 } 346 347 client := clientsetfake.NewSimpleClientset(configMap) 348 cfg, err := clusterConfigHandler.FromCluster(client, testClusterCfg()) 349 if err != nil { 350 t.Fatalf("unexpected failure of FromCluster: %v", err) 351 } 352 353 got := cfg.IsUserSupplied() 354 if got != test.userSupplied { 355 t.Fatalf("mismatch between expected and got:\n\tExpected: %t\n\tGot: %t", test.userSupplied, got) 356 } 357 }) 358 } 359 }) 360 } 361 362 // runClusterConfigFromTest holds common test case data and evaluation code for handler.From* functions 363 func runClusterConfigFromTest(t *testing.T, perform func(t *testing.T, in string) (kubeadmapi.ComponentConfig, error)) { 364 fakeKnownContext(func() { 365 tests := []struct { 366 name string 367 in string 368 out *clusterConfig 369 expectErr bool 370 }{ 371 { 372 name: "Empty document map should return nothing successfully", 373 }, 374 { 375 name: "Non-empty document map without the proper API group returns nothing successfully", 376 in: dedent.Dedent(` 377 apiVersion: api.example.com/v1 378 kind: Configuration 379 `), 380 }, 381 { 382 name: "Old config version returns an error", 383 in: dedent.Dedent(` 384 apiVersion: kubeadm.k8s.io/v1alpha1 385 kind: ClusterConfiguration 386 `), 387 expectErr: true, 388 }, 389 { 390 name: "Unknown kind returns an error", 391 in: dedent.Dedent(fmt.Sprintf(` 392 apiVersion: %s 393 kind: Configuration 394 `, kubeadmapiv1.SchemeGroupVersion.String())), 395 expectErr: true, 396 }, 397 { 398 name: "Valid config gets loaded", 399 in: validUnmarshallableClusterConfig.yaml, 400 out: &clusterConfig{ 401 configBase: configBase{ 402 GroupVersion: clusterConfigHandler.GroupVersion, 403 userSupplied: true, 404 }, 405 config: validUnmarshallableClusterConfig.obj, 406 }, 407 }, 408 { 409 name: "Valid config gets loaded even if coupled with an extra document", 410 in: "apiVersion: api.example.com/v1\nkind: Configuration\n---\n" + validUnmarshallableClusterConfig.yaml, 411 out: &clusterConfig{ 412 configBase: configBase{ 413 GroupVersion: clusterConfigHandler.GroupVersion, 414 userSupplied: true, 415 }, 416 config: validUnmarshallableClusterConfig.obj, 417 }, 418 }, 419 } 420 421 for _, test := range tests { 422 t.Run(test.name, func(t *testing.T) { 423 componentCfg, err := perform(t, test.in) 424 if err != nil { 425 if !test.expectErr { 426 t.Errorf("unexpected failure: %v", err) 427 } 428 } else { 429 if test.expectErr { 430 t.Error("unexpected success") 431 } else { 432 if componentCfg == nil { 433 if test.out != nil { 434 t.Error("unexpected nil result") 435 } 436 } else { 437 if got, ok := componentCfg.(*clusterConfig); !ok { 438 t.Error("different result type") 439 } else { 440 if test.out == nil { 441 t.Errorf("unexpected result: %v", got) 442 } else { 443 if !reflect.DeepEqual(test.out, got) { 444 t.Errorf("mismatch between expected and got:\nExpected:\n%v\n---\nGot:\n%v", test.out, got) 445 } 446 } 447 } 448 } 449 } 450 } 451 }) 452 } 453 }) 454 } 455 456 func TestLoadingFromDocumentMap(t *testing.T) { 457 runClusterConfigFromTest(t, func(t *testing.T, in string) (kubeadmapi.ComponentConfig, error) { 458 gvkmap, err := kubeadmutil.SplitYAMLDocuments([]byte(in)) 459 if err != nil { 460 t.Fatalf("unexpected failure of SplitYAMLDocuments: %v", err) 461 } 462 463 return clusterConfigHandler.FromDocumentMap(gvkmap) 464 }) 465 } 466 467 func TestLoadingFromCluster(t *testing.T) { 468 runClusterConfigFromTest(t, func(t *testing.T, in string) (kubeadmapi.ComponentConfig, error) { 469 client := clientsetfake.NewSimpleClientset( 470 testClusterConfigMap(in, false), 471 ) 472 473 return clusterConfigHandler.FromCluster(client, testClusterCfg()) 474 }) 475 } 476 477 func TestGetVersionStates(t *testing.T) { 478 fakeKnownContext(func() { 479 versionStateCurrent := outputapiv1alpha3.ComponentConfigVersionState{ 480 Group: kubeadmapiv1.GroupName, 481 CurrentVersion: currentClusterConfigVersion, 482 PreferredVersion: currentClusterConfigVersion, 483 } 484 485 cases := []struct { 486 desc string 487 obj runtime.Object 488 expectedErr bool 489 expected outputapiv1alpha3.ComponentConfigVersionState 490 }{ 491 { 492 desc: "appropriate cluster object", 493 obj: testClusterConfigMap(currentFooClusterConfig, false), 494 expected: versionStateCurrent, 495 }, 496 { 497 desc: "old config returns an error", 498 obj: testClusterConfigMap(oldFooClusterConfig, false), 499 expectedErr: true, 500 }, 501 { 502 desc: "appropriate signed cluster object", 503 obj: testClusterConfigMap(currentFooClusterConfig, true), 504 expected: versionStateCurrent, 505 }, 506 { 507 desc: "old signed config", 508 obj: testClusterConfigMap(oldFooClusterConfig, true), 509 expected: outputapiv1alpha3.ComponentConfigVersionState{ 510 Group: kubeadmapiv1.GroupName, 511 CurrentVersion: "", // The config is treated as if it's missing 512 PreferredVersion: currentClusterConfigVersion, 513 }, 514 }, 515 } 516 517 for _, test := range cases { 518 t.Run(test.desc, func(t *testing.T) { 519 client := clientsetfake.NewSimpleClientset(test.obj) 520 521 clusterCfg := testClusterCfg() 522 523 got, err := GetVersionStates(clusterCfg, client) 524 if err != nil && !test.expectedErr { 525 t.Errorf("unexpected error: %v", err) 526 } 527 if err == nil { 528 if test.expectedErr { 529 t.Errorf("expected error not found: %v", test.expectedErr) 530 } 531 if len(got) != 1 { 532 t.Errorf("got %d, but expected only a single result: %v", len(got), got) 533 } else if got[0] != test.expected { 534 t.Errorf("unexpected result:\n\texpected: %v\n\tgot: %v", test.expected, got[0]) 535 } 536 } 537 }) 538 } 539 }) 540 }