"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _debug = _interopRequireDefault(require("debug"));

var _core = require("@kui-shell/core");

var _flags = require("./flags");

var _fqn = require("./fqn");

var _formatTable = require("../../lib/view/formatTable");

var _options = require("./options");

var _commandPrefix = _interopRequireDefault(require("../command-prefix"));

var _fetchFile = _interopRequireWildcard(require("../../lib/util/fetch-file"));

var _trafficLight = _interopRequireDefault(require("../../lib/model/traffic-light"));

var _states = require("../../lib/model/states");

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
 * Copyright 2018-19 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
  function adopt(value) {
    return value instanceof P ? value : new P(function (resolve) {
      resolve(value);
    });
  }

  return new (P || (P = Promise))(function (resolve, reject) {
    function fulfilled(value) {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    }

    function rejected(value) {
      try {
        step(generator["throw"](value));
      } catch (e) {
        reject(e);
      }
    }

    function step(result) {
      result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
    }

    step((generator = generator.apply(thisArg, _arguments || [])).next());
  });
};

const strings = (0, _core.i18n)('plugin-kubectl');
const debug = (0, _debug.default)('plugin-kubectl/controller/kubectl/status');
/** administartive core controllers that we want to ignore */
// const adminCoreFilter = '-l provider!=kubernetes'

/** administrative CRDs that we want to ignore */
// const adminCRDFilter = '-l app!=mixer,app!=istio-pilot,app!=ibmcloud-image-enforcement,app!=ibm-cert-manager'

const usage = command => ({
  command,
  strict: command,
  docs: 'Check the deployment status of a set of resources',
  onlyEnforceOptions: true,
  optional: [{
    name: '--filename',
    alias: '-f',
    file: true,
    docs: 'A kubernetes resource file or kind'
  }, {
    name: '--kustomize',
    alias: '-k',
    file: true,
    docs: 'A kustomize file or directory'
  }, {
    name: 'resourceName',
    positional: true,
    docs: 'The name of a kubernetes resource of the given kind'
  }, {
    name: '--final-state',
    hidden: true
  }, {
    name: '--namespace',
    alias: '-n',
    docs: 'Inspect a specified namespace'
  }, {
    name: '--all',
    alias: '-a',
    docs: 'Show status across all namespaces'
  }, {
    name: '--multi',
    alias: '-m',
    docs: 'Display multi-cluster views as a multiple tables'
  }, {
    name: '--response',
    docs: 'The initial response from the CRUD command'
  }, {
    name: '--command',
    string: true,
    docs: 'The initial command from the CRUD command'
  }, {
    name: '--watching',
    hidden: true,
    boolean: true,
    docs: 'internal use: called as part of the polling loop'
  }, {
    name: '--watch',
    alias: '-w',
    boolean: true,
    docs: 'After listing/getting the requested object, watch for changes'
  }],
  example: `kubectl ${command} @seed/cloud-functions/function/echo.yaml`
});
/**
 * @param file an argument to `-f` or `-k`; e.g. `kubectl -f <file>`
 *
 */


function getResourcesReferencedByFile(file, args) {
  return __awaiter(this, void 0, void 0, function* () {
    const [{
      safeLoadAll
    }, raw] = yield Promise.all([Promise.resolve().then(() => require('js-yaml')), (0, _fetchFile.default)(args.REPL, file)]);
    const namespaceFromCommandLine = yield (0, _options.getNamespace)(args);
    const models = safeLoadAll(raw);
    return models.filter(_ => _.metadata).map(({
      apiVersion,
      kind,
      metadata: {
        name,
        namespace = namespaceFromCommandLine
      }
    }) => {
      const {
        group,
        version
      } = (0, _fqn.versionOf)(apiVersion);
      return {
        group,
        version,
        kind,
        name,
        namespace
      };
    });
  });
}

function getResourcesReferencedByKustomize(kusto, args) {
  return __awaiter(this, void 0, void 0, function* () {
    const [{
      safeLoad
    }, {
      join
    }, raw] = yield Promise.all([Promise.resolve().then(() => require('js-yaml')), Promise.resolve().then(() => require('path')), (0, _fetchFile.fetchFileKustomize)(args.REPL, kusto)]);
    const kustomization = safeLoad(raw.data);

    if (kustomization.resources) {
      const files = yield Promise.all(kustomization.resources.map(resource => {
        return (0, _fetchFile.default)(args.REPL, raw.dir ? join(raw.dir, resource) : resource);
      }));
      return yield Promise.all(files.map(raw => safeLoad(raw[0])).map(resource => __awaiter(this, void 0, void 0, function* () {
        const {
          apiVersion,
          kind,
          metadata
        } = resource;
        const {
          group,
          version
        } = (0, _fqn.versionOf)(apiVersion);
        return {
          group,
          version,
          kind,
          name: metadata.name,
          namespace: metadata.namespace || (yield (0, _options.getNamespace)(args))
        };
      })));
    }

    return [];
  });
}
/**
 * @param argvRest the argv after `kubectl status`, with options stripped off
 *
 */


