dashboard/shell/utils/fleet.ts

322 lines
9.0 KiB
TypeScript

import { isEmpty, isEqual } from 'lodash';
import {
BundleDeploymentResource,
BundleResourceKey,
BundleDeployment,
BundleDeploymentStatus,
Condition,
} from '@shell/types/resources/fleet';
import { mapStateToEnum, STATES_ENUM, STATES } from '@shell/plugins/dashboard-store/resource-class';
import { FLEET as FLEET_LABELS, CAPI } from '@shell/config/labels-annotations';
import { NAME as EXPLORER_NAME } from '@shell/config/product/explorer';
import { FleetDashboardState, FleetResourceState, Target, TargetMode } from '@shell/types/fleet';
import { FLEET, VIRTUAL_HARVESTER_PROVIDER } from '@shell/config/types';
interface Resource extends BundleDeploymentResource {
state: string,
}
type Labels = {
[key: string]: string,
}
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 Application {
excludeHarvesterRule = {
clusterSelector: {
matchExpressions: [{
key: CAPI.PROVIDER,
operator: 'NotIn',
values: [
VIRTUAL_HARVESTER_PROVIDER
],
}],
},
};
getTargetMode(targets: Target[], namespace: string): TargetMode {
if (namespace === 'fleet-local') {
return 'local';
}
if (!targets.length) {
return 'none';
}
let mode: TargetMode = 'all';
for (const target of targets) {
const {
clusterName,
clusterSelector,
clusterGroup,
clusterGroupSelector,
} = target;
if (clusterGroup || clusterGroupSelector) {
return 'advanced';
}
if (clusterName) {
mode = 'clusters';
}
if (!isEmpty(clusterSelector)) {
mode = 'clusters';
}
}
const normalized = [...targets].map((target) => {
delete target.name;
return target;
});
// Check if targets contains only harvester rule after name normalizing
if (isEqual(normalized, [this.excludeHarvesterRule])) {
mode = 'all';
}
return mode;
}
}
class Fleet {
resourceIcons = {
[FLEET.GIT_REPO]: 'icon icon-github',
[FLEET.HELM_OP]: 'icon icon-helm',
};
dashboardIcons = {
[FLEET.GIT_REPO]: 'icon icon-git',
[FLEET.HELM_OP]: 'icon icon-helm',
};
dashboardStates: FleetDashboardState[] = [
{
index: 0,
id: 'error',
label: 'Error',
color: '#F64747',
icon: 'icon icon-error',
stateBackground: 'bg-error'
},
{
index: 1,
id: 'warning',
label: 'Warning',
color: '#DAC342',
icon: 'icon icon-warning',
stateBackground: 'bg-warning'
},
{
index: 2,
id: 'success',
label: 'Active',
color: '#5D995D',
icon: 'icon icon-checkmark',
stateBackground: 'bg-success'
},
{
index: 3,
id: 'info',
label: 'Pending',
color: '#3d98d3',
icon: 'icon icon-warning',
stateBackground: 'bg-info'
},
];
Application = new Application();
GIT_HTTPS_REGEX = /^https?:\/\/github\.com\/(.*?)(\.git)?\/*$/;
GIT_SSH_REGEX = /^git@github\.com:.*\.git$/;
HTTP_REGEX = /^(https?:\/\/[^\s]+)$/;
OCI_REGEX = /^oci:\/\//;
quacksLikeAHash(str: string) {
if (str.match(/^[a-f0-9]{40,}$/i)) {
return true;
}
return false;
}
parseSSHUrl(url: string) {
const parts = (url || '').split(':');
const sshUserAndHost = parts[0];
const repoPath = parts[1]?.replace('.git', '');
return {
sshUserAndHost,
repoPath
};
}
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 }`;
}
detailLocation(r: Resource, mgmtClusterName: string): any {
return mapStateToEnum(r.state) === STATES_ENUM.MISSING ? undefined : {
name: `c-cluster-product-resource${ r.namespace ? '-namespace' : '' }-id`,
params: {
product: EXPLORER_NAME,
cluster: mgmtClusterName,
resource: this.resourceType(r),
namespace: r.namespace,
id: r.name,
},
};
}
/**
* 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));
}
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;
}
}
getResourcesDefaultState(labelGetter: (key: string, args: any, fallback: any) => Record<string, any>, stateKey: string): Record<string, FleetResourceState> {
return [
STATES_ENUM.READY,
STATES_ENUM.NOT_READY,
STATES_ENUM.WAIT_APPLIED,
STATES_ENUM.MODIFIED,
STATES_ENUM.MISSING,
STATES_ENUM.ORPHANED,
STATES_ENUM.UNKNOWN,
].reduce((acc: Record<string, any>, state) => {
acc[state] = {
count: 0,
color: STATES[state].color,
label: labelGetter(`${ stateKey }.${ state }`, null, STATES[state].label ),
status: state
};
return acc;
}, {});
}
getBundlesDefaultState(labelGetter: (key: string, args: any, fallback: any) => Record<string, any>, stateKey: string): Record<string, FleetResourceState> {
return [
STATES_ENUM.READY,
STATES_ENUM.INFO,
STATES_ENUM.WARNING,
STATES_ENUM.NOT_READY,
STATES_ENUM.ERROR,
STATES_ENUM.ERR_APPLIED,
STATES_ENUM.WAIT_APPLIED,
STATES_ENUM.UNKNOWN,
].reduce((acc: Record<string, any>, state) => {
acc[state] = {
count: 0,
color: STATES[state].color,
label: labelGetter(`${ stateKey }.${ state }`, null, STATES[state].label ),
status: state
};
return acc;
}, {});
}
getDashboardStateId(resource: { stateColor: string }): string {
return resource?.stateColor?.replace('text-', '') || 'warning';
}
getDashboardState(resource: { stateColor: string }): FleetDashboardState | {} {
const stateId = this.getDashboardStateId(resource);
return this.dashboardStates.find(({ id }) => stateId === id) || {};
}
}
const instance = new Fleet();
export default instance;