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  };