@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
JavaScript
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,
};