k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/discovery/token/token_test.go (about) 1 /* 2 Copyright 2017 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 token 18 19 import ( 20 "testing" 21 "time" 22 23 "github.com/pmezard/go-difflib/difflib" 24 25 v1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 clientset "k8s.io/client-go/kubernetes" 28 fakeclient "k8s.io/client-go/kubernetes/fake" 29 "k8s.io/client-go/tools/clientcmd" 30 bootstrapapi "k8s.io/cluster-bootstrap/token/api" 31 tokenjws "k8s.io/cluster-bootstrap/token/jws" 32 33 kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" 34 "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" 35 ) 36 37 func TestRetrieveValidatedConfigInfo(t *testing.T) { 38 const ( 39 caCert = `-----BEGIN CERTIFICATE----- 40 MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl 41 cm5ldGVzMB4XDTE5MTEyMDAwNDk0MloXDTI5MTExNzAwNDk0MlowFTETMBEGA1UE 42 AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqQ 43 ctECzA8yFSuVYupOUYgrTmfQeKe/9BaDWagaq7ow9+I2IvsfWFvlrD8QQr8sea6q 44 xjq7TV67Vb4RxBaoYDA+yI5vIcujWUxULun64lu3Q6iC1sj2UnmUpIdgazRXXEkZ 45 vxA6EbAnoxA0+lBOn1CZWl23IQ4s70o2hZ7wIp/vevB88RRRjqtvgc5elsjsbmDF 46 LS7L1Zuye8c6gS93bR+VjVmSIfr1IEq0748tIIyXjAVCWPVCvuP41MlfPc/JVpZD 47 uD2+pO6ZYREcdAnOf2eD4/eLOMKko4L1dSFy9JKM5PLnOC0Zk0AYOd1vS8DTAfxj 48 XPEIY8OBYFhlsxf4TE8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB 49 /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH/OYq8zyl1+zSTmuow3yI/15PL1 50 dl8hB7IKnZNWmC/LTdm/+noh3Sb1IdRv6HkKg/GUn0UMuRUngLhju3EO4ozJPQcX 51 quaxzgmTKNWJ6ErDvRvWhGX0ZcbdBfZv+dowyRqzd5nlJ49hC+NrtFFQq6P05BYn 52 7SemguqeXmXwIj2Sa+1DeR6lRm9o8shAYjnyThUFqaMn18kI3SANJ5vk/3DFrPEO 53 CKC9EzFku2kuxg2dM12PbRGZQ2o0K6HEZgrrIKTPOy3ocb8r9M0aSFhjOV/NqGA4 54 SaupXSW6XfvIi/UHoIbU3pNcsnUJGnQfQvip95XKk/gqcUr+m50vxgumxtA= 55 -----END CERTIFICATE-----` 56 57 caCertHash = "sha256:98be2e6d4d8a89aa308fb15de0c07e2531ce549c68dec1687cdd5c06f0826658" 58 59 expectedKubeconfig = `apiVersion: v1 60 clusters: 61 - cluster: 62 certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1URXlNREF3TkRrME1sb1hEVEk1TVRFeE56QXdORGswTWxvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTXFRCmN0RUN6QTh5RlN1Vll1cE9VWWdyVG1mUWVLZS85QmFEV2FnYXE3b3c5K0kySXZzZldGdmxyRDhRUXI4c2VhNnEKeGpxN1RWNjdWYjRSeEJhb1lEQSt5STV2SWN1aldVeFVMdW42NGx1M1E2aUMxc2oyVW5tVXBJZGdhelJYWEVrWgp2eEE2RWJBbm94QTArbEJPbjFDWldsMjNJUTRzNzBvMmhaN3dJcC92ZXZCODhSUlJqcXR2Z2M1ZWxzanNibURGCkxTN0wxWnV5ZThjNmdTOTNiUitWalZtU0lmcjFJRXEwNzQ4dElJeVhqQVZDV1BWQ3Z1UDQxTWxmUGMvSlZwWkQKdUQyK3BPNlpZUkVjZEFuT2YyZUQ0L2VMT01La280TDFkU0Z5OUpLTTVQTG5PQzBaazBBWU9kMXZTOERUQWZ4agpYUEVJWThPQllGaGxzeGY0VEU4Q0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFIL09ZcTh6eWwxK3pTVG11b3czeUkvMTVQTDEKZGw4aEI3SUtuWk5XbUMvTFRkbS8rbm9oM1NiMUlkUnY2SGtLZy9HVW4wVU11UlVuZ0xoanUzRU80b3pKUFFjWApxdWF4emdtVEtOV0o2RXJEdlJ2V2hHWDBaY2JkQmZaditkb3d5UnF6ZDVubEo0OWhDK05ydEZGUXE2UDA1QlluCjdTZW1ndXFlWG1Yd0lqMlNhKzFEZVI2bFJtOW84c2hBWWpueVRoVUZxYU1uMThrSTNTQU5KNXZrLzNERnJQRU8KQ0tDOUV6Rmt1Mmt1eGcyZE0xMlBiUkdaUTJvMEs2SEVaZ3JySUtUUE95M29jYjhyOU0wYVNGaGpPVi9OcUdBNApTYXVwWFNXNlhmdklpL1VIb0liVTNwTmNzblVKR25RZlF2aXA5NVhLay9ncWNVcittNTB2eGd1bXh0QT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== 63 server: https://127.0.0.1 64 name: somecluster 65 contexts: 66 - context: 67 cluster: somecluster 68 user: token-bootstrap-client 69 name: token-bootstrap-client@somecluster 70 current-context: token-bootstrap-client@somecluster 71 kind: Config 72 preferences: {} 73 users: null 74 ` 75 ) 76 77 tests := []struct { 78 name string 79 tokenID string 80 tokenSecret string 81 cfg *kubeadmapi.Discovery 82 configMap *fakeConfigMap 83 delayedJWSSignaturePatch bool 84 expectedError bool 85 }{ 86 { 87 // This is the default behavior. The JWS signature is patched after the cluster-info ConfigMap is created 88 name: "valid: retrieve a valid kubeconfig with CA verification and delayed JWS signature", 89 tokenID: "123456", 90 tokenSecret: "abcdef1234567890", 91 cfg: &kubeadmapi.Discovery{ 92 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 93 Token: "123456.abcdef1234567890", 94 CACertHashes: []string{caCertHash}, 95 }, 96 }, 97 configMap: &fakeConfigMap{ 98 name: bootstrapapi.ConfigMapClusterInfo, 99 data: map[string]string{}, 100 }, 101 delayedJWSSignaturePatch: true, 102 }, 103 { 104 // Same as above expect this test creates the ConfigMap with the JWS signature 105 name: "valid: retrieve a valid kubeconfig with CA verification", 106 tokenID: "123456", 107 tokenSecret: "abcdef1234567890", 108 cfg: &kubeadmapi.Discovery{ 109 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 110 Token: "123456.abcdef1234567890", 111 CACertHashes: []string{caCertHash}, 112 }, 113 }, 114 configMap: &fakeConfigMap{ 115 name: bootstrapapi.ConfigMapClusterInfo, 116 data: nil, 117 }, 118 }, 119 { 120 // Skipping CA verification is also supported 121 name: "valid: retrieve a valid kubeconfig without CA verification", 122 tokenID: "123456", 123 tokenSecret: "abcdef1234567890", 124 cfg: &kubeadmapi.Discovery{ 125 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 126 Token: "123456.abcdef1234567890", 127 }, 128 }, 129 configMap: &fakeConfigMap{ 130 name: bootstrapapi.ConfigMapClusterInfo, 131 data: nil, 132 }, 133 }, 134 { 135 name: "invalid: token format is invalid", 136 tokenID: "foo", 137 tokenSecret: "bar", 138 cfg: &kubeadmapi.Discovery{ 139 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 140 Token: "foo.bar", 141 }, 142 }, 143 configMap: &fakeConfigMap{ 144 name: bootstrapapi.ConfigMapClusterInfo, 145 data: nil, 146 }, 147 expectedError: true, 148 }, 149 { 150 name: "invalid: missing cluster-info ConfigMap", 151 tokenID: "123456", 152 tokenSecret: "abcdef1234567890", 153 cfg: &kubeadmapi.Discovery{ 154 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 155 Token: "123456.abcdef1234567890", 156 }, 157 }, 158 configMap: &fakeConfigMap{ 159 name: "baz", 160 data: nil, 161 }, 162 expectedError: true, 163 }, 164 { 165 name: "invalid: wrong JWS signature", 166 tokenID: "123456", 167 tokenSecret: "abcdef1234567890", 168 cfg: &kubeadmapi.Discovery{ 169 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 170 Token: "123456.abcdef1234567890", 171 }, 172 }, 173 configMap: &fakeConfigMap{ 174 name: bootstrapapi.ConfigMapClusterInfo, 175 data: map[string]string{ 176 bootstrapapi.KubeConfigKey: "foo", 177 bootstrapapi.JWSSignatureKeyPrefix + "123456": "bar", 178 }, 179 }, 180 expectedError: true, 181 }, 182 { 183 name: "invalid: missing key for JWSSignatureKeyPrefix", 184 tokenID: "123456", 185 tokenSecret: "abcdef1234567890", 186 cfg: &kubeadmapi.Discovery{ 187 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 188 Token: "123456.abcdef1234567890", 189 }, 190 }, 191 configMap: &fakeConfigMap{ 192 name: bootstrapapi.ConfigMapClusterInfo, 193 data: map[string]string{ 194 bootstrapapi.KubeConfigKey: "foo", 195 }, 196 }, 197 expectedError: true, 198 }, 199 { 200 name: "invalid: wrong CA cert hash", 201 tokenID: "123456", 202 tokenSecret: "abcdef1234567890", 203 cfg: &kubeadmapi.Discovery{ 204 BootstrapToken: &kubeadmapi.BootstrapTokenDiscovery{ 205 Token: "123456.abcdef1234567890", 206 CACertHashes: []string{"foo"}, 207 }, 208 }, 209 configMap: &fakeConfigMap{ 210 name: bootstrapapi.ConfigMapClusterInfo, 211 data: nil, 212 }, 213 expectedError: true, 214 }, 215 } 216 217 for _, test := range tests { 218 t.Run(test.name, func(t *testing.T) { 219 kubeconfig := buildSecureBootstrapKubeConfig("127.0.0.1", []byte(caCert), "somecluster") 220 kubeconfigBytes, err := clientcmd.Write(*kubeconfig) 221 if err != nil { 222 t.Fatalf("cannot marshal kubeconfig %v", err) 223 } 224 225 // Generate signature of the insecure kubeconfig 226 sig, err := tokenjws.ComputeDetachedSignature(string(kubeconfigBytes), test.tokenID, test.tokenSecret) 227 if err != nil { 228 t.Fatalf("cannot compute detached JWS signature: %v", err) 229 } 230 231 // If the JWS signature is delayed, only add the kubeconfig 232 if test.delayedJWSSignaturePatch { 233 test.configMap.data = map[string]string{} 234 test.configMap.data[bootstrapapi.KubeConfigKey] = string(kubeconfigBytes) 235 } 236 237 // Populate the default cluster-info data 238 if test.configMap.data == nil { 239 test.configMap.data = map[string]string{} 240 test.configMap.data[bootstrapapi.KubeConfigKey] = string(kubeconfigBytes) 241 test.configMap.data[bootstrapapi.JWSSignatureKeyPrefix+test.tokenID] = sig 242 } 243 244 // Create a fake client and create the cluster-info ConfigMap 245 client := fakeclient.NewSimpleClientset() 246 if err = test.configMap.createOrUpdate(client); err != nil { 247 t.Fatalf("could not create ConfigMap: %v", err) 248 } 249 250 // Set arbitrary discovery timeout and retry interval 251 timeout := time.Millisecond * 500 252 interval := time.Millisecond * 20 253 254 // Patch the JWS signature after a short delay 255 if test.delayedJWSSignaturePatch { 256 test.configMap.data[bootstrapapi.JWSSignatureKeyPrefix+test.tokenID] = sig 257 go func() { 258 time.Sleep(time.Millisecond * 60) 259 if err := test.configMap.createOrUpdate(client); err != nil { 260 t.Errorf("could not update the cluster-info ConfigMap with a JWS signature: %v", err) 261 } 262 }() 263 } 264 265 // Retrieve validated configuration 266 kubeconfig, err = retrieveValidatedConfigInfo(client, test.cfg, interval, timeout) 267 if (err != nil) != test.expectedError { 268 t.Errorf("expected error %v, got %v, error: %v", test.expectedError, err != nil, err) 269 } 270 271 // Return if an error is expected 272 if test.expectedError { 273 return 274 } 275 276 // Validate the resulted kubeconfig 277 kubeconfigBytes, err = clientcmd.Write(*kubeconfig) 278 if err != nil { 279 t.Fatalf("cannot marshal resulted kubeconfig %v", err) 280 } 281 if string(kubeconfigBytes) != expectedKubeconfig { 282 t.Error("unexpected kubeconfig") 283 diff := difflib.UnifiedDiff{ 284 A: difflib.SplitLines(expectedKubeconfig), 285 B: difflib.SplitLines(string(kubeconfigBytes)), 286 FromFile: "expected", 287 ToFile: "got", 288 Context: 10, 289 } 290 diffstr, err := difflib.GetUnifiedDiffString(diff) 291 if err != nil { 292 t.Fatalf("error generating unified diff string: %v", err) 293 } 294 t.Errorf("\n%s", diffstr) 295 } 296 }) 297 } 298 } 299 300 type fakeConfigMap struct { 301 name string 302 data map[string]string 303 } 304 305 func (c *fakeConfigMap) createOrUpdate(client clientset.Interface) error { 306 return apiclient.CreateOrUpdateConfigMap(client, &v1.ConfigMap{ 307 ObjectMeta: metav1.ObjectMeta{ 308 Name: c.name, 309 Namespace: metav1.NamespacePublic, 310 }, 311 Data: c.data, 312 }) 313 }