Compare commits

...

28 Commits

Author SHA1 Message Date
Chris Farhood d8d995308b Merge dev into uat (PR #39) — QA-approved promotion
Resolves add/add conflict in audit-ci.jsonc: both branches independently
added the CTO-approved allowlist (PRI-854); identical content, kept the
POSIX-compliant trailing newline from uat/main. Also adds trailing newline
to dual-approval.yaml (missed in dev commit 990c796).

Changes promoted from dev:
- .github/workflows/dual-approval.yaml: Promotion Gate workflow (uat+main trigger)
- audit-ci.jsonc: CTO-approved allowlist for 3 inherited dev-only CVEs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 04:32:16 +00:00
Chris Farhood 990c796d04 Add audit-ci.jsonc allowlist and fix trailing newline
audit-ci.jsonc: matches CTO-approved allowlist from PRI-854 (same
three dev-only CVEs from @kinvolk/headlamp-plugin transitive deps).
Required by shared plugin-ci.yaml (updated 2026-05-06).

dual-approval.yaml: add trailing newline per POSIX standard.
2026-05-14 04:28:08 +00:00
Chris Farhood d9aaf5a146 Fix promotion gate: add uat branch trigger, rename to Promotion Gate
Follows canonical pattern from headlamp-sealed-secrets-plugin.
The pull_request trigger now fires on [uat, main] so the promotion
gate check auto-runs on PR open/sync for dev→uat PRs, not just
on review events.
2026-05-14 04:09:48 +00:00
privilegedescalation-engineer[bot] 59f1519f66 chore(ci): add audit-ci allowlist for inherited @kinvolk/headlamp-plugin CVEs (PRI-855)
QA reviewed and approved. Adds audit-ci.jsonc with 3 CVE allowlist entries for dev-only dependencies.
2026-05-12 22:22:44 +00:00
privilegedescalation-ceo[bot] dedf6538c7 Merge pull request #26 from privilegedescalation/fix/elliptic-vulnerability-override
fix: override elliptic to patched version for GHSA-848j-6mx2-7j84
2026-05-05 18:40:42 +00:00
Chris Farhood 0af4939d8e chore: update pnpm lockfile for elliptic override
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 12:58:43 +00:00
Chris Farhood c24e96da97 fix: override elliptic to patched version for GHSA-848j-6mx2-7j84 2026-05-05 12:51:05 +00:00
privilegedescalation-ceo[bot] 4b26b97caf Merge pull request #15 from privilegedescalation/gandalf/fix-duplicate-deps-pnpm-overrides
fix: remove duplicate tar and undici from devDependencies (PRI-557)
2026-05-05 10:30:42 +00:00
privilegedescalation-ceo[bot] 5b5ed9897b Merge pull request #16 from privilegedescalation/gandalf/pri-589-cleanup
fix: add markdownlint config to resolve CI failures (PRI-589)
2026-05-05 10:30:37 +00:00
privilegedescalation-ceo[bot] f8c8b82e87 Merge pull request #17 from privilegedescalation/hugh/add-dual-approval-gate
add dual approval gate workflow
2026-05-05 10:30:31 +00:00
privilegedescalation-ceo[bot] 6aefdb00a8 Merge pull request #10 from privilegedescalation/chore/add-renovate-config
chore: add renovate.json extending org preset
2026-05-05 10:29:59 +00:00
privilegedescalation-ceo[bot] 5db792f0a7 Merge pull request #11 from privilegedescalation/release/v0.1.2
release: v0.1.2
2026-05-05 10:29:55 +00:00
privilegedescalation-ceo[bot] 413634a01e Merge pull request #12 from privilegedescalation/dev
docs: redirect headlamp install namespace to headlamp (PRI-439)
2026-05-05 10:29:51 +00:00
Chris Farhood e4d7a56547 add dual approval gate workflow
headlamp-argocd-plugin was missing the dual-approval (CTO + QA) gate
required by SDLC. Added identical workflow to all other plugin repos.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 04:54:58 +00:00
privilegedescalation-engineer[bot] 0e41bb649d fix: resolve markdownlint CI failures in headlamp-argocd-plugin (#9)
* Remove duplicate tar/undici from devDependencies (already in pnpm.overrides)

Consolidates dual override blocks by removing the duplicate entries
from devDependencies. These packages are already pinned via pnpm.overrides
and should not appear in devDependencies.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: add markdownlint config to resolve CI failures

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: sync pnpm-lock.yaml after removing tar and undici deps

The pnpm-lock.yaml was out of sync with package.json after tar and undici
were removed. Regenerated to resolve pnpm install failure in CI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-05 00:24:20 +00:00
Chris Farhood de8a20f99a fix: add markdownlint config to resolve CI failures (PRI-589)
Cherry-picked from PR #9 original commit, removing out-of-scope
tar/undici dependency changes that should not have been included.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 21:19:09 +00:00
privilegedescalation-engineer[bot] 320154f29b Cleanup: consolidate dual override blocks in package.json (#8)
Removed duplicate tar/undici devDeps (already pinned in pnpm.overrides), removed stale overrides.lodash block, regenerated lockfile. QA: privilegedescalation-qa  | CTO: privilegedescalation-cto  | CI: green 
2026-05-04 21:03:17 +00:00
Chris Farhood f0de1fa33a fix: remove duplicate tar and undici from devDependencies
Both packages are already pinned via pnpm.overrides and should not
appear in devDependencies. Removes duplicates introduced during lockfile
conflict resolution.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 20:10:40 +00:00
privilegedescalation-engineer[bot] 34f6e0e13b fix(ci): add dev branch to pull_request trigger
Aligns PR trigger with push trigger. QA approved (PRI-547), CTO approved, CI green.
2026-05-04 18:59:37 +00:00
privilegedescalation-engineer[bot] 557a00a758 fix: enable CI on feature branches and add workflow_dispatch (#13)
Fixes PRI-524. Changes push trigger from branches:[main] to branches:['**'] so CI fires on every branch. Adds workflow_dispatch for manual trigger. Adds permissions: contents: read for least-privilege hardening.

All gates clear: CI green, UAT correctly skipped (YAML-only), QA approved (Regina), CTO approved (Nancy).
2026-05-04 18:26:45 +00:00
Chris Farhood 827b4f31cc docs: confirm headlamp namespace audit (PRI-439)
Audit of headlamp-argocd-plugin for kube-system → headlamp namespace redirect.
No in-scope kube-system references found.

In-scope files audited (all clean):
- README.md: no install snippet referencing kube-system
- CLAUDE.md: no kube-system references
- artifacthub-pkg.yml: no kube-system references

Out-of-scope upstream-workload references verified untouched:
- ArgoCD server lives in 'argocd' namespace (upstream watched workload)
- Plugin install path is via Headlamp plugin manager (ArtifactHub), not Helm

No code/text changes required. PR opened for SDLC sign-off.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 08:31:11 +00:00
github-actions[bot] c648b43493 release: v0.1.2 2026-05-04 06:38:54 +00:00
Chris Farhood 01c37a85d7 chore: add renovate.json extending org preset
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 05:35:28 +00:00
privilegedescalation-engineer[bot] 730f7cbe54 fix: override lodash >=4.18.0 to patch code injection vulnerability (#7)
* fix: override lodash >=4.18.0 to patch code injection vulnerability

GHSA-r5fr-rjxr-66jc is a code injection vulnerability in lodash
below 4.18.0. The vulnerable transitive dependency comes through
@kinvolk/headlamp-plugin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Regenerate lockfile for lodash override

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-04 03:24:00 +00:00
privilegedescalation-ceo[bot] 59c176621f chore: add FUNDING.yml for GitHub Sponsors
Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
2026-04-22 18:52:47 +00:00
privilegedescalation-engineer[bot] e87b065821 feat: scaffold headlamp-argocd-plugin with standard plugin structure
Squash merge of PR #1. CI  | QA (Regina)  | CTO (Nancy) . Merged by CEO (Countess von Containerheim).
2026-04-22 13:41:13 +00:00
privilegedescalation-ceo[bot] 9d664fda45 feat(page-injections): ArgoCD section on Namespace and Deployment detail pages
Merging after full approval chain: CI , QA (Regina) , CTO (Nancy) . Injects ArgoCD status into Headlamp native Namespace and Deployment detail pages.
2026-04-22 09:35:26 +00:00
Test User bcbed693b1 feat(page-injections): inject ArgoCD info into Namespace and Deployment detail views
- Register detail view sections for Namespace and Deployment resource kinds
- NamespaceArgoSection: shows ArgoCD apps whose spec.destination.namespace matches
- DeploymentArgoBadge: shows ArgoCD app managing the deployment (via status.resources)
- 9 unit tests for matching logic (appsForNamespace, appsForDeployment)
- All checks pass: pnpm tsc, pnpm test (40/40), pnpm lint (0 errors)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 20:53:51 +00:00
16 changed files with 687 additions and 18 deletions
+1
View File
@@ -0,0 +1 @@
github_sponsors: [privilegedescalation]
+6 -2
View File
@@ -2,9 +2,13 @@ name: CI
on:
push:
branches: [main]
branches: ['**']
pull_request:
branches: [main]
branches: [main, dev]
workflow_dispatch:
permissions:
contents: read
jobs:
ci:
+20
View File
@@ -0,0 +1,20 @@
name: Promotion Gate
# Calls the shared promotion gate workflow.
# dev PRs: no gate (engineer self-merges).
# uat PRs: QA approval required.
# main PRs: UAT approval required (uat→main promotions).
on:
pull_request_review:
types: [submitted, dismissed]
pull_request:
branches: [uat, main]
types: [opened, reopened, synchronize]
jobs:
promotion-gate:
uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
secrets: inherit
with:
pr_number: ${{ github.event.pull_request.number }}
+53
View File
@@ -0,0 +1,53 @@
{
"config": {
// Line length — not enforced for docs with code examples
"MD013": false,
// First line heading — files use YAML frontmatter, not headings
"MD041": false,
// Emphasis as heading — common pattern for Option 1/2/3 sections
"MD036": false,
// No duplicate heading — changelog files repeat section names intentionally
"MD024": false,
// Fenced code language — not always applicable for diagram blocks
"MD040": false,
// Table column style — table alignment is visual, not semantic
"MD060": false,
// Ordered list item prefix — number resets are intentional in documents
"MD029": false,
// No inline HTML — each elements are valid in valid Markdown
"MD033": false,
// List marker space — spacing after list markers varies by editor
"MD030": false,
// Blanks around headings — not always needed in compact docs
"MD022": false,
// Blanks around lists — not always needed in compact docs
"MD032": false,
// Blanks around fences — not always needed between adjacent blocks
"MD031": false,
// Multiple blanks — editor artifacts, not semantic
"MD012": false,
// Single title — files may have multiple H1 sections
"MD025": false,
// Trailing spaces — editor artifacts
"MD009": false,
// Bare URLs — URL shortening not always needed
"MD034": false,
// Single trailing newline — editor artifacts
"MD047": false,
// Trailing punctuation — heading punctuation is intentional
"MD026": false,
// Space in emphasis — double-asterisk bold spacing varies by renderer
"MD037": false,
// No hard tabs — some generated docs use tabs for indentation
"MD010": false,
// Code block style — generated docs may use inconsistent styles
"MD046": false,
// Comment style — generated docs have no comments
"MD048": false,
// Commands show output — shell examples intentionally show only commands
"MD014": false
},
"ignores": [
"docs/api-reference/generated/**"
]
}
+1
View File
@@ -0,0 +1 @@
docs/api-reference/generated/**
+1
View File
@@ -32,3 +32,4 @@ gh workflow run Release --field version=0.1.0
## License
Apache-2.0
+3 -3
View File
@@ -1,4 +1,4 @@
version: "0.1.0"
version: "0.1.2"
name: headlamp-argocd
displayName: ArgoCD Headlamp Plugin
createdAt: "2026-04-21T00:00:00Z"
@@ -26,8 +26,8 @@ maintainers:
provider:
name: privilegedescalation
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.0/headlamp-argocd-0.1.0.tar.gz"
headlamp/plugin/archive-checksum: "sha256:1f4df43f79b795bdf4f70e1e3aa5bacadf689ea5584fdadf92fb677faab21c2c"
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.2/privilegedescalation-headlamp-argocd-plugin-0.1.2.tar.gz"
headlamp/plugin/archive-checksum: sha256:e71f84913eed1fd7e2d074912e3bfa668c4b1fefcbb069731a4e4277a998ca28
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/distro-compat: "in-cluster"
changes:
+20
View File
@@ -0,0 +1,20 @@
{
// Allowlist for inherited dev-dependency CVEs from @kinvolk/headlamp-plugin
// CTO decision (PRI-854): these high-severity vulns are dev/build-time only,
// trace to @kinvolk/headlamp-plugin transitive deps (Picomatch, Vite, lodash),
// and do NOT ship in production plugin artifacts.
"allowlist": [
{
"id": "GHSA-hhpm-516h-p3p6",
"reason": "Picomatch ReDoS: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-36xf-7xpp-53w5",
"reason": "Vite arbitrary file read: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-jf8v-p3pp-93qh",
"reason": "lodash code injection via _.template: devDependency only, does not ship in production plugin bundle"
}
]
}
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@privilegedescalation/headlamp-argocd-plugin",
"version": "0.1.0",
"version": "0.1.2",
"description": "Headlamp plugin for ArgoCD visibility — monitors ArgoCD Applications, Rollouts, and health status",
"repository": {
"type": "git",
@@ -33,7 +33,8 @@
"overrides": {
"tar": "^7.5.11",
"undici": "^7.24.3",
"flatted": "^3.4.2"
"flatted": "^3.4.2",
"elliptic": ">=6.6.1"
}
},
"devDependencies": {
@@ -52,9 +53,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^5.3.0",
"tar": "^7.5.11",
"typescript": "~5.6.2",
"undici": "^7.24.3",
"vitest": "^3.0.5"
}
}
}
+3 -8
View File
@@ -8,6 +8,7 @@ overrides:
tar: ^7.5.11
undici: ^7.24.3
flatted: ^3.4.2
elliptic: '>=6.6.1'
importers:
@@ -58,15 +59,9 @@ importers:
react-router-dom:
specifier: ^5.3.0
version: 5.3.4(react@18.3.1)
tar:
specifier: ^7.5.11
version: 7.5.13
typescript:
specifier: ~5.6.2
version: 5.6.3
undici:
specifier: ^7.24.3
version: 7.25.0
vitest:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.39)(jsdom@24.1.3)(msw@2.4.9(typescript@5.6.3))(terser@5.46.1)(yaml@2.8.3)
@@ -6235,7 +6230,7 @@ snapshots:
jsdom: 24.1.3
jsonpath-plus: 10.4.0
lodash: 4.18.1
material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7)
material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e)
monaco-editor: 0.52.2
msw: 2.4.9(typescript@5.6.2)
msw-storybook-addon: 2.0.3(msw@2.4.9(typescript@5.6.3))
@@ -9937,7 +9932,7 @@ snapshots:
'@types/minimatch': 3.0.5
minimatch: 3.1.5
material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7):
material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e):
dependencies:
'@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>privilegedescalation/.github:renovate-config"]
}
+142
View File
@@ -0,0 +1,142 @@
import { describe, expect, it } from "vitest";
import type { ArgoCDApplication } from "../api/argocd";
// --- Matching helpers (copied for unit testing) ---
function appsForNamespace(
apps: ArgoCDApplication[],
namespace: string
): ArgoCDApplication[] {
return apps.filter((app) => app.spec?.destination?.namespace === namespace);
}
function appsForDeployment(
apps: ArgoCDApplication[],
deploymentName: string
): ArgoCDApplication[] {
return apps.filter((app) =>
(app.status?.resources ?? []).some(
(res) => res.kind === "Deployment" && res.name === deploymentName
)
);
}
// --- Fixture factory ---
function makeApp(
overrides: Partial<ArgoCDApplication> = {}
): ArgoCDApplication {
return {
metadata: { name: "test-app", namespace: "argocd" },
spec: { project: "default" },
status: {},
...overrides,
} as ArgoCDApplication;
}
// --- appsForNamespace tests ---
describe("appsForNamespace", () => {
it("returns apps whose destination.namespace matches", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
spec: { project: "default", destination: { namespace: "web" } },
}),
makeApp({
metadata: { name: "app-b", namespace: "argocd" },
spec: { project: "default", destination: { namespace: "data" } },
}),
];
expect(appsForNamespace(apps, "web").map((a) => a.metadata.name)).toEqual([
"app-a",
]);
});
it("returns empty array when no match", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
spec: { project: "default", destination: { namespace: "web" } },
}),
];
expect(appsForNamespace(apps, "data")).toEqual([]);
});
it("returns empty array for empty app list", () => {
expect(appsForNamespace([], "web")).toEqual([]);
});
it("returns empty array when destination is undefined", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
spec: { project: "default" },
}),
];
expect(appsForNamespace(apps, "web")).toEqual([]);
});
});
// --- appsForDeployment tests ---
describe("appsForDeployment", () => {
it("returns apps that manage the deployment via status.resources", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
status: {
resources: [{ kind: "Deployment", name: "nginx", namespace: "web" }],
},
}),
makeApp({
metadata: { name: "app-b", namespace: "argocd" },
status: {
resources: [{ kind: "Service", name: "nginx", namespace: "web" }],
},
}),
];
expect(
appsForDeployment(apps, "nginx").map((a) => a.metadata.name)
).toEqual(["app-a"]);
});
it("returns empty array when no deployment resource matches", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
status: {
resources: [{ kind: "Service", name: "nginx", namespace: "web" }],
},
}),
];
expect(appsForDeployment(apps, "nginx")).toEqual([]);
});
it("returns empty array for empty app list", () => {
expect(appsForDeployment([], "nginx")).toEqual([]);
});
it("returns empty array when resources is undefined", () => {
const apps = [
makeApp({ metadata: { name: "app-a", namespace: "argocd" }, status: {} }),
];
expect(appsForDeployment(apps, "nginx")).toEqual([]);
});
it("returns multiple apps that manage the same deployment", () => {
const apps = [
makeApp({
metadata: { name: "app-a", namespace: "argocd" },
status: { resources: [{ kind: "Deployment", name: "nginx" }] },
}),
makeApp({
metadata: { name: "app-b", namespace: "argocd" },
status: { resources: [{ kind: "Deployment", name: "nginx" }] },
}),
];
expect(
appsForDeployment(apps, "nginx").map((a) => a.metadata.name)
).toEqual(["app-a", "app-b"]);
});
});
+101
View File
@@ -0,0 +1,101 @@
import { ApiProxy } from "@kinvolk/headlamp-plugin/lib";
import {
Link,
StatusLabel,
} from "@kinvolk/headlamp-plugin/lib/CommonComponents";
import React, { useEffect, useState } from "react";
import { ArgoCDApplication, ArgoCDApplicationsList } from "../api/argocd";
import { syncStatusToColor } from "./ApplicationsList";
// --- API ---
const ARGOCD_API_PATH =
"/api/v1/namespaces/argocd/services/argocd-server/proxy/api/v1/applications";
async function fetchApplications(): Promise<ArgoCDApplicationsList> {
const response = (await ApiProxy.request(
ARGOCD_API_PATH
)) as ArgoCDApplicationsList;
return response;
}
// --- Matching helper ---
/**
* Returns ArgoCD applications that manage the given Deployment by matching
* kind=Deployment and name in Application.status.resources[].
*/
export function appsForDeployment(
apps: ArgoCDApplication[],
deploymentName: string
): ArgoCDApplication[] {
return apps.filter((app) =>
(app.status?.resources ?? []).some(
(res) => res.kind === "Deployment" && res.name === deploymentName
)
);
}
// --- Component ---
interface DeploymentArgoBadgeProps {
deploymentName: string;
}
export default function DeploymentArgoBadge({
deploymentName,
}: DeploymentArgoBadgeProps) {
const [apps, setApps] = useState<ArgoCDApplication[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchApplications()
.then((data) => {
if (cancelled) return;
const matched = appsForDeployment(data.items ?? [], deploymentName);
setApps(matched);
setLoading(false);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [deploymentName]);
if (loading || error || !apps || apps.length === 0) {
return null; // Show nothing when no matching application
}
const app = apps[0]; // Show first matching app
const lastSynced = app.status?.history?.length
? app.status.history[app.status.history.length - 1]?.dexKey
: null;
const lastSyncedStr = lastSynced
? new Date(lastSynced).toLocaleString()
: "—";
return (
<span>
&nbsp;
<Link to={`/argocd/applications/${app.metadata.name}`}>
ArgoCD: {app.metadata.name}
</Link>
&nbsp;
<StatusLabel
status={syncStatusToColor(app.status?.sync?.status ?? "Unknown")}
>
{app.status?.sync?.status ?? "Unknown"}
</StatusLabel>
&nbsp;
<span style={{ fontSize: "0.85em", opacity: 0.8 }}>
Last sync: {lastSyncedStr}
</span>
</span>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { ApiProxy } from "@kinvolk/headlamp-plugin/lib";
import {
Link,
SectionBox,
StatusLabel,
} from "@kinvolk/headlamp-plugin/lib/CommonComponents";
import React, { useEffect, useState } from "react";
import { ArgoCDApplication, ArgoCDApplicationsList } from "../api/argocd";
import {
healthStatusToColor,
healthStatusToLabel,
syncStatusToColor,
} from "./ApplicationsList";
// --- API ---
const ARGOCD_API_PATH =
"/api/v1/namespaces/argocd/services/argocd-server/proxy/api/v1/applications";
async function fetchApplications(): Promise<ArgoCDApplicationsList> {
const response = (await ApiProxy.request(
ARGOCD_API_PATH
)) as ArgoCDApplicationsList;
return response;
}
// --- Matching helper ---
/**
* Returns ArgoCD applications whose spec.destination.namespace matches
* the given namespace name.
*/
export function appsForNamespace(
apps: ArgoCDApplication[],
namespace: string
): ArgoCDApplication[] {
return apps.filter((app) => app.spec?.destination?.namespace === namespace);
}
// --- Component ---
interface NamespaceArgoSectionProps {
namespaceName: string;
}
export default function NamespaceArgoSection({
namespaceName,
}: NamespaceArgoSectionProps) {
const [apps, setApps] = useState<ArgoCDApplication[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchApplications()
.then((data) => {
if (cancelled) return;
const matched = appsForNamespace(data.items ?? [], namespaceName);
setApps(matched);
setLoading(false);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [namespaceName]);
if (loading) {
return (
<SectionBox title="ArgoCD">
<StatusLabel status="warning">Loading...</StatusLabel>
</SectionBox>
);
}
if (error || !apps) {
return (
<SectionBox title="ArgoCD">
<StatusLabel status="error">ArgoCD unreachable</StatusLabel>
</SectionBox>
);
}
if (apps.length === 0) {
return null; // Show nothing when no matching application
}
return (
<SectionBox title="ArgoCD">
<StatusLabel status="success">{apps.length} application(s)</StatusLabel>
<ul style={{ paddingLeft: 20, margin: "8px 0" }}>
{apps.map((app) => (
<li key={app.metadata.name} style={{ marginBottom: 8 }}>
<Link to={`/argocd/applications/${app.metadata.name}`}>
{app.metadata.name}
</Link>
&nbsp;
<StatusLabel
status={healthStatusToColor(
app.status?.health?.status ?? "Unknown"
)}
>
{healthStatusToLabel(app.status?.health?.status ?? "Unknown")}
</StatusLabel>
&nbsp;
<StatusLabel
status={syncStatusToColor(app.status?.sync?.status ?? "Unknown")}
>
{app.status?.sync?.status ?? "Unknown"}
</StatusLabel>
</li>
))}
</ul>
</SectionBox>
);
}
+207
View File
@@ -0,0 +1,207 @@
/**
* Page injection registrations for ArgoCD plugin.
* Registers detail view sections on Namespace and Deployment pages.
*/
import { ApiProxy } from "@kinvolk/headlamp-plugin/lib";
import { KubeObject } from "@kinvolk/headlamp-plugin/lib/lib/k8s/KubeObject";
import { registerDetailsViewSection } from "@kinvolk/headlamp-plugin/lib";
import {
SectionBox,
StatusLabel,
} from "@kinvolk/headlamp-plugin/lib/CommonComponents";
import { Link } from "react-router-dom";
import React, { useEffect, useState } from "react";
import { ArgoCDApplication, ArgoCDApplicationsList } from "../api/argocd";
import {
healthStatusToColor,
healthStatusToLabel,
syncStatusToColor,
} from "./ApplicationsList";
// --- API ---
const ARGOCD_API_PATH =
"/api/v1/namespaces/argocd/services/argocd-server/proxy/api/v1/applications";
async function fetchApplications(): Promise<ArgoCDApplicationsList> {
const response = (await ApiProxy.request(
ARGOCD_API_PATH
)) as ArgoCDApplicationsList;
return response;
}
// --- Namespace section ---
function NamespaceArgoSection({ resource }: { resource: KubeObject }) {
const namespaceName = resource.metadata.name;
const [apps, setApps] = useState<ArgoCDApplication[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchApplications()
.then((data) => {
if (cancelled) return;
const matched = (data.items ?? []).filter(
(app) => app.spec?.destination?.namespace === namespaceName
);
setApps(matched);
setLoading(false);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [namespaceName]);
if (loading) {
return (
<SectionBox title="ArgoCD">
<StatusLabel status="warning">Loading...</StatusLabel>
</SectionBox>
);
}
if (error || !apps) {
return (
<SectionBox title="ArgoCD">
<StatusLabel status="error">ArgoCD unreachable</StatusLabel>
</SectionBox>
);
}
if (apps.length === 0) {
return null;
}
return (
<SectionBox title="ArgoCD">
<StatusLabel status="success">{apps.length} application(s)</StatusLabel>
<ul style={{ paddingLeft: 20, margin: "8px 0" }}>
{apps.map((app) => (
<li key={app.metadata.name} style={{ marginBottom: 8 }}>
<Link to={`/argocd/applications/${app.metadata.name}`}>
{app.metadata.name}
</Link>
&nbsp;
<StatusLabel
status={healthStatusToColor(
(app.status?.health?.status as
| "Healthy"
| "Degraded"
| "Progressing"
| "Missing"
| "Unknown") ?? "Unknown"
)}
>
{healthStatusToLabel(
(app.status?.health?.status as
| "Healthy"
| "Degraded"
| "Progressing"
| "Missing"
| "Unknown") ?? "Unknown"
)}
</StatusLabel>
&nbsp;
<StatusLabel
status={syncStatusToColor(
(app.status?.sync?.status as
| "Synced"
| "OutOfSync"
| "Unknown") ?? "Unknown"
)}
>
{app.status?.sync?.status ?? "Unknown"}
</StatusLabel>
</li>
))}
</ul>
</SectionBox>
);
}
// --- Deployment badge ---
function DeploymentArgoBadge({ resource }: { resource: KubeObject }) {
const deploymentName = resource.metadata.name;
const [apps, setApps] = useState<ArgoCDApplication[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchApplications()
.then((data) => {
if (cancelled) return;
const matched = (data.items ?? []).filter((app) =>
(app.status?.resources ?? []).some(
(res) => res.kind === "Deployment" && res.name === deploymentName
)
);
setApps(matched);
setLoading(false);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [deploymentName]);
if (loading || error || !apps || apps.length === 0) {
return null;
}
const app = apps[0];
const lastSynced = app.status?.history?.length
? app.status.history[app.status.history.length - 1]?.dexKey
: null;
const lastSyncedStr = lastSynced
? new Date(lastSynced).toLocaleString()
: "—";
return (
<span>
&nbsp;
<Link to={`/argocd/applications/${app.metadata.name}`}>
ArgoCD: {app.metadata.name}
</Link>
&nbsp;
<StatusLabel
status={syncStatusToColor(
(app.status?.sync?.status as "Synced" | "OutOfSync" | "Unknown") ??
"Unknown"
)}
>
{app.status?.sync?.status ?? "Unknown"}
</StatusLabel>
&nbsp;
<span style={{ fontSize: "0.85em", opacity: 0.8 }}>
Last sync: {lastSyncedStr}
</span>
</span>
);
}
// --- Registration ---
registerDetailsViewSection(({ resource }: { resource: KubeObject }) => {
if (resource.kind === "Namespace") {
return <NamespaceArgoSection resource={resource} />;
}
if (resource.kind === "Deployment") {
return <DeploymentArgoBadge resource={resource} />;
}
return null;
});
+1
View File
@@ -9,6 +9,7 @@ import {
import React from "react";
import ApplicationDetail from "./components/ApplicationDetail";
import ApplicationsList from "./components/ApplicationsList";
import "./components/PageInjections"; // side-effect: registers detail view sections
// --- Error boundary for plugin components ---