github.com/kyma-project/kyma-environment-broker@v0.0.1/testing/e2e/skr/utils/index.js (about) 1 const k8s = require('@kubernetes/client-node'); 2 const {expect} = require('chai'); 3 4 const kc = new k8s.KubeConfig(); 5 let k8sDynamicApi; 6 let k8sAppsApi; 7 let k8sCoreV1Api; 8 let k8sRbacAuthorizationV1Api; 9 10 let watch; 11 12 function initializeK8sClient(opts) { 13 opts = opts || {}; 14 try { 15 console.log('Trying to initialize a K8S client'); 16 if (opts.kubeconfigPath) { 17 console.log('Path initialization'); 18 kc.loadFromFile(opts.kubeconfigPath); 19 } else if (opts.kubeconfig) { 20 console.log('Kubeconfig initialization'); 21 kc.loadFromString(opts.kubeconfig); 22 } else { 23 console.log('Default initialization'); 24 kc.loadFromDefault(); 25 } 26 27 console.log('Clients creation'); 28 k8sDynamicApi = kc.makeApiClient(k8s.KubernetesObjectApi); 29 console.log('Making Api client - Apps'); 30 k8sAppsApi = kc.makeApiClient(k8s.AppsV1Api); 31 console.log('Making Api client - Core'); 32 k8sCoreV1Api = kc.makeApiClient(k8s.CoreV1Api); 33 console.log('Making Api client - Auth'); 34 k8sRbacAuthorizationV1Api = kc.makeApiClient(k8s.RbacAuthorizationV1Api); 35 console.log('Making Api client - Logs'); 36 k8sLog = new k8s.Log(kc); 37 console.log('Making Api client - Watch'); 38 watch = new k8s.Watch(kc); 39 k8sServerUrl = kc.getCurrentCluster() ? kc.getCurrentCluster().server : null; 40 } catch (err) { 41 console.log(err.message); 42 } 43 } 44 initializeK8sClient(); 45 46 function sleep(ms) { 47 return new Promise((resolve) => setTimeout(resolve, ms)); 48 } 49 50 async function k8sDelete(listOfSpecs, namespace) { 51 for (const res of listOfSpecs) { 52 if (namespace) { 53 res.metadata.namespace = namespace; 54 } 55 debug(`Delete ${res.metadata.name}`); 56 try { 57 if (res.kind) { 58 await k8sDynamicApi.delete(res); 59 } else if (res.metadata.selfLink) { 60 await k8sDynamicApi.requestPromise({ 61 url: k8sDynamicApi.basePath + res.metadata.selfLink, 62 method: 'DELETE', 63 }); 64 } else { 65 throw Error( 66 'Object kind or metadata.selfLink is required to delete the resource', 67 ); 68 } 69 if (res.kind === 'CustomResourceDefinition') { 70 const version = res.spec.version || res.spec.versions[0].name; 71 const path = `/apis/${res.spec.group}/${version}/${res.spec.names.plural}`; 72 await deleteAllK8sResources(path); 73 } 74 } catch (err) { 75 ignore404(err); 76 } 77 } 78 } 79 80 async function getSecret(name, namespace) { 81 const path = `/api/v1/namespaces/${namespace}/secrets/${name}`; 82 const response = await k8sDynamicApi.requestPromise({ 83 url: k8sDynamicApi.basePath + path, 84 }); 85 return JSON.parse(response.body); 86 } 87 88 async function k8sApply(resources, namespace, patch = true) { 89 const options = { 90 headers: {'Content-type': 'application/merge-patch+json'}, 91 }; 92 for (const resource of resources) { 93 if (!resource || !resource.kind || !resource.metadata.name) { 94 debug('Skipping invalid resource:', resource); 95 continue; 96 } 97 if (!resource.metadata.namespace) { 98 resource.metadata.namespace = namespace; 99 } 100 if (resource.kind == 'Namespace') { 101 resource.metadata.labels = { 102 'istio-injection': 'enabled', 103 }; 104 } 105 try { 106 await k8sDynamicApi.patch( 107 resource, 108 undefined, 109 undefined, 110 undefined, 111 undefined, 112 options, 113 ); 114 debug(resource.kind, resource.metadata.name, 'reconfigured'); 115 } catch (e) { 116 { 117 if (e.body && e.body.reason === 'NotFound') { 118 try { 119 await k8sDynamicApi.create(resource); 120 debug(resource.kind, resource.metadata.name, 'created'); 121 } catch (createError) { 122 debug(resource.kind, resource.metadata.name, 'failed to create'); 123 debug(JSON.stringify(createError, null, 4)); 124 throw createError; 125 } 126 } else { 127 throw e; 128 } 129 } 130 } 131 } 132 } 133 134 // Allows to pass watch with different than global K8S context. 135 function waitForK8sObject(path, query, checkFn, timeout, timeoutMsg, watcher = watch) { 136 debug('waiting for', path); 137 let res; 138 let timer; 139 return new Promise((resolve, reject) => { 140 watcher.watch( 141 path, 142 query, 143 (type, apiObj, watchObj) => { 144 if (checkFn(type, apiObj, watchObj)) { 145 if (res) { 146 res.abort(); 147 } 148 clearTimeout(timer); 149 debug('finished waiting for ', path); 150 resolve(watchObj.object); 151 } 152 }, 153 () => { 154 }, 155 ) 156 .then((r) => { 157 res = r; 158 timer = setTimeout(() => { 159 res.abort(); 160 reject(new Error(timeoutMsg)); 161 }, timeout); 162 }); 163 }); 164 } 165 166 function waitForSecret( 167 secretName, 168 namespace = 'default', 169 timeout = 90_000, 170 ) { 171 return waitForK8sObject( 172 `/api/v1/namespaces/${namespace}/secrets`, 173 {}, 174 (_type, _apiObj, watchObj) => { 175 return watchObj.object.metadata.name.includes( 176 secretName, 177 ); 178 }, 179 timeout, 180 `Waiting for ${secretName} Secret timeout (${timeout} ms)`, 181 ); 182 } 183 184 async function getSecretData(name, namespace) { 185 try { 186 const secret = await getSecret(name, namespace); 187 const encodedData = secret.data; 188 return Object.fromEntries( 189 Object.entries(encodedData).map(([key, value]) => { 190 const buff = Buffer.from(value, 'base64'); 191 const decoded = buff.toString('ascii'); 192 return [key, decoded]; 193 }), 194 ); 195 } catch (e) { 196 console.log('Error:', e); 197 throw e; 198 } 199 } 200 201 function ignore404(e) { 202 if ( 203 (e.statusCode && e.statusCode === 404) || 204 (e.response && e.response.statusCode && e.response.statusCode === 404) 205 ) { 206 debug('Warning: Ignoring NotFound error'); 207 return; 208 } 209 210 throw e; 211 } 212 213 // NOTE: this no longer works, it relies on kube-api sending `selfLink` but the field has been deprecated 214 async function deleteAllK8sResources( 215 path, 216 query = {}, 217 retries = 2, 218 interval = 1000, 219 keepFinalizer = false, 220 ) { 221 try { 222 let i = 0; 223 while (i < retries) { 224 if (i++) { 225 await sleep(interval); 226 } 227 const response = await k8sDynamicApi.requestPromise({ 228 url: k8sDynamicApi.basePath + path, 229 qs: query, 230 }); 231 const body = JSON.parse(response.body); 232 if (body.items && body.items.length) { 233 for (const o of body.items) { 234 await deleteK8sResource(o, path, keepFinalizer); 235 } 236 } else if (!body.items) { 237 await deleteK8sResource(body, path, keepFinalizer); 238 } 239 } 240 } catch (e) { 241 debug('Error during delete ', path, String(e).substring(0, 1000)); 242 debug(e); 243 } 244 } 245 246 async function deleteK8sResource(o, path, keepFinalizer = false) { 247 if (o.metadata.finalizers && o.metadata.finalizers.length && !keepFinalizer) { 248 const options = { 249 headers: {'Content-type': 'application/merge-patch+json'}, 250 }; 251 252 const obj = { 253 kind: o.kind || 'Secret', // Secret list doesn't return kind and apiVersion 254 apiVersion: o.apiVersion || 'v1', 255 metadata: { 256 name: o.metadata.name, 257 namespace: o.metadata.namespace, 258 finalizers: [], 259 }, 260 }; 261 262 debug('Removing finalizers from', obj); 263 try { 264 await k8sDynamicApi.patch(obj, undefined, undefined, undefined, undefined, options); 265 } catch (err) { 266 ignore404(err); 267 } 268 } 269 270 try { 271 let objectUrl = `${k8sDynamicApi.basePath + path}/${o.metadata.name}`; 272 if (o.metadata.selfLink) { 273 debug('using selfLink for deleting object'); 274 objectUrl = k8sDynamicApi.basePath + o.metadata.selfLink; 275 } 276 277 debug('Deleting resource: ', objectUrl); 278 await k8sDynamicApi.requestPromise({ 279 url: objectUrl, 280 method: 'DELETE', 281 }); 282 } catch (err) { 283 ignore404(err); 284 } 285 286 debug( 287 'Deleted resource:', 288 o.metadata.name, 289 'namespace:', 290 o.metadata.namespace, 291 ); 292 } 293 294 async function getKymaAdminBindings() { 295 const {body} = await k8sRbacAuthorizationV1Api.listClusterRoleBinding(); 296 const adminRoleBindings = body.items; 297 return adminRoleBindings 298 .filter( 299 (clusterRoleBinding) => clusterRoleBinding.roleRef.name === 'cluster-admin', 300 ) 301 .map((clusterRoleBinding) => ({ 302 name: clusterRoleBinding.metadata.name, 303 role: clusterRoleBinding.roleRef.name, 304 users: clusterRoleBinding.subjects 305 .filter((sub) => sub.kind === 'User') 306 .map((sub) => sub.name), 307 groups: clusterRoleBinding.subjects 308 .filter((sub) => sub.kind === 'Group') 309 .map((sub) => sub.name), 310 })); 311 } 312 313 async function findKymaAdminBindingForUser(targetUser) { 314 const kymaAdminBindings = await getKymaAdminBindings(); 315 return kymaAdminBindings.find( 316 (binding) => binding.users.indexOf(targetUser) >= 0, 317 ); 318 } 319 320 async function ensureKymaAdminBindingExistsForUser(targetUser) { 321 const binding = await findKymaAdminBindingForUser(targetUser); 322 expect(binding).not.to.be.undefined; 323 expect(binding.users).to.include(targetUser); 324 } 325 326 async function ensureKymaAdminBindingDoesNotExistsForUser(targetUser) { 327 const binding = await findKymaAdminBindingForUser(targetUser); 328 expect(binding).to.be.undefined; 329 } 330 331 let DEBUG = process.env.DEBUG === 'true'; 332 333 function log(prefix, ...args) { 334 if (args.length === 0) { 335 return; 336 } 337 338 args = [...args]; 339 const fmt = `[${prefix}] ` + args[0]; 340 args = args.slice(1); 341 console.log.apply(console, [fmt, ...args]); 342 } 343 344 function isDebugEnabled() { 345 return DEBUG; 346 } 347 348 function switchDebug(on = true) { 349 DEBUG = on; 350 } 351 352 function debug(...args) { 353 if (!isDebugEnabled()) { 354 return; 355 } 356 log('DEBUG', ...args); 357 } 358 359 function info(...args) { 360 log('INFO', ...args); 361 } 362 363 function error(...args) { 364 log('ERROR', ...args); 365 } 366 367 function fromBase64(s) { 368 return Buffer.from(s, 'base64').toString('utf8'); 369 } 370 371 function toBase64(s) { 372 return Buffer.from(s).toString('base64'); 373 } 374 375 function genRandom(len) { 376 let res = ''; 377 const chrs = 'abcdefghijklmnopqrstuvwxyz0123456789'; 378 for (let i = 0; i < len; i++) { 379 res += chrs.charAt(Math.floor(Math.random() * chrs.length)); 380 } 381 382 return res; 383 } 384 385 function getEnvOrThrow(key) { 386 if (!process.env[key]) { 387 throw new Error(`Env ${key} not present`); 388 } 389 390 return process.env[key]; 391 } 392 393 function wait(fn, checkFn, timeout, interval) { 394 return new Promise((resolve, reject) => { 395 const th = setTimeout(function() { 396 debug('wait timeout'); 397 done(reject, new Error('wait timeout')); 398 }, timeout); 399 const ih = setInterval(async function() { 400 let res; 401 try { 402 res = await fn(); 403 } catch (ex) { 404 res = ex; 405 } 406 checkFn(res) && done(resolve, res); 407 }, interval); 408 409 function done(fn, arg) { 410 clearTimeout(th); 411 clearInterval(ih); 412 fn(arg); 413 } 414 }); 415 } 416 417 module.exports = { 418 initializeK8sClient, 419 k8sApply, 420 k8sDelete, 421 waitForK8sObject, 422 waitForSecret, 423 ensureKymaAdminBindingExistsForUser, 424 ensureKymaAdminBindingDoesNotExistsForUser, 425 getSecret, 426 getSecretData, 427 k8sDynamicApi, 428 k8sAppsApi, 429 k8sCoreV1Api, 430 info, 431 error, 432 debug, 433 switchDebug, 434 isDebugEnabled, 435 fromBase64, 436 toBase64, 437 genRandom, 438 getEnvOrThrow, 439 wait, 440 };