mirror of https://github.com/rancher/dashboard.git
274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
/**
|
|
* This is the main dynamic content file that provides the 'fetchAndProcessDynamicContent' function
|
|
*
|
|
* This is the main entry point for reading and processing dynamic content
|
|
*/
|
|
|
|
import day from 'dayjs';
|
|
import * as jsyaml from 'js-yaml';
|
|
import semver from 'semver';
|
|
import { isAdminUser } from '@shell/store/type-map';
|
|
import { getVersionData } from '@shell/config/version';
|
|
import { processReleaseVersion } from './new-release';
|
|
import { processSupportNotices } from './support-notice';
|
|
import { Context, DynamicContent, VersionInfo } from './types';
|
|
import { createLogger, LOCAL_STORAGE_CONTENT_DEBUG_LOG } from './util';
|
|
import { getConfig } from './config';
|
|
import { SystemInfoProvider } from './info';
|
|
|
|
const FETCH_DELAY = 3 * 1000; // Short delay to let UI settle before we fetch the updates document
|
|
const FETCH_REQUEST_TIMEOUT = 15000; // Time out the request after 15 seconds
|
|
const FETCH_CONCURRENT_SECONDS = 30; // Time to wait to ignore another in-progress fetch (seconds)
|
|
|
|
export const UPDATE_DATE_FORMAT = 'YYYY-MM-DD'; // Format of the fetch date
|
|
|
|
const LOCAL_STORAGE_UPDATE_FETCH_DATE = 'rancher-updates-fetch-next'; // Local storage setting that holds the date when we should next try and fetch content
|
|
const LOCAL_STORAGE_UPDATE_CONTENT = 'rancher-updates-last-content'; // Local storage setting that holds the last fetched content
|
|
const LOCAL_STORAGE_UPDATE_ERRORS = 'rancher-updates-fetch-errors'; // Local storage setting that holds the count of contiguous errors
|
|
const LOCAL_STORAGE_UPDATE_FETCHING = 'rancher-updates-fetching'; // Local storage setting that holds the date and time of the last fetch that was started
|
|
|
|
const BACKOFFS = [1, 1, 1, 2, 2, 3, 5]; // Backoff in days for the contiguous number of errors (i.e. after 1 errors, we wait 1 day, after 3 errors, we wait 2 days, etc.)
|
|
|
|
const DEFAULT_RELEASE_NOTES_URL = 'https://github.com/rancher/rancher/releases/tag/v$version'; // Default release notes URL
|
|
|
|
/**
|
|
* Fetch dynamic content if needed and process it if it has changed since we last checked
|
|
*/
|
|
export async function fetchAndProcessDynamicContent(dispatch: Function, getters: any, axios: any) {
|
|
// Check that the product is Rancher
|
|
// => Check that we are NOT in single product mode (e.g. Harvester)
|
|
const isSingleProduct = getters['isSingleProduct'];
|
|
|
|
if (!!isSingleProduct) {
|
|
return;
|
|
}
|
|
|
|
const config = getConfig(getters);
|
|
|
|
// If not enabled via the configuration, then just return
|
|
if (!config.enabled) {
|
|
console.log('Dynamic content disabled through configuration'); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
|
|
const logger = createLogger(config);
|
|
|
|
// Common context to pass through to functions for store access, logging, etc
|
|
const context: Context = {
|
|
dispatch,
|
|
getters,
|
|
axios,
|
|
logger,
|
|
config,
|
|
isAdmin: isAdminUser(getters),
|
|
settings: {
|
|
releaseNotesUrl: DEFAULT_RELEASE_NOTES_URL,
|
|
suseExtensions: [],
|
|
}
|
|
};
|
|
|
|
logger.debug('Read configuration', context.config);
|
|
|
|
try {
|
|
// Fetch the dynamic content if required, otherwise return the cached content or empty object if no content available
|
|
const content = await fetchDynamicContent(context);
|
|
|
|
// Version metadata
|
|
const versionData = getVersionData();
|
|
const version = semver.coerce(versionData.Version);
|
|
|
|
if (!version || !content) {
|
|
return;
|
|
}
|
|
|
|
const versionInfo: VersionInfo = {
|
|
version,
|
|
isPrime: config.prime,
|
|
};
|
|
|
|
// If not logging, then clear out any log data from local storage
|
|
if (!config.log) {
|
|
window.localStorage.removeItem(LOCAL_STORAGE_CONTENT_DEBUG_LOG);
|
|
}
|
|
|
|
if (content?.settings) {
|
|
// Update the settings data from the content, so that it is has the settings with their defaults or values from the dynamic content payload
|
|
context.settings = {
|
|
...context.settings,
|
|
...content.settings
|
|
};
|
|
}
|
|
|
|
// If the cached content has a debug version then use that as an override for the current version number
|
|
// This is only for debug and testing purposes
|
|
if (content.settings?.debugVersion) {
|
|
versionInfo.version = semver.coerce(content.settings.debugVersion);
|
|
logger.debug(`Overriding version number to ${ content.settings.debugVersion }`);
|
|
}
|
|
|
|
// We always process the content in case the Rancher version has changed or the date means that an announcement/notification should now be shown
|
|
|
|
// New release notifications and support notifications are shown to ALL community users, but only to admin users when Prime
|
|
if (!config.prime || context.isAdmin) {
|
|
// New release notifications
|
|
processReleaseVersion(context, content.releases, versionInfo);
|
|
|
|
// EOM, EOL notifications
|
|
processSupportNotices(context, content.support, versionInfo);
|
|
}
|
|
} catch (e) {
|
|
logger.error('Error reading or processing dynamic content', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We use a signal to timeout the connection
|
|
* For air-gapped environments, this ensures the request will timeout after FETCH_REQUEST_TIMEOUT
|
|
* This timeout is set relatively low (15s). The default, otherwise, is 2 minutes.
|
|
*
|
|
* @param timeoutMs Time in milliseconds after which the abort signal should signal
|
|
*/
|
|
function newRequestAbortSignal(timeoutMs: number) {
|
|
const abortController = new AbortController();
|
|
|
|
setTimeout(() => abortController.abort(), timeoutMs || 0);
|
|
|
|
return abortController.signal;
|
|
}
|
|
|
|
/**
|
|
* Update the local storage data that tracks when to next fetch content and how many consecutive errors we have had
|
|
*
|
|
* @param didError Indicates if we should update to record content retrieved without error or with error
|
|
*/
|
|
function updateFetchInfo(didError: boolean) {
|
|
if (!didError) {
|
|
// No error, so check again tomorrow and remove the backoff setting, so it will get its default next time
|
|
const nextFetch = day().add(1, 'day');
|
|
const nextFetchString = nextFetch.format(UPDATE_DATE_FORMAT);
|
|
|
|
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCH_DATE, nextFetchString);
|
|
window.localStorage.removeItem(LOCAL_STORAGE_UPDATE_ERRORS);
|
|
} else {
|
|
// Did error, read the backoff, increase and add to the date
|
|
const contiguousErrorsString = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_ERRORS) || '0';
|
|
|
|
let contiguousErrors = parseInt(contiguousErrorsString, 10);
|
|
|
|
// Increase the number of errors that have happened in a row
|
|
contiguousErrors++;
|
|
|
|
// Once we reach the max backoff, just stick with it
|
|
if (contiguousErrors >= BACKOFFS.length ) {
|
|
contiguousErrors = BACKOFFS.length - 1;
|
|
}
|
|
|
|
// Now find the backoff (days) given the error count and calculate the date of the next fetch
|
|
const daysToAdd = BACKOFFS[contiguousErrors];
|
|
const nextFetch = day().add(daysToAdd, 'day');
|
|
const nextFetchString = nextFetch.format(UPDATE_DATE_FORMAT);
|
|
|
|
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCH_DATE, nextFetchString);
|
|
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_ERRORS, contiguousErrors.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch dynamic content (if needed)
|
|
*/
|
|
export async function fetchDynamicContent(context: Context): Promise<Partial<DynamicContent> | undefined> {
|
|
const { getters, logger, config } = context;
|
|
|
|
// Check if we already have done an update check today
|
|
let content: Partial<DynamicContent> = {};
|
|
|
|
try {
|
|
const today = day();
|
|
const todayString = today.format(UPDATE_DATE_FORMAT);
|
|
const nextFetch = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_FETCH_DATE) || todayString;
|
|
|
|
// Read the cached content from local storage if possible
|
|
content = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_UPDATE_CONTENT) || '{}');
|
|
|
|
const nextFetchDay = day(nextFetch);
|
|
|
|
// Just in case next day gets reset to the past or corrupt, otherwise next fetch needs to not be in the future
|
|
if (!nextFetchDay.isValid() || !nextFetchDay.isAfter(today)) {
|
|
logger.info(`Performing update check on ${ todayString }`);
|
|
logger.debug(`Performing update check on ${ todayString }`);
|
|
|
|
const activeFetch = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_FETCHING);
|
|
|
|
if (activeFetch) {
|
|
const activeFetchDate = day(activeFetch);
|
|
|
|
if (activeFetchDate.isValid() && today.diff(activeFetchDate, 'second') < FETCH_CONCURRENT_SECONDS) {
|
|
logger.debug('Already fetching dynamic content in another tab (or previous tab closed while fetching) - skipping');
|
|
|
|
return content;
|
|
}
|
|
}
|
|
|
|
// Set the local storage key that indicates a tab is fetching the content - prevents other tabs doing so at the same time
|
|
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCHING, today.toString());
|
|
|
|
// Wait a short while before fetching dynamic content
|
|
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY));
|
|
|
|
const systemData = new SystemInfoProvider(getters, content?.settings || {});
|
|
const qs = systemData.buildQueryString();
|
|
const distribution = config.prime ? 'prime' : 'community';
|
|
const url = `${ config.endpoint.replace('$dist', distribution) }?${ qs }`;
|
|
|
|
logger.debug(`Fetching dynamic content from: ${ url.split('?')[0] }`, url);
|
|
|
|
// We use axios directly so that we can pass in the abort signal to implement the connection timeout
|
|
const res = await context.axios({
|
|
url,
|
|
method: 'get',
|
|
timeout: FETCH_REQUEST_TIMEOUT,
|
|
noApiCsrf: true,
|
|
withCredentials: false,
|
|
signal: newRequestAbortSignal(FETCH_REQUEST_TIMEOUT),
|
|
responseType: 'text' // We always want the raw text back - otherwise YAML gives text and JSON gives object
|
|
});
|
|
|
|
// The data should be YAML (or JSON) in the 'data' attribute
|
|
if (res?.data) {
|
|
try {
|
|
content = jsyaml.load(res.data) as any;
|
|
|
|
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_CONTENT, JSON.stringify(content));
|
|
|
|
// Update the last date now
|
|
updateFetchInfo(false);
|
|
} catch (e) {
|
|
logger.error('Failed to parse YAML/JSON from dynamic content package', e);
|
|
}
|
|
} else {
|
|
logger.error('Error fetching dynamic content package (unexpected data)');
|
|
}
|
|
} else {
|
|
logger.info(`Skipping update check for dynamic content - next check due on ${ nextFetch } (today is ${ todayString })`);
|
|
|
|
// If debug mode, then wait a bit to simulate the delay we would have had if we were fetching
|
|
if (config.debug) {
|
|
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.error('Error occurred reading dynamic content', e);
|
|
|
|
// We had an error, so update data in local storage so that we try again appropriately next time
|
|
updateFetchInfo(true);
|
|
}
|
|
|
|
logger.debug('End fetchDynamicContent');
|
|
|
|
// Remove the local storage key that indicates a tab is fetching the content
|
|
window.localStorage.removeItem(LOCAL_STORAGE_UPDATE_FETCHING);
|
|
|
|
return content;
|
|
}
|