github.com/kyma-project/kyma-environment-broker@v0.0.1/testing/e2e/skr/kcp/client.js (about) 1 const execa = require('execa'); 2 const fs = require('fs'); 3 const stream = require('stream'); 4 const { 5 getEnvOrThrow, 6 debug, 7 wait, 8 } = require('../utils'); 9 const {inspect} = require('util'); 10 11 class KCPConfig { 12 static fromEnv() { 13 return new KCPConfig(); 14 } 15 constructor() { 16 this.host = getEnvOrThrow('KCP_KEB_API_URL'); 17 this.issuerURL = getEnvOrThrow('KCP_OIDC_ISSUER_URL'); 18 this.gardenerNamespace = getEnvOrThrow('KCP_GARDENER_NAMESPACE'); 19 this.username = getEnvOrThrow('KCP_TECH_USER_LOGIN'); 20 this.password = getEnvOrThrow('KCP_TECH_USER_PASSWORD'); 21 this.clientID = getEnvOrThrow('KCP_OIDC_CLIENT_ID'); 22 23 if (process.env.KCP_OIDC_CLIENT_SECRET) { 24 this.clientSecret = getEnvOrThrow('KCP_OIDC_CLIENT_SECRET'); 25 } else { 26 this.oauthClientID = getEnvOrThrow('KCP_OAUTH2_CLIENT_ID'); 27 this.oauthSecret = getEnvOrThrow('KCP_OAUTH2_CLIENT_SECRET'); 28 this.oauthIssuer = getEnvOrThrow('KCP_OAUTH2_ISSUER_URL'); 29 } 30 31 this.motherShipApiUrl = getEnvOrThrow('KCP_MOTHERSHIP_API_URL'); 32 this.kubeConfigApiUrl = getEnvOrThrow('KCP_KUBECONFIG_API_URL'); 33 } 34 } 35 36 class KCPWrapper { 37 constructor(config) { 38 this.kcpConfigPath = config.kcpConfigPath; 39 this.gardenerNamespace = config.gardenerNamespace; 40 this.clientID = config.clientID; 41 this.clientSecret = config.clientSecret; 42 this.oauthClientID = config.oauthClientID; 43 this.oauthSecret = config.oauthSecret; 44 this.oauthIssuer = config.oauthIssuer; 45 46 this.issuerURL = config.issuerURL; 47 this.motherShipApiUrl = config.motherShipApiUrl; 48 this.kubeConfigApiUrl = config.kubeConfigApiUrl; 49 50 this.username = config.username; 51 this.password = config.password; 52 this.host = config.host; 53 54 this.kcpConfigPath = 'config.yaml'; 55 const stream = fs.createWriteStream(`${this.kcpConfigPath}`); 56 stream.once('open', (_) => { 57 stream.write(`gardener-namespace: "${this.gardenerNamespace}"\n`); 58 if (process.env.KCP_OIDC_CLIENT_SECRET) { 59 stream.write(`oidc-client-id: "${this.clientID}"\n`); 60 stream.write(`oidc-client-secret: ${this.clientSecret}\n`); 61 stream.write(`username: ${this.username}\n`); 62 } else { 63 stream.write(`oauth2-client-id: "${this.oauthClientID}"\n`); 64 stream.write(`oauth2-client-secret: "${this.oauthSecret}"\n`); 65 stream.write(`oauth2-issuer-url: "${this.oauthIssuer}"\n`); 66 } 67 68 stream.write(`keb-api-url: "${this.host}"\n`); 69 stream.write(`oidc-issuer-url: "${this.issuerURL}"\n`); 70 stream.write(`mothership-api-url: "${this.motherShipApiUrl}"\n`); 71 stream.write(`kubeconfig-api-url: "${this.kubeConfigApiUrl}"\n`); 72 stream.end(); 73 }); 74 } 75 76 async runtimes(query) { 77 let args = ['runtimes', '--output', 'json']; 78 if (query.account) { 79 args = args.concat('--account', `${query.account}`); 80 } 81 if (query.subaccount) { 82 args = args.concat('--subaccount', `${query.subaccount}`); 83 } 84 if (query.instanceID) { 85 args = args.concat('--instance-id', `${query.instanceID}`); 86 } 87 if (query.runtimeID) { 88 args = args.concat('--runtime-id', `${query.runtimeID}`); 89 } 90 if (query.region) { 91 args = args.concat('--region', `${query.region}`); 92 } 93 if (query.shoot) { 94 args = args.concat('--shoot', `${query.shoot}`); 95 } 96 if (query.state) { 97 args = args.concat('--state', `${query.state}`); 98 } 99 if (query.ops) { 100 args = args.concat('--ops'); 101 } 102 const result = await this.exec(args); 103 return JSON.parse(result); 104 } 105 106 async reconciliations(query) { 107 let args = ['reconciliations', `${query.parameter}`, '--output', 'json']; 108 if (query.runtimeID) { 109 args = args.concat('--runtime-id', `${query.runtimeID}`); 110 } 111 if (query.schedulingID) { 112 args = args.concat('--scheduling-id', `${query.schedulingID}`); 113 } 114 const result = await this.exec(args); 115 return JSON.parse(result); 116 } 117 118 async login() { 119 let args; 120 if (process.env.KCP_OIDC_CLIENT_SECRET) { 121 args = ['login', '-u', `${this.username}`, '-p', `${this.password}`]; 122 } else { 123 args = ['login']; 124 } 125 126 return await this.exec(args); 127 } 128 129 async version() { 130 const args = ['--version']; 131 return await this.exec(args); 132 } 133 134 async upgradeKyma(instanceID, kymaUpgradeVersion, upgradeTimeoutMin = 30) { 135 const args = ['upgrade', 'kyma', `--version=${kymaUpgradeVersion}`, '--target', `instance-id=${instanceID}`]; 136 try { 137 console.log('Executing kcp upgrade'); 138 const res = await this.exec(args, true, true); 139 140 console.log('Checking orchestration'); 141 // output if successful: 142 // "Note: Ignore sending slack notification when slackAPIURL is empty\n" + 143 // "OrchestrationID: 22f19856-679b-4e68-b533-f1a0a46b1eed" 144 // so we need to extract the uuid 145 if (!res.includes('OrchestrationID: ')) { 146 throw new Error(`Kyma Upgrade failed. KCP upgrade command returned no OrchestrationID. Response: \"${res}\"`); 147 } 148 const orchestrationID = res.split('OrchestrationID: ')[1]; 149 debug(`OrchestrationID: ${orchestrationID}`); 150 151 try { 152 console.log('Ensure execution suceeded'); 153 const orchestrationStatus = await this.ensureOrchestrationSucceeded(orchestrationID, upgradeTimeoutMin); 154 return orchestrationStatus; 155 } catch (error) { 156 debug(error); 157 } 158 159 try { 160 console.log('Check runtime status'); 161 const runtime = await this.runtimes({instanceID: instanceID}); 162 debug(`Runtime Status: ${inspect(runtime, false, null, false)}`); 163 } catch (error) { 164 debug(error); 165 } 166 167 try { 168 console.log('Check orchestration'); 169 const orchestration = await this.getOrchestrationStatus(orchestrationID); 170 debug(`Orchestration Status: ${inspect(orchestration, false, null, false)}`); 171 } catch (error) { 172 debug(error); 173 } 174 175 try { 176 console.log('Check operations'); 177 const operations = await this.getOrchestrationsOperations(orchestrationID); 178 debug(`Operations: ${inspect(operations, false, null, false)}`); 179 } catch (error) { 180 debug(error); 181 } 182 183 throw new Error('Kyma Upgrade failed'); 184 } catch (error) { 185 debug(error); 186 throw new Error('failed during upgradeKyma'); 187 } 188 }; 189 190 async getReconciliationsOperations(runtimeID) { 191 await this.login(); 192 const reconciliationsOperations = await this.reconciliations({parameter: 'operations', 193 runtimeID: runtimeID}); 194 return JSON.stringify(reconciliationsOperations, null, '\t'); 195 } 196 197 async getReconciliationsInfo(schedulingID) { 198 await this.login(); 199 const reconciliationsInfo = await this.reconciliations({parameter: 'info', 200 schedulingID: schedulingID}); 201 202 return JSON.stringify(reconciliationsInfo, null, '\t'); 203 } 204 205 async getRuntimeEvents(instanceID) { 206 await this.login(); 207 return this.exec(['runtimes', '--instance-id', instanceID, '--events']); 208 } 209 210 async getRuntimeStatusOperations(instanceID) { 211 await this.login(); 212 const runtimeStatus = await this.runtimes({instanceID: instanceID, ops: true}); 213 214 return JSON.stringify(runtimeStatus, null, '\t'); 215 } 216 217 async getOrchestrationsOperations(orchestrationID) { 218 // debug('Running getOrchestrationsOperations...') 219 const args = ['orchestration', `${orchestrationID}`, 'operations', '-o', 'json']; 220 try { 221 const res = await this.exec(args); 222 const operations = JSON.parse(res); 223 // debug(`getOrchestrationsOperations output: ${operations}`) 224 225 return operations; 226 } catch (error) { 227 debug(error); 228 throw new Error('failed during getOrchestrationsOperations'); 229 } 230 } 231 232 async getOrchestrationsOperationStatus(orchestrationID, operationID) { 233 // debug('Running getOrchestrationsOperationStatus...') 234 const args = ['orchestration', `${orchestrationID}`, '--operation', `${operationID}`, '-o', 'json']; 235 try { 236 let res = await this.exec(args); 237 res = JSON.parse(res); 238 239 return res; 240 } catch (error) { 241 debug(error); 242 throw new Error('failed during getOrchestrationsOperationStatus'); 243 } 244 } 245 246 async getOrchestrationStatus(orchestrationID) { 247 // debug('Running getOrchestrationStatus...') 248 const args = ['orchestrations', `${orchestrationID}`, '-o', 'json']; 249 try { 250 const res = await this.exec(args); 251 const o = JSON.parse(res); 252 253 debug(`OrchestrationID: ${o.orchestrationID} (${o.type} to version ${o.parameters.kyma.version}) 254 Status: ${o.state}`); 255 256 const operations = await this.getOrchestrationsOperations(o.orchestrationID); 257 // debug(`Got ${operations.length} operations for OrchestrationID ${o.orchestrationID}`) 258 259 let upgradeOperation = {}; 260 if (operations.count > 0) { 261 upgradeOperation = await this.getOrchestrationsOperationStatus(orchestrationID, operations.data[0].operationID); 262 debug(`OrchestrationID: ${orchestrationID} 263 OperationID: ${operations.data[0].operationID} 264 OperationStatus: ${upgradeOperation[0].state}`); 265 } else { 266 debug(`No operations in OrchestrationID ${o.orchestrationID}`); 267 } 268 269 return o; 270 } catch (error) { 271 debug(error); 272 throw new Error('failed during getOrchestrationStatus'); 273 } 274 }; 275 276 async ensureOrchestrationSucceeded(orchenstrationID, upgradeTimeoutMin = 30) { 277 // Decides whether to go to the next step of while or not based on 278 // the orchestration result (0 = succeeded, 1 = failed, 2 = cancelled, 3 = pending/other) 279 debug(`Waiting for Kyma Upgrade with OrchestrationID ${orchenstrationID} to succeed...`); 280 try { 281 const res = await wait( 282 () => this.getOrchestrationStatus(orchenstrationID), 283 (res) => res && res.state && (res.state === 'succeeded' || res.state === 'failed'), 284 1000 * 60 * upgradeTimeoutMin, // 30 min 285 1000 * 30, // 30 seconds 286 ); 287 288 if (res.state !== 'succeeded') { 289 debug('KEB Orchestration Status:', res); 290 throw new Error(`orchestration didn't succeed in 15min: ${JSON.stringify(res)}`); 291 } 292 293 const descSplit = res.description.split(' '); 294 if (descSplit[1] !== '1') { 295 throw new Error(`orchestration didn't succeed (number of scheduled operations should be equal to 1): 296 ${JSON.stringify(res)}`); 297 } 298 299 return res; 300 } catch (error) { 301 debug(error); 302 throw new Error('failed during ensureOrchestrationSucceeded'); 303 } 304 } 305 306 async reconcileInformationLog(runtimeStatus) { 307 try { 308 const objRuntimeStatus = JSON.parse(runtimeStatus); 309 310 try { 311 if (!objRuntimeStatus.data[0].runtimeID) {} 312 } catch (e) { 313 console.log('skipping reconciliation logging: no runtimeID provided by runtimeStatus'); 314 return; 315 } 316 317 // kcp reconciliations operations -r <runtimeID> -o json 318 const reconciliationsOperations = await this.getReconciliationsOperations(objRuntimeStatus.data[0].runtimeID); 319 320 const objReconciliationsOperations = JSON.parse(reconciliationsOperations); 321 322 if ( objReconciliationsOperations == null ) { 323 console.log(`skipping reconciliation logging: kcp rc operations -r ${objRuntimeStatus.data[0].runtimeID} 324 -o json returned null`); 325 return; 326 } 327 328 const objReconciliationsOperationsLength = objReconciliationsOperations.length; 329 330 if (objReconciliationsOperationsLength === 0) { 331 console.log(`no reconciliation operations found`); 332 return; 333 } 334 console.log(`number of reconciliation operations: ${objReconciliationsOperationsLength}`); 335 336 // using only last three operations 337 const lastObjReconciliationsOperations = objReconciliationsOperations. 338 slice(Math.max(0, objReconciliationsOperations.length - 3), objReconciliationsOperations.length); 339 340 for (const i of lastObjReconciliationsOperations) { 341 console.log(`reconciliation operation status: ${i.status}`); 342 343 // kcp reconciliations info -i <scheduling-id> -o json 344 await this.getReconciliationsInfo(i.schedulingID); 345 } 346 } catch { 347 console.log('skipping reconciliation logging: error in reconcileInformationLog'); 348 } 349 } 350 351 async exec(args, pipeStdout = false, sendYes = false) { 352 try { 353 const defaultArgs = [ 354 '--config', `${this.kcpConfigPath}`, 355 ]; 356 // debug([`> kcp`, defaultArgs.concat(args).join(" ")].join(" ")) 357 const subprocess = execa('kcp', defaultArgs.concat(args), {stdio: 'pipe'}); 358 if ( pipeStdout ) { 359 subprocess.stdout.pipe(process.stdout); 360 } 361 362 if ( sendYes ) { 363 const inStream = new stream.Readable(); 364 inStream.push('Y'); 365 inStream.push(null); 366 inStream.pipe(subprocess.stdin); 367 } 368 369 const output = await subprocess; 370 return output.stdout; 371 } catch (err) { 372 if (err.stderr === undefined) { 373 throw new Error(`failed to process kcp binary output: ${err.toString()}`); 374 } 375 throw new Error(`kcp command failed: ${err.toString()}`); 376 } 377 } 378 } 379 380 module.exports = { 381 KCPConfig, 382 KCPWrapper, 383 };