ThinkDeepTech/k8s

View on GitHub
src/k8s-api.js

Summary

Maintainability
D
2 days
Test Coverage
import k8s from '@kubernetes/client-node';
import {k8sManifest} from '@thinkdeep/k8s-manifest';
import {ErrorNotFound} from './error/error-not-found.js';
import {normalizeKind} from './normalize-kind.js';

const DEFAULT_NAMESPACE = 'default';

/**
 * Wrapper around K8s javascript client handling interactions with the cluster.
 */
class K8sApi {
  /**
   * Constructor for theeee K8sApi object.
   */
  constructor() {
    this._apiVersionToApiClient = {};
    this._kindToApiClients = {};
    this._kindToGroupVersion = {};
    this._groupVersionToPreferredVersion = {};

    this._kindApiVersionMemo = {};
    this.defaultNamespace = DEFAULT_NAMESPACE;
  }

  /**
   * Initialize the api maps.
   *
   * @param {object} kubeConfig K8s javascript client KubeConfig object.
   * @param {Array<any>} [apis = k8s.APIS] The k8s API objects to use. This is intended for testing.
   */
  async init(kubeConfig, apis = k8s.APIS) {
    if (!this.initialized()) {
      await this._initClientMappings(kubeConfig, apis);
      console.log(`Initialized the k8s api.`);
    }
  }

  /**
   * Set the default namespace to use.
   *
   * @param {string} [val = DEFAULT_NAMESPACE] Default namespace to apply.
   */
  set defaultNamespace(val = DEFAULT_NAMESPACE) {
    this._defaultNamespace = val;
  }

  /**
   * Read the default namespace in use.
   *
   * @return {String} Default namespace.
   */
  get defaultNamespace() {
    return this._defaultNamespace || DEFAULT_NAMESPACE;
  }

  /**
   * Determine if the api has been initialized.
   *
   * @return {Boolean} True if initialized. False otherwise.
   */
  initialized() {
    return (
      Object.keys(this._apiVersionToApiClient).length > 0 &&
      Object.keys(this._kindToApiClients).length > 0 &&
      Object.keys(this._kindToGroupVersion).length > 0 &&
      Object.keys(this._groupVersionToPreferredVersion).length > 0
    );
  }

  /**
   * Map groups to group preferred version.
   * @param {Object} _ Unused API client object.
   * @param {any} apiGroup K8s javascript client APIGroup manifest.
   */
  _applyPreferredVersionToGroupMap(_, apiGroup) {
    for (const entry of apiGroup.versions) {
      /**
       * Initialize group version to preferred api version.
       */
      this._groupVersionToPreferredVersion[entry.groupVersion.toLowerCase()] =
        apiGroup.preferredVersion.groupVersion;
    }
  }

  /**
   * Store resource list data in maps.
   *
   * @param {Object} apiClient K8s javascript client API object.
   * @param {any} resourceList K8s javascript client ResourceList manifest.
   */
  _applyResourceListValuesToMaps(apiClient, resourceList) {
    /**
     * Initialize apiVersion-specific client mappings.
     */
    this._apiVersionToApiClient[resourceList.groupVersion.toLowerCase()] =
      apiClient;

    for (const resource of resourceList.resources) {
      const resourceKind = normalizeKind(resource.kind).toLowerCase();
      if (!this._kindToApiClients[resourceKind]) {
        this._kindToApiClients[resourceKind] = [];
      }

      /**
       * Initialize broadcast capability based on kind.
       */
      this._kindToApiClients[resourceKind].push(apiClient);

      if (!this._kindToGroupVersion[resourceKind]) {
        this._kindToGroupVersion[resourceKind] = new Set();
      }

      /**
       * Enable mapping of kind to group version for preferred version determination.
       */
      this._kindToGroupVersion[resourceKind].add(resourceList.groupVersion);

      /**
       * Some API groups aren't listed so the initial preferred version will be equivalent to
       * the current group version.
       */
      this._groupVersionToPreferredVersion[
        resourceList.groupVersion.toLowerCase()
      ] = resourceList.groupVersion;
    }
  }

