department-of-veterans-affairs/vets-website

View on GitHub
src/platform/monitoring/DowntimeNotification/util/helpers.js

Summary

Maintainability
A
1 hr
Test Coverage
import camelCaseKeysRecursive from 'camelcase-keys-recursive';
import moment from 'moment';
import * as Sentry from '@sentry/browser';

import environment from 'platform/utilities/environment';
import ENVIRONMENTS from 'site/constants/environments';

import externalServiceStatus from '../config/externalServiceStatus';
import defaultExternalServices from '../config/externalServices';
/**
 * Derives downtime status based on a time range
 * @param {string|Date|Moment} startTime
 * @param {string|Date|Moment} endTime
 * @returns {string} A service status
 */
export function getStatusForTimeframe(startTime, endTime) {
  const now = moment();
  const hasStarted = now.isSameOrAfter(startTime);

  if (hasStarted) {
    // Check for indefinite downtime (null endTime) or that the endTime is in the future
    if (!endTime || now.isBefore(endTime)) {
      return externalServiceStatus.down;
    }
    // The downtime must be old and outdated. The API should filter these so this shouldn't happen.
    return externalServiceStatus.ok;
  }

  const startsWithinHour = now.add(1, 'hour').isSameOrAfter(startTime);
  if (startsWithinHour) return externalServiceStatus.downtimeApproaching;

  return externalServiceStatus.ok;
}

export function createGlobalMaintenanceWindow({
  startTime,
  endTime,
  externalServices = defaultExternalServices,
}) {
  return [
    {
      attributes: {
        externalService: 'global',
        startTime,
        endTime,
      },
    },
    ...Object.keys(externalServices).map(externalService => ({
      attributes: {
        externalService,
        startTime,
        endTime,
      },
    })),
  ];
}

/**
 * Creates a Map of downtime information using the "externalService" property as keys
 * @param {Array} maintenanceWindows The raw JSON data from the API
 * @returns {Map}
 */

export function createServiceMap(maintenanceWindows = []) {
  const serviceMap = new Map();

  // Maintenance windows should be sorted in ascending order
  // so that when a single externalService has multiple upcoming
  // maintenance windows, we can easily grab the one with the
  // earliest startTime and ignore any others that we encounter
  // Expected format for `attributes.startTime`: YYYY-MM-DDTHH:MM:SS.SSSZ
  const sortedMaintenanceWindows = maintenanceWindows.sort((a, b) => {
    const aStart = a.attributes.startTime;
    const bStart = b.attributes.startTime;

    return aStart.localeCompare(bStart);
  });

  for (const maintenanceWindow of sortedMaintenanceWindows) {
    const {
      attributes: {
        externalService,
        startTime: startTimeRaw,
        endTime: endTimeRaw,
        description,
      },
    } = maintenanceWindow;

    const startTime = moment(startTimeRaw);
    const endTime = endTimeRaw && moment(endTimeRaw);
    const status = getStatusForTimeframe(startTime, endTime);

    // For each externalService, we only care about the maintenance
    // window with the earliest startTime (the sorting above should
    // guarantee that the first one we encounter has the earliest
    // startTime)
    if (!serviceMap.has(externalService)) {
      serviceMap.set(externalService, {
        externalService,
        status,
        startTime,
        endTime,
        description,
      });
    }
  }

  return serviceMap;
}

/**
 * Determines the downtime with the soonest startTime by using a service map to look up downtime information for each service in a list of service names
 * @param {Map} serviceMap A Map as created by createServiceMap
 * @param {Array<string>} serviceNames A list of external services
 * @returns {object} A downtime object containing properties "externalService", "status", "startTime", and "endTime"
 */
export function getSoonestDowntime(serviceMap, serviceNames) {
  return serviceNames
    .map(serviceName => serviceMap.get(serviceName))
    .filter(service => !!service)
    .filter(service => service.status !== externalServiceStatus.ok)
    .reduce((mostUrgentService, service) => {
      if (!mostUrgentService) return service;
      return mostUrgentService.startTime.isBefore(service.startTime)
        ? mostUrgentService
        : service;
    }, null);
}

/**
 * Retrieves a list of global downtimes from a cached JSON file and gets the
 * downtime that includes the current time.
 *
 * The file is generated by a vets-api job that pulls the global maintenance
 * windows from PagerDuty every hour. The content is in JSON, structured as an
 * array of maintenance window objects with the follow attributes:
 * pagerduty_id, external_service, start_time, end_time, description
 *
 * @returns {object} A global downtime window that covers the current time
 *     if it exists and null if not
 */
export const getCurrentGlobalDowntime = (() => {
  const BUCKET_BASE_URL = 's3-us-gov-west-1.amazonaws.com';

  const MAINTENANCE_WINDOWS_SUBDOMAINS = Object.freeze({
    [ENVIRONMENTS.VAGOVDEV]: 'dev-va-gov-maintenance-windows',
    [ENVIRONMENTS.VAGOVSTAGING]: 'staging-va-gov-maintenance-windows',
    [ENVIRONMENTS.VAGOVPROD]: 'prod-va-gov-maintenance-windows',
  });

  const subdomain = MAINTENANCE_WINDOWS_SUBDOMAINS[environment.BUILDTYPE];

  const maintenanceWindowsUrl = subdomain
    ? `https://${subdomain}.${BUCKET_BASE_URL}/maintenance_windows.json`
    : null;

  const includesCurrentTime = ({ startTime, endTime }) =>
    moment().isAfter(startTime) && moment().isBefore(endTime);

  return async () => {
    try {
      const response = await fetch(maintenanceWindowsUrl);
      const data = camelCaseKeysRecursive(await response.json());
      return data.find(includesCurrentTime) || null;
    } catch (error) {
      Sentry.withScope(scope => {
        scope.setExtra('error', error);
        Sentry.captureMessage('Error fetching maintenance windows file');
      });
      return null;
    }
  };
})();