function getResourcesReferencedByCommandLine(argvRest, args) {
  return __awaiter(this, void 0, void 0, function* () {
    // Notes: kubectl create secret <generic> <name> <-- the name is in a different slot :(
    const [kind, nameGroupVersion, nameAlt] = argvRest;
    const isDelete = args.parsedOptions['final-state'] === _states.FinalState.OfflineLike;
    const isCreateSecret = !isDelete && /secret(s)?/i.test(kind);
    const [name, group, version] = isCreateSecret ? [nameAlt] : nameGroupVersion.split(/\./);
    const namespace = yield (0, _options.getNamespace)(args);
    return [{
      group,
      version,
      kind,
      name,
      namespace
    }];
  });
}
/**
 * Has the resource represented by the given table Row reached its
 * desired final state?
 *
 */


function isResourceReady(row, finalState) {
  const status = row.attributes.find(_ => _.key === 'STATUS');

  if (status !== undefined) {
    // primary plan: use the STATUS column
    return (0, _states.isDone)(status.value, finalState);
  } else {
    // backup plan: use the READY column, of the form nReady/nTotal
    const ready = row.attributes.find(_ => _.key === 'READY');

    if (ready !== undefined) {
      const [nReady, nTotal] = ready.value.split(/\//);
      return nReady && nTotal && nReady === nTotal;
    }
  } // heuristic: if we find neither a STATUS nor a READY column,
  // then assume it's ready; e.g. configmaps have this property


  return true;
}
/**
 * The table push notification `update` routine assumes it has a copy of the rows.
 *
 */


function clone(row) {
  const copy = Object.assign({}, row);
  copy.attributes = row.attributes.map(_ => Object.assign({}, _));
  return copy;
}

class StatusPoller {
  constructor(tab, ref, row, finalState, done, pusher, contextArgs, command, pollInterval = 1000, ladder = StatusPoller.calculateLadder(pollInterval)) {
    this.tab = tab;
    this.ref = ref;
    this.row = row;
    this.finalState = finalState;
    this.done = done;
    this.pusher = pusher;
    this.contextArgs = contextArgs;
    this.command = command;
    this.ladder = ladder;
    this.pollOnce(0);
  }

  pollOnce(iter) {
    return __awaiter(this, void 0, void 0, function* () {
      const sleepTime = iter < this.ladder.length ? this.ladder[iter] : this.ladder[this.ladder.length - 1];
      debug('pollOnce', this.ref, sleepTime, (0, _fqn.fqnOfRef)(this.ref));

      try {
        const table = yield this.tab.REPL.qexec(`${this.command} get ${(0, _fqn.fqnOfRef)(this.ref)} ${this.contextArgs}`);
        debug('pollOnce table', table);

        if (table && table.body && table.body.length === 1) {
          const row = table.body[0];
          const isReady = isResourceReady(row, this.finalState);
          const newStatusAttr = row.attributes.find(_ => _.key === 'STATUS') || row.attributes.find(_ => _.key === 'READY');
          const rowForUpdate = clone(this.row);
          const statusAttr = rowForUpdate.attributes.find(({
            key
          }) => key === 'STATUS');
          statusAttr.value = newStatusAttr ? newStatusAttr.value : 'Ready';

          if (isReady) {
            statusAttr.css = _trafficLight.default.Green;
          }

          this.pusher.update(rowForUpdate);

          if (isReady) {
            debug('resource is ready', this.ref, this.row, newStatusAttr);
            this.done();
            return;
          }
        } else {
          console.error('unexpected tabular response in poller', table);
        }
      } catch (error) {
        const err = error;

        if (err.code === 404 && this.finalState === _states.FinalState.OfflineLike) {
          this.pusher.offline(this.ref.name);
          this.done();
          return;
        }
      }

      this.timer = setTimeout(() => this.pollOnce(iter + 1), sleepTime);
    });
  }
  /**
   * calculate the polling ladder
   *
   */


  static calculateLadder(initial) {
    // final polling rate (do not increase the interval beyond this!)
    let finalPolling = 5000;

    try {
      finalPolling = require('@kui-shell/client/config.d/limits.json').tablePollingInterval;
    } catch (err) {
      debug('using default tablePollingInterval', err);
    }

    const ladder = [initial];
    let current = initial; // increment the polling interval

    while (current < finalPolling) {
      if (current < 1000) {
        current = current + 250 < 1000 ? current + 250 : 1000;
        ladder.push(current);
      } else {
        ladder.push(current);
        current = current + 2000 < finalPolling ? current + 2000 : finalPolling;
        ladder.push(current);
      }
    } // debug('ladder', ladder)


    return ladder;
  }
  /**
   * Our impl of `Abortable`
   *
   */


  abort() {
    if (this.timer) {
      clearTimeout(this.timer);
    }
  }

}

class StatusWatcher {
  // eslint-disable-next-line no-useless-constructor
  constructor(tab, resourcesToWaitFor, finalState, contextArgs, command) {
    this.tab = tab;
    this.resourcesToWaitFor = resourcesToWaitFor;
    this.finalState = finalState;
    this.contextArgs = contextArgs;
    this.command = command;
    this.pollers = [];
  }
  /**
   * Our impl of `Abortable` for use by the table view
   *
   */


  abort() {
    this.pollers.forEach(poller => {
      if (poller) {
        // be careful: the done() method below may nullify
        // this.pollers[] entries
        poller.abort();
      }
    });
  }
  /**
   * Our impl of the `Watcher` API. This is the callback we will
   * receive from the table UI when it is ready for us to start
   * injecting updates to the table.
   *
   */


  init(pusher) {
    let countdown = this.resourcesToWaitFor.length;

    const done = () => {
      if (--countdown === 0) {
        debug('all resources are ready');
        pusher.done();

        for (let idx = 0; idx < this.pollers.length; idx++) {
          this.pollers[idx] = undefined;
        }
      }
    };

    this.resourcesToWaitFor.map((_, idx) => {
      const row = this.initialBody[idx];
      return new StatusPoller(this.tab, _, row, this.finalState, done, pusher, this.contextArgs, this.command);
    }).forEach(_ => {
      this.pollers.push(_);
    });
  }
  /**
   * We only display a NAMESPACE column if at least one of the
   * resources as a non-default namespace.
   *
   */


  nsAttr(ns, anyNonDefaultNamespaces) {
    return !anyNonDefaultNamespaces ? [] : [{
      key: 'NAMESPACE',
      value: ns
    }];
  }
  /**
   * Formulate an initial response for the REPL
   *
   */


  initialTable() {
    const anyNonDefaultNamespaces = this.resourcesToWaitFor.some(({
      namespace
    }) => namespace !== 'default');
    this.initialBody = this.resourcesToWaitFor.map(ref => {
      const {
        group = '',
        version = '',
        kind,
        name,
        namespace
      } = ref;
      return {
        name,
        onclick: `${this.command} get ${(0, _fqn.fqnOfRef)(ref)} -o yaml`,
        onclickSilence: true,
        attributes: this.nsAttr(namespace, anyNonDefaultNamespaces).concat([{
          key: 'KIND',
          value: kind + (group.length > 0 ? `.${version}.${group}` : ''),
          outerCSS: '',
          css: ''
        }, {
          key: 'STATUS',
          tag: 'badge',
          value: strings('Pending'),
          outerCSS: '',
          css: _trafficLight.default.Yellow
        } // { key: 'MESSAGE', value: '', outerCSS: 'hide-with-sidecar' }
        ])
      };
    });
    const initialHeader = {
      key: 'NAME',
      name: 'Name',
      attributes: this.initialBody[0].attributes.map(({
        key,
        outerCSS
      }) => ({
        key,
        value: (0, _formatTable.initialCapital)(key),
        outerCSS
      }))
    };
    return {
      header: initialHeader,
      body: this.initialBody,
      watch: this
    };
  }

}

const doStatus = command => args => __awaiter(void 0, void 0, void 0, function* () {
  const rest = args.argvNoOptions.slice(args.argvNoOptions.indexOf('status') + 1);
  const commandArg = command || args.parsedOptions.command;
  const file = (0, _options.fileOf)(args);
  const kusto = (0, _options.kustomizeOf)(args);
  const contextArgs = (0, _options.getContextForArgv)(args); // const fileArgs = file ? `-f ${file}` : ''
  // const cmd = `${command} get ${rest} --watch ${fileArgs} ${contextArgs}`

  try {
    const resourcesToWaitFor = file ? yield getResourcesReferencedByFile(file, args) : kusto ? yield getResourcesReferencedByKustomize(kusto, args) : yield getResourcesReferencedByCommandLine(rest, args);
    debug('resourcesToWaitFor', resourcesToWaitFor);
    /* if (nResourcesToWaitFor > 1) {
    // we don't yet support this; return whatever kubectl emitted from
    // the initial command
    return initialResponse
    } */
    // the desired final state of the specified resources

    const finalState = args.parsedOptions['final-state'];
    return new StatusWatcher(args.tab, resourcesToWaitFor, finalState, contextArgs, commandArg).initialTable();
  } catch (err) {
    console.error('error constructing StatusWatcher', err); // the text that the create or delete emitted, i.e. the command that
    // initiated this status request

    const initialResponse = args.parsedOptions.response;
    return initialResponse;
  }
  /* return args.REPL.qexec(
    cmd,
    args.block,
    undefined,
    Object.assign({}, args.execOptions, {
      finalState,
      nResourcesToWaitFor,
      initialResponse
    })
  ) */

});
/**
 * Register the commands
 *
 */


var _default = registrar => {
  const opts = Object.assign({
    usage: usage('status')
  }, (0, _flags.flags)(['watching']));
  registrar.listen(`/${_commandPrefix.default}/kubectl/status`, doStatus('kubectl'), opts);
  registrar.listen(`/${_commandPrefix.default}/k/status`, doStatus('k'), opts);
  registrar.listen(`/${_commandPrefix.default}/status`, doStatus(''), opts);
};

exports.default = _default;