  /**
   * Initialize mappings to clients.
   *
   * @param {k8s.KubeConfig} kubeConfig
   * @param {Array<Object>} apis APIs against which the resource function will be executed. This is primarily used for testing.
   */
  async _initClientMappings(kubeConfig, apis = k8s.APIS) {
    /**
     * The ordering of the following functions is important due to group to preferred version mapping.
     * Some api groups may not return (i.e, v1). The first function initializes the preferred group
     * version to its current group version and the second overwrites that with the actual registered
     * preferred version returned by the API. Therefore, the order should be:
     *
     * resource list processing -> api group processing.
     */
    await this._forEachApiResourceList(
      kubeConfig,
      this._applyResourceListValuesToMaps.bind(this),
      apis
    );

    await this._forEachApiGroup(
      kubeConfig,
      this._applyPreferredVersionToGroupMap.bind(this),
      apis
    );
  }

  /**
   * Execute a callback over each cluster API group.
   * @param {k8s.KubeConfig} kubeConfig K8s javascript client kube config.
   * @param {Function<Object, any>} callback Function to execute of the form (apiClient, manifest) => {...}
   * @param {Array<Object>} apis APIs against which the resource function will be executed. This is primarily used for testing.
   */
  async _forEachApiGroup(kubeConfig, callback, apis = k8s.APIS) {
    await this._forEachApi(kubeConfig, 'getAPIGroup', callback, apis);
  }

  /**
   * Execute a callback over each cluster API resource list.
   * @param {k8s.KubeConfig} kubeConfig K8s javascript client kube config.
   * @param {Function<Object, any>} callback Function to execute of the form (apiClient, manifest) => {...}
   * @param {Array<Object>} apis APIs against which the resource function will be executed. This is primarily used for testing.
   */
  async _forEachApiResourceList(kubeConfig, callback, apis = k8s.APIS) {
    await this._forEachApi(kubeConfig, 'getAPIResources', callback, apis);
  }

  /**
   * Run a callback over the results from executing a resource function.
   * @param {k8s.KubeConfig} kubeConfig K8s javascript client kube config.
   * @param {String} resourceFunctionName Name of the function to execute on each API.
   * @param {Function<apiClient, any>} callback Callback to execute with resultant data.
   * @param {Array<Object>} apis APIs against which the resource function will be executed. This is primarily used for testing.
   */
  async _forEachApi(kubeConfig, resourceFunctionName, callback, apis) {
    if (!(kubeConfig instanceof k8s.KubeConfig)) {
      throw new Error(`Supplied k8s kube configuration was invalid.`);
    }

    if (!Array.isArray(apis)) {
      throw new Error(`Supplied APIs must be an array.`);
    }

    for (const api of apis) {
      const apiClient = kubeConfig.makeApiClient(api);

      if (!(resourceFunctionName in apiClient)) {
        /**
         * These cases should be ignored because the k8s javascript client APIs don't
         * all include the same functions. Therefore, when iterating over all the APIs,
         * it's necessary to take that into account.
         */
        continue;
      }

      const fetchResources = apiClient[resourceFunctionName];

      if (typeof fetchResources !== 'function') {
        throw new Error(
          `The resource function provided was not a function: ${resourceFunctionName}`
        );
      }

      try {
        const {
          response: {body},
        } = await fetchResources.bind(apiClient)();

        const manifest = k8sManifest(this._configuredManifestObject(body));

        this._memoizeManifestMetadata(manifest);

        callback(apiClient, manifest);
      } catch (e) {
        if (!e?.response?.statusCode || e?.response?.statusCode !== 404) {
          throw e;
        }
      }
    }
  }

  /**
   * Fetch all registered group versions.
   * @param {String} kind  K8s kind.
   * @return {Set<String>} Group versions.
   */
  _groupVersions(kind) {
    return (
      this._kindToGroupVersion[normalizeKind(kind).toLowerCase()] || new Set()
    );
  }

  /**
   * Fetch all relevant client APIs.
   *
   * @param {String} kind K8s kind.
   * @return {Array<Object>} K8s javascript client APIs (i.e, from kubeConfig.makeApiClient(...))
   */
  _clientApis(kind) {
    return this._kindToApiClients[normalizeKind(kind).toLowerCase()] || [];
  }

  /**
   * Get the registered client API associated with a specific api version.
   * @param {String} apiVersion K8s api version.
   * @return {Object} K8s javascript client API instance (i.e, from kubeConfig.makeApiClient(...))
   */
  _clientApi(apiVersion) {
    const client =
      this._apiVersionToApiClient[apiVersion.toLowerCase()] || null;

    if (!client) {
      throw new ErrorNotFound(
        `The specified api version ${apiVersion} was not recognized. Are you sure you're using one accepted by k8s?`
      );
    }

    return client;
  }

