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  }