UNPKG

@openshift-console/dynamic-plugin-sdk

Version:

Provides core APIs, types and utilities used by dynamic plugins at runtime.

232 lines (231 loc) 9.56 kB
import * as _ from 'lodash'; import { action } from 'typesafe-actions'; import { getReferenceForModel } from '../../../utils/k8s/k8s-ref'; import { k8sList, k8sGet } from '../../../utils/k8s/k8s-resource'; import { k8sWatch } from '../../../utils/k8s/k8s-utils'; import { getImpersonate } from '../../core/reducers/coreSelectors'; export var ActionType; (function (ActionType) { ActionType["ReceivedResources"] = "resources"; ActionType["GetResourcesInFlight"] = "getResourcesInFlight"; ActionType["StartWatchK8sObject"] = "startWatchK8sObject"; ActionType["StartWatchK8sList"] = "startWatchK8sList"; ActionType["ModifyObject"] = "modifyObject"; ActionType["StopWatchK8s"] = "stopWatchK8s"; ActionType["Errored"] = "errored"; ActionType["Loaded"] = "loaded"; ActionType["BulkAddToList"] = "bulkAddToList"; ActionType["UpdateListFromWS"] = "updateListFromWS"; ActionType["FilterList"] = "filterList"; })(ActionType || (ActionType = {})); export const updateListFromWS = (id, k8sObjects) => action(ActionType.UpdateListFromWS, { id, k8sObjects }); export const loaded = (id, k8sObjects) => action(ActionType.Loaded, { id, k8sObjects }); export const bulkAddToList = (id, k8sObjects) => action(ActionType.BulkAddToList, { id, k8sObjects }); export const startWatchK8sObject = (id) => action(ActionType.StartWatchK8sObject, { id }); export const startWatchK8sList = (id, query) => action(ActionType.StartWatchK8sList, { id, query }); export const modifyObject = (id, k8sObjects) => action(ActionType.ModifyObject, { id, k8sObjects }); export const stopWatchK8s = (id) => action(ActionType.StopWatchK8s, { id }); export const errored = (id, k8sObjects) => action(ActionType.Errored, { id, k8sObjects }); export const filterList = (id, name, value) => action(ActionType.FilterList, { id, name, value }); export const partialObjectMetadataListHeader = { Accept: 'application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io,application/json', }; export const partialObjectMetadataHeader = { Accept: 'application/json;as=PartialObjectMetadata;v=v1;g=meta.k8s.io,application/json', }; const WS = {}; const POLLs = {}; const REF_COUNTS = {}; const paginationLimit = 250; export const stopK8sWatch = (id) => (dispatch) => { REF_COUNTS[id] -= 1; if (REF_COUNTS[id] > 0) { return _.noop; } const ws = WS[id]; if (ws) { ws.destroy(); delete WS[id]; } const poller = POLLs[id]; clearInterval(poller); delete POLLs[id]; delete REF_COUNTS[id]; return dispatch(stopWatchK8s(id)); }; export const watchK8sList = (id, query, k8skind, extraAction, partialMetadata = false) => (dispatch, getState) => { // Only one watch per unique list ID if (id in REF_COUNTS) { REF_COUNTS[id] += 1; return _.noop; } dispatch(startWatchK8sList(id, query)); REF_COUNTS[id] = 1; const incrementallyLoad = async (continueToken = '') => { // the list may not still be around... if (!REF_COUNTS[id]) { // let .then handle the cleanup return null; } const requestOptions = partialMetadata ? { headers: partialObjectMetadataListHeader, } : {}; const response = await k8sList(k8skind, { limit: paginationLimit, ...query, ...(continueToken ? { continue: continueToken } : {}), }, true, requestOptions); if (!REF_COUNTS[id]) { return null; } if (!continueToken) { [loaded, extraAction].forEach((f) => f && dispatch(f(id, response.items))); } else { dispatch(bulkAddToList(id, response.items)); } if (response.metadata.continue) { return incrementallyLoad(response.metadata.continue); } return response.metadata.resourceVersion; }; /** * Incrementally fetch list (XHR) using k8s pagination then use its resourceVersion to * start listening on a WS (?resourceVersion=$resourceVersion) * start the process over when: * 1. the WS closes abnormally * 2. the WS can not establish a connection within $TIMEOUT */ const pollAndWatch = async () => { delete POLLs[id]; try { const resourceVersion = await incrementallyLoad(); // ensure this watch should still exist because pollAndWatch is recursiveish if (!REF_COUNTS[id]) { // eslint-disable-next-line no-console console.log(`stopped watching ${id} before finishing incremental loading.`); // call cleanup function out of abundance of caution... dispatch(stopK8sWatch(id)); return; } if (WS[id]) { // eslint-disable-next-line no-console console.warn(`Attempted to create multiple websockets for ${id}.`); return; } if (!_.get(k8skind, 'verbs', ['watch']).includes('watch')) { // eslint-disable-next-line no-console console.warn(`${getReferenceForModel(k8skind)} does not support watching, falling back to polling.`); if (!POLLs[id]) { POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); } return; } const { subprotocols } = getImpersonate(getState()) || {}; WS[id] = k8sWatch(k8skind, { ...query, resourceVersion }, { subprotocols, timeout: 60 * 1000 }); } catch (e) { if (!REF_COUNTS[id]) { // eslint-disable-next-line no-console console.log(`stopped watching ${id} before finishing incremental loading with error ${e}!`); // call cleanup function out of abundance of caution... dispatch(stopK8sWatch(id)); return; } dispatch(errored(id, e)); if (!POLLs[id]) { POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); } return; } WS[id] .onclose((event) => { // Close Frame Status Codes: https://tools.ietf.org/html/rfc6455#section-7.4.1 if (event.code !== 1006) { return; } // eslint-disable-next-line no-console console.log('WS closed abnormally'); const ws = WS[id]; const timedOut = true; ws && ws.destroy(timedOut); }) .ondestroy((timedOut) => { if (!timedOut) { return; } // If the WS is unsuccessful for timeout duration, assume it is less work // to update the entire list and then start the WS again // eslint-disable-next-line no-console console.log(`WS ${id} timed out - restarting polling`); delete WS[id]; if (POLLs[id]) { return; } POLLs[id] = window.setTimeout(pollAndWatch, 15 * 1000); }) .onbulkmessage((events) => [updateListFromWS, extraAction].forEach((f) => f && dispatch(f(id, events)))); }; return pollAndWatch(); }; export const watchK8sObject = (id, name, namespace, query, k8sModel, partialMetadata = false) => (dispatch, getState) => { if (id in REF_COUNTS) { REF_COUNTS[id] += 1; return _.noop; } const watch = dispatch(startWatchK8sObject(id)); REF_COUNTS[id] = 1; const requestOptions = partialMetadata ? { headers: partialObjectMetadataHeader, } : {}; const poller = () => { k8sGet(k8sModel, name, namespace, {}, requestOptions) .then((o) => dispatch(modifyObject(id, o)), (e) => dispatch(errored(id, e))) .catch((err) => { // eslint-disable-next-line no-console console.log(err); }); }; POLLs[id] = window.setInterval(poller, 30 * 1000); poller(); if (!_.get(k8sModel, 'verbs', ['watch']).includes('watch')) { // eslint-disable-next-line no-console console.warn(`${getReferenceForModel(k8sModel)} does not support watching`); return _.noop; } // Validate that a namespace is provided when watching a singular namespaced object. Must happen // on frontend since we use field selectors against the list endpoint to watch singular resources. if (k8sModel.namespaced && query.name && !query.ns) { // eslint-disable-next-line no-console console.error('Namespace required to watch namespaced resource: ', k8sModel.kind, query.name); return _.noop; } if (query.name) { query.fieldSelector = `metadata.name=${query.name}`; delete query.name; } const { subprotocols } = getImpersonate(getState()) || {}; WS[id] = k8sWatch(k8sModel, query, { subprotocols, }).onbulkmessage((events) => events.forEach((e) => dispatch(modifyObject(id, e.object)))); return watch; }; export const receivedResources = (resources) => action(ActionType.ReceivedResources, { resources }); export const getResourcesInFlight = () => action(ActionType.GetResourcesInFlight); const k8sActions = { startWatchK8sObject, startWatchK8sList, modifyObject, stopWatchK8s, errored, loaded, bulkAddToList, updateListFromWS, filterList, receivedResources, getResourcesInFlight, };