  /**
   * Check if the kind has been registered with the API.
   *
   * @param {String} kind K8s kind.
   * @return {Boolean} True if it was registered. False otherwise.
   */
  _registeredKind(kind) {
    return kind.toLowerCase() in this._kindToGroupVersion;
  }

  /**
   * Get the preferred api versions for the specified kind.
   *
   * NOTE: One kind can be part of multiple groups. Therefore, multiple preferred versions can exist.
   *
   * @param {string} kind K8s Kind.
   * @return {Array<String>} Preferred api versions or [] if none exist.
   */
  preferredApiVersions(kind) {
    const kindGroups = this._groupVersions(kind);

    return this._preferredApiVersions(kindGroups);
  }

  /**
   * Get the preferred API version for each group mentioned.
   * @param {Array<String>} groupVersions K8s cluster group versions.
   * @return {Array<String>} Preferred API versions for each group or [].
   */
  _preferredApiVersions(groupVersions) {
    /**
     * A given kind can be part of multiple groups. Therefore, there are multiple preferred versions.
     */
    const preferredVersions = new Set();
    for (const groupVersion of groupVersions) {
      const registeredPreferredVersion =
        this._groupVersionToPreferredVersion[groupVersion.toLowerCase()] || '';
      preferredVersions.add(registeredPreferredVersion);
    }

    return [...preferredVersions].filter((val) => Boolean(val));
  }

  /**
   *  Create all objects on the cluster.
   *
   * @param {Array<any>} manifests K8s javascript client objects.
   * @return {Promise< Array<any> >} K8s client objects or [] if the objects already exist.
   */
  async createAll(manifests) {
    const targets = [];
    for (const manifest of manifests) {
      try {
        const strategy = this._creationStrategy(manifest);

        const received = await strategy();

        const returnedManifest = k8sManifest(
          this._configuredManifestObject(received.response.body)
        );

        this._memoizeManifestMetadata(returnedManifest);

        targets.push(returnedManifest);
      } catch (e) {
        if (!e?.response?.statusCode || e?.response?.statusCode !== 409) {
          throw e;
        }
      }
    }

    return targets;
  }

  /**
   * Fetch the strategy to use for object creation.
   * @param {any} manifest K8s javascript client manifest object.
   * @return {Function} Strategy to use to create the specified object on the cluster with arguments bound.
   */
  _creationStrategy(manifest) {
    const kind = normalizeKind(manifest.kind);

    if (!this._registeredKind(kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const api = this._clientApi(manifest.apiVersion);
    if (api[`create${kind}`]) {
      return api[`create${kind}`].bind(api, manifest);
    } else if (api[`createNamespaced${kind}`]) {
      return api[`createNamespaced${kind}`].bind(
        api,
        manifest?.metadata?.namespace || this.defaultNamespace,
        manifest
      );
    }

    throw new Error(`
                The creation function for kind ${kind} wasn't found. This may be because it has an incorrect api version or that it hasn't yet been implemented yet. Please double-check the api version you used in the manifest and if the issue persists submit an issue on the github repo.
            `);
  }

  /**
   * Determine if the specified object exists on the cluster.
   *
   * @param {string} kind K8s kind.
   * @param {string} name K8s object metadata name.
   * @param {string} namespace K8s namespace.
   * @return {Boolean} True if the object exists on the cluster. False otherwise.
   */
  async exists(kind, name, namespace) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    try {
      await this.read(_kind, name, namespace);
      return true;
    } catch (e) {
      if (e.constructor.name !== 'ErrorNotFound') {
        throw e;
      }
      return false;
    }
  }

  /**
   * Read an object from the cluster.
   *
   * NOTE: If the object doesn't exist on the cluster a ErrorNotFound exception will be thrown.
   *
   * @param {string} kind K8s kind.
   * @param {string} name Name of the object as seen in the metadata.name field.
   * @param {string} [namespace = this.defaultNamespace] Namespace of the object as seen in the metadata.namespace field.
   * @return {any} A kubernetes javascript client representation of the object on the cluster.
   */
  async read(kind, name, namespace = this.defaultNamespace) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const results = await this._broadcastReadStrategy(_kind, name, namespace)();

    if (results.length <= 0) {
      const namespaceMessage = namespace ? `in namespace ${namespace}` : ``;
      throw new ErrorNotFound(
        `The resource of kind ${kind} with name ${name} ${namespaceMessage} wasn't found.`
      );
    }

    return results.map((received) => {
      const manifest = k8sManifest(
        this._configuredManifestObject(received.response.body)
      );

      this._memoizeManifestMetadata(manifest);

      return manifest;
    })[0];
  }

