vault/ui/mirage/handlers/clients.js
lane-wetmore ddd34fdbc5
UI: CE Datepicker (#30488)
* CE date picker and header changes

* update test

* add changelog and update empty component css

* update modal values from args

* update modal copy

* header changes to add docs and move edit button

* move + update test

* Update ui/app/components/clients/date-range.ts

Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>

* Update ui/app/components/clients/date-range.ts

Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>

* refactor modal state and update docs

* update tests

* update modal copy

* Update changelog/30488.txt

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>

* update date parsing, docs link copy and tidy

* update var name for clarity

* add date range test to check for disallowing current month selection

* update tests to reflect disallowal of current month (or beyond) date selection

* move ce header into page header comp and update tests

---------

Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
2025-05-09 12:26:43 -05:00

391 lines
13 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import {
addMonths,
differenceInCalendarMonths,
endOfMonth,
formatRFC3339,
fromUnixTime,
isAfter,
isBefore,
isSameMonth,
isWithinInterval,
startOfMonth,
subMonths,
} from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { CLIENT_TYPES } from 'core/utils/client-count-utils';
/*
HOW TO ADD NEW TYPES:
1. add key to CLIENT_TYPES
2. Find "ADD NEW CLIENT TYPES HERE" comment below and add type to destructuring array
3. Add generateMounts() for that client type to the mounts array
*/
export const LICENSE_START = new Date('2023-07-02T00:00:00Z');
export const STATIC_NOW = new Date('2024-01-25T23:59:59Z');
export const STATIC_PREVIOUS_MONTH = new Date('2023-12-25T23:59:59Z');
const COUNTS_START = subMonths(STATIC_NOW, 12); // user started Vault cluster on 2023-01-25
// upgrade happened 2 month after license start
export const UPGRADE_DATE = addMonths(LICENSE_START, 2); // monthly attribution added
// exported so that tests not using this scenario can use the same response
export const CONFIG_RESPONSE = {
request_id: 'some-config-id',
data: {
billing_start_timestamp: formatRFC3339(LICENSE_START),
enabled: 'default-enabled',
minimum_retention_months: 48,
queries_available: false,
reporting_enabled: true,
retention_months: 48,
},
};
// --------- FOR DATA GENERATION
function getSum(array, key) {
return array.reduce((sum, { counts }) => sum + counts[key], 0);
}
function getTotalCounts(array) {
return CLIENT_TYPES.reduce((obj, key) => {
obj[key] = getSum(array, key);
return obj;
}, {});
}
function randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function generateMountBlock(path, counts) {
const baseObject = CLIENT_TYPES.reduce((obj, key) => {
obj[key] = 0;
return obj;
}, {});
return {
mount_path: path,
counts: {
...baseObject,
// object contains keys for which 0-values of base object to overwrite
...counts,
},
};
}
function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts = false) {
const min = isLowerCounts ? 10 : 50;
const max = isLowerCounts ? 100 : 1000;
const nsBlock = {
namespace_id: ns?.namespace_id || (idx === 0 ? 'root' : Math.random().toString(36).slice(2, 7) + idx),
namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns${idx}`),
counts: {},
mounts: {},
};
if (skipCounts) return nsBlock; // skip counts to generate empty ns block with namespace ids and paths
// generates one mount per client type
const mountsArray = (idx) => {
// * ADD NEW CLIENT TYPES HERE and pass to a new generateMountBlock() function below
const [acme_clients, entity_clients, non_entity_clients, secret_syncs] = CLIENT_TYPES.map(() =>
randomBetween(min, max)
);
// each mount type generates a different type of client
return [
generateMountBlock(`auth/authid/${idx}`, {
clients: non_entity_clients + entity_clients,
non_entity_clients,
entity_clients,
}),
generateMountBlock(`kvv2-engine-${idx}`, { clients: secret_syncs, secret_syncs }),
generateMountBlock(`pki-engine-${idx}`, { clients: acme_clients, acme_clients }),
];
};
// two mounts per client type for more varied mock data
const mounts = [...mountsArray(0), ...mountsArray(1)];
mounts.sort((a, b) => b.counts.clients - a.counts.clients);
nsBlock.mounts = mounts;
nsBlock.counts = getTotalCounts(mounts);
return nsBlock;
}
function generateMonths(startDate, endDate, namespaces) {
const startDateObject = parseAPITimestamp(startDate);
const endDateObject = parseAPITimestamp(endDate);
const numberOfMonths = differenceInCalendarMonths(endDateObject, startDateObject) + 1;
const months = [];
// only generate monthly block if queried dates span or follow upgrade to 1.10
const upgradeWithin = isWithinInterval(UPGRADE_DATE, { start: startDateObject, end: endDateObject });
const upgradeAfter = isAfter(startDateObject, UPGRADE_DATE);
if (upgradeWithin || upgradeAfter) {
for (let i = 0; i < numberOfMonths; i++) {
const month = addMonths(startOfMonth(startDateObject), i);
const hasNoData = isBefore(month, UPGRADE_DATE) && !isSameMonth(month, UPGRADE_DATE);
if (hasNoData) {
months.push({
timestamp: formatRFC3339(month),
counts: null,
namespaces: null,
new_clients: null,
});
continue;
}
const monthNs = namespaces.map((ns, idx) => generateNamespaceBlock(idx, false, ns));
const newClients = namespaces.map((ns, idx) => generateNamespaceBlock(idx, true, ns));
months.push({
timestamp: formatRFC3339(month),
counts: getTotalCounts(monthNs),
namespaces: monthNs.sort((a, b) => b.counts.clients - a.counts.clients),
new_clients: {
counts: getTotalCounts(newClients),
namespaces: newClients.sort((a, b) => b.counts.clients - a.counts.clients),
},
});
}
}
return months;
}
function generateActivityResponse(startDate, endDate) {
let namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx, null, null, true));
const months = generateMonths(startDate, endDate, namespaces);
if (months.length) {
const monthlyCounts = months.filter((m) => m.counts);
// sum namespace counts from monthly data
namespaces.forEach((ns) => {
const nsData = monthlyCounts.map((d) =>
d.namespaces.find((n) => n.namespace_path === ns.namespace_path)
);
const mountCounts = nsData.flatMap((d) => d.mounts);
const paths = nsData[0].mounts.map(({ mount_path }) => mount_path);
ns.mounts = paths.map((path) => {
const counts = getTotalCounts(mountCounts.filter((m) => m.mount_path === path));
return { mount_path: path, counts };
});
ns.counts = getTotalCounts(nsData);
});
} else {
// if no monthly data due to upgrade stuff, generate counts
namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx));
}
namespaces.sort((a, b) => b.counts.clients - a.counts.clients);
return {
start_time: isAfter(new Date(startDate), COUNTS_START) ? startDate : formatRFC3339(COUNTS_START),
end_time: endDate,
by_namespace: namespaces,
months,
total: getTotalCounts(namespaces),
};
}
// --------- FOR MOCK FILTERING
/**
* Helper fn for calculating total counts based on array containing counts block
*/
function calcCounts(arr) {
return arr.reduce(
(prev, ns) => {
const base = ns.counts;
prev.entity_clients += base.entity_clients;
prev.non_entity_clients += base.non_entity_clients;
prev.clients += base.clients;
prev.secret_syncs += base.secret_syncs;
prev.acme_clients += base.acme_clients;
return prev;
},
{
entity_clients: 0,
non_entity_clients: 0,
clients: 0,
secret_syncs: 0,
acme_clients: 0,
}
);
}
/**
* Helper fn to filter namespaces to include the namespace itself, and any children
*/
function filterByNamespace(namespaces, namespacePath) {
// if we simply do a check for startsWith, filtering for `ns1` will include `ns11` as well as the desired `ns1/child`
return namespaces.filter(
(ns) => ns.namespace_path === namespacePath || ns.namespace_path.startsWith(`${namespacePath}/`)
);
}
/**
* Helper fn to filter months data from activity response
*/
function filterMonths(months, namespacePath) {
return months.map((month) => {
if (!month.namespaces) return month;
const newMonth = {
...month,
};
const filteredNs = filterByNamespace(month.namespaces, namespacePath);
const monthsCount = calcCounts(filteredNs);
if (month.new_clients?.namespaces) {
const filteredNewNs = filterByNamespace(month.new_clients.namespaces, namespacePath);
const newCount = calcCounts(filteredNewNs);
newMonth.new_clients.namespaces = filteredNewNs;
newMonth.new_clients.counts = newCount;
}
newMonth.namespaces = filteredNs;
newMonth.counts = monthsCount;
return newMonth;
});
}
/**
* Util to mock filter namespace data from the activity response, matching what the API does
*/
export function filterActivityResponse(originalData, namespacePath) {
// make a deep copy of the object so we don't mutate the original
const data = JSON.parse(JSON.stringify(originalData));
if (!namespacePath) return data;
const filteredMonths = filterMonths(data.months, namespacePath);
const filteredNs = filterByNamespace(data.by_namespace, namespacePath);
const filteredTotals = calcCounts(filteredNs);
return {
...data,
months: filteredMonths,
by_namespace: filteredNs,
total: filteredTotals,
};
}
// --------- SERVER FN
export default function (server) {
server.get('sys/license/status', function () {
return {
request_id: 'my-license-request-id',
data: {
autoloaded: {
license_id: 'my-license-id',
start_time: formatRFC3339(LICENSE_START),
expiration_time: formatRFC3339(endOfMonth(addMonths(STATIC_NOW, 6))),
},
},
};
});
server.get('sys/internal/counters/config', function () {
return CONFIG_RESPONSE;
});
server.get('/sys/internal/counters/activity', (schema, req) => {
const activities = schema['clients/activities'];
const namespace = req.requestHeaders['X-Vault-Namespace'];
let { start_time, end_time } = req.queryParams;
if (!start_time && !end_time) {
// if there are no date query params, the activity log default behavior
// queries from the builtin license start timestamp to the current month
start_time = LICENSE_START.toISOString();
end_time = STATIC_NOW.toISOString();
}
// backend returns a timestamp if given unix time, so first convert to timestamp string here
if (!start_time.includes('T')) start_time = fromUnixTime(start_time).toISOString();
if (!end_time.includes('T')) end_time = fromUnixTime(end_time).toISOString();
const record = activities.findBy({ start_time, end_time });
let data;
if (record) {
// if we already have data for the given start/end time, use that
data = {
start_time: record.start_time,
end_time: record.end_time,
by_namespace: record.by_namespace,
months: record.months,
total: record.total,
};
} else {
data = generateActivityResponse(start_time, end_time);
activities.create(data);
}
return {
request_id: 'some-activity-id',
lease_id: '',
renewable: false,
lease_duration: 0,
data: filterActivityResponse(data, namespace),
wrap_info: null,
warnings: req.queryParams.end_time
? null
: [
'Since this usage period includes both the current month and at least one historical month, counts returned in this usage period are an estimate. Client counts for this period will no longer be estimated at the start of the next month.',
],
auth: null,
};
});
// client counting has changed in different ways since 1.9 see link below for details
// https://developer.hashicorp.com/vault/docs/concepts/client-count/faq#client-count-faq
server.get('sys/version-history', function () {
return {
request_id: 'version-history-request-id',
data: {
keys: ['1.9.0', '1.9.1', '1.10.1', '1.10.3', '1.14.4', '1.16.0', '1.17.0'],
key_info: {
// entity/non-entity breakdown added
'1.9.0': {
// we don't currently use build_date, including for accuracy. it's only tracked in versions >= 1.11.0
build_date: null,
previous_version: null,
timestamp_installed: '2023-07-02T00:00:00Z',
},
'1.9.1': {
build_date: null,
previous_version: '1.9.0',
timestamp_installed: '2023-08-02T00:00:00Z',
},
// auth mount attribution added in 1.10.0
'1.10.1': {
build_date: null,
previous_version: '1.9.1',
timestamp_installed: '2023-09-02T00:00:00Z', // same as UPGRADE_DATE
},
'1.10.3': {
build_date: null,
previous_version: '1.10.1',
timestamp_installed: '2023-10-23T00:00:00Z',
},
// no notable UI changes
'1.14.4': {
build_date: '2023-11-02T00:00:00Z',
previous_version: '1.10.3',
timestamp_installed: '2023-11-02T00:00:00Z',
},
// sync clients added
'1.16.0': {
build_date: '2023-11-23T00:00:00Z',
previous_version: '1.14.4',
timestamp_installed: '2023-11-23T00:00:00Z',
},
// acme_clients separated from non-entity clients
'1.17.0': {
build_date: '2023-12-02T00:00:00Z',
previous_version: '1.16.0',
timestamp_installed: '2023-12-02T00:00:00Z',
},
},
},
};
});
}