mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 15:46:43 +00:00
feat: add Docker label audit across Fleet and Stack views (#1531)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sourcesPresent, matchesSearch, type LabelSource } from '@/lib/labelInventory';
|
||||
|
||||
describe('sourcesPresent', () => {
|
||||
it('returns distinct sources in the fixed display order regardless of input order', () => {
|
||||
const input: LabelSource[] = ['runtime', 'unknown', 'compose', 'image', 'compose-system'];
|
||||
expect(sourcesPresent(input)).toEqual(['compose', 'image', 'runtime', 'compose-system', 'unknown']);
|
||||
});
|
||||
|
||||
it('de-duplicates and omits sources not present', () => {
|
||||
expect(sourcesPresent(['runtime', 'runtime', 'image'])).toEqual(['image', 'runtime']);
|
||||
expect(sourcesPresent([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesSearch', () => {
|
||||
it('matches when any part contains the (case-insensitive) query', () => {
|
||||
expect(matchesSearch('WATCH', 'com.centurylinklabs.watchtower.enable')).toBe(true);
|
||||
expect(matchesSearch('nope', 'traefik.enable', 'true')).toBe(false);
|
||||
});
|
||||
|
||||
it('treats an empty or whitespace query as matching everything', () => {
|
||||
expect(matchesSearch('', 'anything')).toBe(true);
|
||||
expect(matchesSearch(' ', null)).toBe(true);
|
||||
});
|
||||
|
||||
it('trims the query and tolerates null/undefined parts', () => {
|
||||
expect(matchesSearch(' enable ', 'traefik.enable')).toBe(true);
|
||||
expect(matchesSearch('x', null, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ export const CAPABILITIES = [
|
||||
'update-guard',
|
||||
'compose-networking',
|
||||
'env-inventory',
|
||||
'container-label-inventory',
|
||||
'project-env-files',
|
||||
'compose-storage',
|
||||
'cross-node-rbac',
|
||||
|
||||
@@ -42,6 +42,7 @@ export type FleetTab =
|
||||
| 'snapshots'
|
||||
| 'configuration'
|
||||
| 'dependencies'
|
||||
| 'container-labels'
|
||||
| 'deployments'
|
||||
| 'routing'
|
||||
| 'federation'
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
export type LabelSource = 'compose' | 'runtime' | 'image' | 'compose-system' | 'unknown';
|
||||
|
||||
export interface LabelValue {
|
||||
key: string;
|
||||
value: string;
|
||||
source: LabelSource;
|
||||
redacted?: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerLabelRow {
|
||||
id: string;
|
||||
name: string;
|
||||
stack: string | null;
|
||||
service: string | null;
|
||||
state: string;
|
||||
labels: LabelValue[];
|
||||
}
|
||||
|
||||
export interface LabelIndexContainerRef {
|
||||
id: string;
|
||||
name: string;
|
||||
stack: string | null;
|
||||
service: string | null;
|
||||
nodeId?: number;
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
export interface LabelIndexRow {
|
||||
key: string;
|
||||
value: string;
|
||||
redacted?: boolean;
|
||||
source: LabelSource;
|
||||
containers: LabelIndexContainerRef[];
|
||||
}
|
||||
|
||||
export interface FleetLabelInventoryResponse {
|
||||
nodes: Array<{
|
||||
nodeId: number;
|
||||
nodeName: string;
|
||||
status: 'ok' | 'error';
|
||||
inventory: {
|
||||
nodeId: number;
|
||||
containers: ContainerLabelRow[];
|
||||
byLabel: LabelIndexRow[];
|
||||
partial: boolean;
|
||||
generatedAt: number;
|
||||
} | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
aggregatedByLabel: LabelIndexRow[];
|
||||
nodeErrors: Record<number, string>;
|
||||
generatedAt: number;
|
||||
}
|
||||
|
||||
export interface StackLabelReplica {
|
||||
id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
runtimeLabels: LabelValue[];
|
||||
onlyInCompose: string[];
|
||||
onlyOnContainer: string[];
|
||||
inBoth: string[];
|
||||
// Optional so older nodes (pre-provenance) still typecheck during mixed-version operation.
|
||||
changed?: string[];
|
||||
inspectFailed?: boolean;
|
||||
}
|
||||
|
||||
export interface StackServiceLabelRow {
|
||||
service: string;
|
||||
declaredLabels: LabelValue[];
|
||||
replicas: StackLabelReplica[];
|
||||
}
|
||||
|
||||
export interface StackLabelInventory {
|
||||
stackName: string;
|
||||
renderable: boolean;
|
||||
services: StackServiceLabelRow[];
|
||||
// Optional so older nodes still typecheck during mixed-version operation.
|
||||
partial?: boolean;
|
||||
generatedAt: number;
|
||||
}
|
||||
|
||||
export const LABEL_DISAMBIGUATION_COPY =
|
||||
'Docker labels are metadata declared on Compose services or attached to running containers. They are different from Sencho Stack Labels used for organizing stacks and Node Labels used for Blueprint placement.';
|
||||
|
||||
export const SOURCE_LABELS: Record<LabelSource, string> = {
|
||||
compose: 'Declared in Compose',
|
||||
runtime: 'Present at runtime',
|
||||
image: 'Image',
|
||||
'compose-system': 'Docker Compose system label',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
|
||||
/** Concise facet-chip labels (concept-aligned with SOURCE_LABELS, not a rename of the badges). */
|
||||
export const SOURCE_FACET_LABELS: Record<LabelSource, string> = {
|
||||
compose: 'Compose File',
|
||||
runtime: 'Runtime',
|
||||
image: 'Image',
|
||||
'compose-system': 'System',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
|
||||
/** Stable display order for source facets. */
|
||||
const SOURCE_ORDER: LabelSource[] = ['compose', 'image', 'runtime', 'compose-system', 'unknown'];
|
||||
|
||||
/** The distinct sources present, in stable display order, for building data-driven facet rows. */
|
||||
export function sourcesPresent(sources: Iterable<LabelSource>): LabelSource[] {
|
||||
const present = new Set(sources);
|
||||
return SOURCE_ORDER.filter(s => present.has(s));
|
||||
}
|
||||
|
||||
/** Case-insensitive substring match across a variable list of fields; empty query matches all. */
|
||||
export function matchesSearch(query: string, ...parts: (string | null | undefined)[]): boolean {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
return parts.some(p => (p ?? '').toLowerCase().includes(q));
|
||||
}
|
||||
Reference in New Issue
Block a user