  /**
   * Get the k8s javascript client strategy for reading cluster objects.
   *
   * @param {string} kind K8s kind.
   * @param {string} name Name of the object as seen in the metadata.name field.
   * @param {string} [namespace = this.defaultNamespace] Namespace of the object as seen in the metadata.namespace field.
   * @return {Function} Read function broadcasting to all kind apis with arguments bound.
   */
  _broadcastReadStrategy(kind, name, namespace = this.defaultNamespace) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const apis = this._clientApis(_kind);

    const strategies = [];
    for (const api of apis) {
      strategies.push(
        this._readClusterObjectStrategy(api, _kind, name, namespace)
      );
    }

    return this._handleStrategyExecution.bind(this, strategies);
  }

  /**
   * Get the read function from the k8s javascript client API.
   *
   * @param {any} api K8s javascript client API with the needed function.
   * @param {string} kind K8s kind.
   * @param {string} name Name of the object as seen in the metadata.name field.
   * @param {string} [namespace = this.defaultNamespace] Namespace of the object as seen in the metadata.namespace field.
   * @return {Function} Function to use to read the specified cluster object with arguments bound.
   */
  _readClusterObjectStrategy(
    api,
    kind,
    name,
    namespace = this.defaultNamespace
  ) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    if (!api) {
      throw new Error(`Api needs to be defined.`);
    }

    if (api[`read${_kind}`]) {
      return api[`read${_kind}`].bind(api, name);
    } else if (api[`readNamespaced${_kind}`]) {
      return api[`readNamespaced${_kind}`].bind(api, name, namespace);
    }

    const namespaceText = namespace ? `and namespace ${namespace}` : ``;

    throw new ErrorNotFound(`
                The read function for kind ${kind} ${namespaceText} wasn't found. This may be because it hasn't yet been implemented. Please submit an issue on the github repo relating to this.
            `);
  }

  /**
   * Update the cluster objects with the specified manifests.
   *
   * @param {Array<any>} manifests K8s javascript client objects.
   * @return {Promise< Array<any> >} Promise that resolves to the updated k8s client objects.
   */
  patchAll(manifests) {
    return Promise.all(
      manifests.map(async (manifest) => {
        const responses = await this._broadcastPatchStrategy(manifest)();

        if (responses.length === 0) {
          const namespaceMessage = manifest.metadata.namespace
            ? `in namespace ${manifest.metadata.namespace}`
            : '';
          throw new ErrorNotFound(
            `The resource ${manifest.metadata.name} of kind ${manifest.kind} ${namespaceMessage} wasn't found and, therefore, can't be updated.`
          );
        }

        const received = responses[0];

        const returnedManifest = k8sManifest(
          this._configuredManifestObject(received.response.body)
        );

        this._memoizeManifestMetadata(returnedManifest);

        return returnedManifest;
      })
    );
  }

  /**
   * Fetch the strategy for broadcasting the patch to the relevant APIs.
   *
   * @param {any} manifest K8s javascript client manifest object.
   * @return {Function} Strategy to use to broadcast the patch update to the relevant APIs with the arguments bound.
   */
  _broadcastPatchStrategy(manifest) {
    const kind = normalizeKind(manifest.kind);

    if (!this._registeredKind(kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const apis = this._clientApis(kind);

    const strategies = [];
    for (const api of apis) {
      strategies.push(this._patchClusterObjectStrategy(api, kind, manifest));
    }

    return this._handleStrategyExecution.bind(this, strategies);
  }

  /**
   * Fetch the patch strategy for the specified cluster object.
   *
   * @param {any} api K8s client API object (i.e, those in k8s.APIS).
   * @param {String} kind K8s kind.
   * @param {any} manifest K8s javascript client manifest object.
   * @return {Function} Strategy for interfacing with the cluster.
   */
  _patchClusterObjectStrategy(api, kind, manifest) {
    const pretty = undefined;

    const dryRun = undefined;

    const fieldManager = undefined;

    const force = undefined;

    const options = {
      headers: {
        'Content-type': 'application/merge-patch+json',
      },
    };

    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    if (api[`patchNamespaced${_kind}`]) {
      return api[`patchNamespaced${_kind}`].bind(
        api,
        manifest.metadata.name,
        manifest.metadata.namespace,
        manifest,
        pretty,
        dryRun,
        fieldManager,
        force,
        options
      );
    } else if (api[`patch${_kind}`]) {
      return api[`patch${_kind}`].bind(
        api,
        manifest.metadata.name,
        manifest,
        pretty,
        dryRun,
        fieldManager,
        force,
        options
      );
    }
    throw new Error(`
                The patch function for kind ${kind} wasn't found. This may be because it hasn't yet been implemented. Please submit an issue on the github repo relating to this.
            `);
  }

  /**
   * List all cluster objects.
   *
   * @param {string} kind The k8s kind (i.e, CronJob).
   * @param {string} namespace The namespace of the object as seen in the k8s metadata namespace field.
   * @return {Promise<Array<any>>} Promise that resolves to array of kind list objects (i.e, CronJobList) taken from the various relevant APIs (i.e, batch/v1 and batch/v1beta1).
   */
  async listAll(kind, namespace) {
    if (!this._registeredKind(kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const responses = await this._broadcastListStrategy(kind, namespace)();

    return Promise.all(
      responses.map((data) => {
        const {
          response: {body},
        } = data;

        if (!body) {
          throw new Error(
            `The API response didn't include a valid body. Received: ${body}`
          );
        }

        const kindList = k8sManifest(this._configuredManifestObject(body));

        for (let i = 0; i < kindList.items.length; i++) {
          const itemKind = normalizeKind(
            kindList.items[i]?.constructor?.name || ''
          );
          kindList.items[i].kind = itemKind;
          kindList.items[i].apiVersion = kindList.apiVersion;
          kindList.items[i] = this._configuredManifestObject(kindList.items[i]);
        }

        this._memoizeManifestMetadata(kindList);

        if (Array.isArray(kindList.items) && kindList.items.length > 0) {
          this._memoizeManifestMetadata(kindList.items[0]);
        }

        return kindList;
      })
    );
  }

  /**
   * Fetch the strategy to use to broadcast the list request across the relevant apis.
   *
   * @param {String} kind K8s kind.
   * @param {String} namespace K8s namespace.
   * @return {Function} Strategy that will broadcast list request with arguments bound.
   */
  _broadcastListStrategy(kind, namespace) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    const apis = this._clientApis(_kind);

    const strategies = [];
    for (const api of apis) {
      strategies.push(this._listClusterObjectsStrategy(api, _kind, namespace));
    }

    return this._handleStrategyExecution.bind(this, strategies);
  }

  /**
   * Fetch the k8s client list function usable to interface with the cluster.
   *
   * @param {any} api K8s client api type defined in k8s.APIS.
   * @param {string} kind K8s kind.
   * @param {string} namespace K8s cluster namespace
   * @return {Function} Strategy to use to interface with the cluster with the arguments bound.
   */
  _listClusterObjectsStrategy(api, kind, namespace) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    if (api[`list${_kind}`]) {
      return api[`list${_kind}`].bind(api);
    } else if (!namespace && api[`list${_kind}ForAllNamespaces`]) {
      return api[`list${_kind}ForAllNamespaces`].bind(api);
    } else if (api[`listNamespaced${_kind}`]) {
      return api[`listNamespaced${_kind}`].bind(
        api,
        namespace || this.defaultNamespace
      );
    }

    const namespaceText = namespace ? `and namespace ${namespace}` : ``;

    throw new Error(`
                The list function for kind ${kind} ${namespaceText} wasn't found. This may be because it hasn't yet been implemented. Please submit an issue on the github repo relating to this.
            `);
  }

  /**
   * Delete all the specified objects on the cluster.
   *
   * @param {Array<any>} manifests K8s javascript client objects.
   * @return {Promise} Promise that resolves once deletion completes for all manifests.
   */
  deleteAll(manifests) {
    return Promise.all(
      manifests.map((manifest) => this._deletionStrategy(manifest)())
    );
  }

  /**
   * Fetch the deletion strategy.
   *
   * @param {any} manifest K8s client object manifest.
   * @return {Function} Deletion strategy with parameters bound.
   */
  _deletionStrategy(manifest) {
    if (!manifest) {
      throw new Error(`The manifest value wasn't defined.`);
    }

    if (!this._registeredKind(manifest.kind)) {
      throw new ErrorNotFound(
        `Kind ${manifest.kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    if (!manifest.apiVersion) {
      throw new Error(`The api version wasn't defined.`);
    }

    const api = this._clientApi(manifest.apiVersion);

    return this._handleStrategyExecution.bind(this, [
      this._deleteClusterObjectStrategy(
        api,
        normalizeKind(manifest.kind),
        manifest
      ),
    ]);
  }

  /**
   * Fetch the strategy to use to delete the specified cluster object.
   *
   * @param {Object} api Api object of a type included in k8s.APIS.
   * @param {string} kind K8s kind.
   * @param {any} manifest K8s client object manifest.
   * @return {Function} Deletion strategy to use for the specified object with the necessary parameters bound.
   */
  _deleteClusterObjectStrategy(api, kind, manifest) {
    const _kind = normalizeKind(kind);

    if (!this._registeredKind(_kind)) {
      throw new ErrorNotFound(
        `Kind ${kind} was not found in the API. Are you sure it's correctly spelled?`
      );
    }

    if (api[`deleteNamespaced${_kind}`]) {
      return api[`deleteNamespaced${_kind}`].bind(
        api,
        manifest.metadata.name,
        manifest.metadata.namespace
      );
    } else if (api[`delete${_kind}`]) {
      return api[`delete${_kind}`].bind(api, manifest.metadata.name);
    }
    throw new Error(`
                The deletion function for kind ${kind} wasn't found. This may be because it hasn't yet been implemented. Please submit an issue on the github repo relating to this.
            `);
  }

  /**
   * Execute all the provided strategies and filter out falsy values.
   *
   * @param {Array<Function>} strategies Strategies to execute.
   * @return {Promise< Array<Object> >} Array containing the response from the strategy. This typically has the form:
   * {
   *      response: {
   *         body: { ... }
   *      }
   * }
   */
  async _handleStrategyExecution(strategies) {
    return (
      await Promise.all(
        strategies.map(async (strategy) => {
          try {
            return await strategy();
          } catch (e) {
            if (!e?.response?.statusCode || e?.response?.statusCode !== 404) {
              throw e;
            }

            return null;
          }
        })
      )
    ).filter((value) => Boolean(value));
  }

  /**
   * Check that required fields are defined on the incoming object and, if they aren't, attempt to fill them in with known data.
   *
   * @param {Object} configuration Manifest object.
   * @return {Object} Configuration with inferred fields if they weren't already present.
   */
  _configuredManifestObject(configuration) {
    if (!configuration) {
      throw new Error(`The configuration must be defined.`);
    }

    if (!configuration.apiVersion) {
      const kind = configuration.kind || '';
      configuration.apiVersion =
        this._inferApiVersion(kind) || configuration.groupVersion || '';
    }

    return configuration;
  }

  /**
   * Infer the api version from those preferred by the cluster or, if there aren't any, from those that have been seen on the objects encountered.
   *
   * @param {string} kind K8s kind.
   * @return {string|null} Inferred API version or null.
   */
  _inferApiVersion(kind) {
    const _kind = normalizeKind(kind).toLowerCase();
    return (
      this.preferredApiVersions(_kind)[0] ||
      this._memoizedApiVersions(_kind)[0] ||
      null
    );
  }

  /**
   * Memoize manifest metadata.
   *
   * @param {any} manifest K8s client object with a valid kind.
   */
  _memoizeManifestMetadata(manifest) {
    const kind = manifest.kind || '';

    if (!kind) {
      throw new Error(`The kind must be defined`);
    }

    const _kind = kind.toLowerCase();
    if (!this._memoizedApiVersions(_kind)) {
      this._kindApiVersionMemo[_kind] = new Set();
    }

    const apiVersion = manifest.apiVersion || '';
    if (
      Boolean(apiVersion) &&
      !this._memoizedApiVersions(_kind).includes(apiVersion)
    ) {
      this._kindApiVersionMemo[_kind].add(apiVersion);
    }
  }

  /**
   * Fetch the memoized api versions.
   *
   * @param {string} kind K8s kind.
   * @return {Array<string>} The api versions or [].
   */
  _memoizedApiVersions(kind) {
    const _kind = kind.toLowerCase();
    if (!this._kindApiVersionMemo[_kind]) {
      this._kindApiVersionMemo[_kind] = new Set();
    }

    return [...this._kindApiVersionMemo[_kind]] || [];
  }
}

export {K8sApi};