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