dashboard/shell/utils/fleet.ts

192 lines
6.2 KiB
TypeScript

import {
BundleDeploymentResource,
BundleResourceKey,
BundleDeployment,
BundleDeploymentStatus,
BundleStatus,
Condition,
} from '@shell/types/resources/fleet';
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
import { FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
interface Resource extends BundleDeploymentResource {
state: string,
}
type Labels = {
[key: string]: string,
}
interface StatesCounter { [state: string]: number }
function incr(counter: StatesCounter, state: string) {
if (!counter[state]) {
counter[state] = 0;
}
counter[state]++;
}
function resourceKey(r: BundleResourceKey): string {
return `${ r.kind }/${ r.namespace }/${ r.name }`;
}
function conditionIsTrue(conditions: Condition[] | undefined, type: string): boolean {
if (!conditions) {
return false;
}
return !!conditions.find((c) => c.type === type && c.status.toLowerCase() === 'true');
}
class Fleet {
resourceId(r: BundleResourceKey): string {
return r.namespace ? `${ r.namespace }/${ r.name }` : r.name;
}
/**
* resourceType normalizes APIVersion and Kind from a Resources into a single string
*/
resourceType(r: Resource): string {
// ported from https://github.com/rancher/fleet/blob/v0.10.0/internal/cmd/controller/grutil/resourcekey.go#L116-L128
const type = r.kind.toLowerCase();
if (!r.apiVersion || r.apiVersion === 'v1') {
return type;
}
return `${ r.apiVersion.split('/', 2)[0] }.${ type }`;
}
/**
* resourcesFromBundleDeploymentStatus extracts the list of resources deployed by a BundleDeployment
*/
resourcesFromBundleDeploymentStatus(status: BundleDeploymentStatus): Resource[] {
// status.resources includes of resources that were deployed by Fleet *and still exist in the cluster*
// Use a map to avoid `find` over and over again
const resources = (status?.resources || []).reduce((res, r) => {
res[resourceKey(r)] = Object.assign({ state: STATES_ENUM.READY }, r);
return res;
}, {} as { [resourceKey: string]: Resource });
const modified: Resource[] = [];
for (const r of status?.modifiedStatus || []) {
const state = r.missing ? STATES_ENUM.MISSING : r.delete ? STATES_ENUM.ORPHANED : STATES_ENUM.MODIFIED;
const found: Resource = resources[resourceKey(r)];
// Depending on the state, the same resource can appear in both fields
if (found) {
found.state = state;
} else {
modified.push(Object.assign({ state }, r));
}
}
for (const r of status?.nonReadyStatus || []) {
const state = r.summary?.state || STATES_ENUM.UNKNOWN;
const found: Resource = resources[resourceKey(r)];
if (found) {
found.state = state;
}
}
return modified.concat(Object.values(resources));
}
/**
* resourcesFromBundleStatus extracts the list of resources deployed by a Bundle
*/
resourcesFromBundleStatus(status: BundleStatus): Resource[] {
// The state of every resource is spread all over the bundle status.
// resourceKey contains one entry per resource AND cluster (built by Fleet from all the child BundleDeployments).
// However, those entries do not contain the cluster that they belong to, leading to duplicate entries
// 1. Fold resourceKey by using a unique key, initializing counters for multiple occurrences of the same resource
const resources = (status.resourceKey || []).reduce((res, r) => {
const k = resourceKey(r);
if (!res[k]) {
res[k] = { r, count: {} };
}
incr(res[k].count, STATES_ENUM.READY);
return res;
}, {} as { [resourceKey: string]: { r: BundleResourceKey, count: StatesCounter } });
// 2. Non-ready resources are counted differently and may also appear in resourceKey, depending on their state
for (const bundle of status.summary?.nonReadyResources || []) {
for (const r of bundle.modifiedStatus || []) {
const k = resourceKey(r);
if (!resources[k]) {
resources[k] = { r, count: {} };
}
if (r.missing) {
incr(resources[k].count, STATES_ENUM.MISSING);
} else if (r.delete) {
resources[k].count[STATES_ENUM.READY]--;
incr(resources[k].count, STATES_ENUM.ORPHANED);
} else {
resources[k].count[STATES_ENUM.READY]--;
incr(resources[k].count, STATES_ENUM.MODIFIED);
}
}
for (const r of bundle.nonReadyStatus || []) {
const k = resourceKey(r);
const state = r.summary?.state || STATES_ENUM.UNKNOWN;
resources[k].count[STATES_ENUM.READY]--;
incr(resources[k].count, state);
}
}
// 3. Unfold back to an array of resources for display
return Object.values(resources).reduce((res, e) => {
const { r, count } = e;
for (const state in count) {
for (let x = 0; x < count[state]; x++) {
res.push(Object.assign({ state }, r));
}
}
return res;
}, [] as Resource[]);
}
clusterIdFromBundleDeploymentLabels(labels?: Labels): string {
const clusterNamespace = labels?.[FLEET_LABELS.CLUSTER_NAMESPACE];
const clusterName = labels?.[FLEET_LABELS.CLUSTER];
return `${ clusterNamespace }/${ clusterName }`;
}
bundleIdFromBundleDeploymentLabels(labels?: Labels): string {
const bundleNamespace = labels?.[FLEET_LABELS.BUNDLE_NAMESPACE];
const bundleName = labels?.[FLEET_LABELS.BUNDLE_NAME];
return `${ bundleNamespace }/${ bundleName }`;
}
bundleDeploymentState(bd: BundleDeployment): string {
// Ported from https://github.com/rancher/fleet/blob/534dbfdd6f74caf97bccd4cf977e42c5009b2432/internal/cmd/controller/summary/summary.go#L89
if (bd.status?.appliedDeploymentId !== bd.spec.deploymentId) {
return conditionIsTrue(bd.status?.conditions, 'Deployed') ? STATES_ENUM.WAIT_APPLIED : STATES_ENUM.ERR_APPLIED;
} else if (!bd.status?.ready) {
return STATES_ENUM.NOT_READY;
} else if (bd.spec.deploymentId !== bd.spec.stagedDeploymentId) {
return STATES_ENUM.OUT_OF_SYNC;
} else if (!bd.status?.nonModified) {
return STATES_ENUM.MODIFIED;
} else {
return STATES_ENUM.READY;
}
}
}
const instance = new Fleet();
export default instance;