폐쇄망 환경이거나 네트워크 방화벽, 또는 npx 명령 실행이 막혀 있는 경우 npx CLI 도구 없이 직접 수동으로 프로젝트 구조와 파일을 생성하여 빌드하면 됩니다.
Headlamp 플러그인은 특수한 바이너리가 아니라 표준 TypeScript/React 웹팩 프로젝트이므로, 디렉터리를 만들고 아래 파일들을 그대로 복사해 넣으면 동일하게 동작합니다.
mkdir -p lakehouse-monitor/src
cd lakehouse-monitor
package.json 생성package.json 파일을 생성하고 아래 내용을 저장합니다.
{
"name": "lakehouse-monitor",
"version": "0.1.0",
"description": "Headlamp plugin for monitoring CNPG and StarRocks clusters",
"main": "dist/main.js",
"scripts": {
"build": "headlamp-plugin build"
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.8.0",
"@types/react": "^18.2.0",
"typescript": "^5.0.0"
},
"dependencies": {
"react": "^18.2.0"
}
}
tsconfig.json 생성tsconfig.json 파일을 생성하고 아래 TypeScript 설정을 저장합니다.
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
src/index.tsx 소스코드 작성src/index.tsx 파일에 아래 대시보드 코드를 작성합니다.
import {
registerSidebarEntry,
registerRoute,
K8s,
CommonComponents
} from '@kinvolk/headlamp-plugin/lib';
import React from 'react';
const { SectionBox, SimpleTable, StatusLabel } = CommonComponents;
// 1. CRD 정의
const CNPGCluster = K8s.ResourceClasses.makeCustomResourceClass({
apiInfo: [{ group: 'postgresql.cnpg.io', version: 'v1', resource: 'clusters' }],
isNamespaced: true,
});
const StarRocksCluster = K8s.ResourceClasses.makeCustomResourceClass({
apiInfo: [{ group: 'starrocks.com', version: 'v1', resource: 'starrocksclusters' }],
isNamespaced: true,
});
// 2. 대시보드 컴포넌트
function LakehouseDashboard() {
const [cnpgList, setCnpgList] = React.useState<any[]>([]);
const [srList, setSrList] = React.useState<any[]>([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
Promise.all([
CNPGCluster.apiEndpoint.get('/apis/postgresql.cnpg.io/v1/clusters'),
StarRocksCluster.apiEndpoint.get('/apis/starrocks.com/v1/starrocksclusters'),
])
.then(([cnpgRes, srRes]) => {
setCnpgList(cnpgRes?.items || []);
setSrList(srRes?.items || []);
})
.catch((err) => console.error('Failed to fetch Lakehouse CRDs:', err))
.finally(() => setLoading(false));
}, []);
if (loading) {
return <div>Loading Lakehouse Clusters...</div>;
}
return (
<div>
<SectionBox title={`CloudNativePG Clusters (${cnpgList.length})`}>
<SimpleTable
columns={[
{ label: 'Namespace', getter: (item: any) => item.metadata.namespace },
{ label: 'Name', getter: (item: any) => item.metadata.name },
{
label: 'Phase / Status',
getter: (item: any) => {
const phase = item.status?.phase || 'Unknown';
const statusType = phase === 'Cluster in healthy state' ? 'success' : 'warning';
return <StatusLabel status={statusType}>{phase}</StatusLabel>;
}
},
{ label: 'Primary Pod', getter: (item: any) => item.status?.currentPrimary || '-' },
{
label: 'Instances (Ready/Total)',
getter: (item: any) => `${item.status?.readyInstances || 0} / ${item.status?.instances || 0}`
},
]}
data={cnpgList}
/>
</SectionBox>
<SectionBox title={`StarRocks Clusters (${srList.length})`}>
<SimpleTable
columns={[
{ label: 'Namespace', getter: (item: any) => item.metadata.namespace },
{ label: 'Name', getter: (item: any) => item.metadata.name },
{
label: 'Phase',
getter: (item: any) => {
const phase = item.status?.phase || 'Running';
const statusType = phase === 'Running' ? 'success' : 'error';
return <StatusLabel status={statusType}>{phase}</StatusLabel>;
}
},
{
label: 'FE Replicas (Ready/Req)',
getter: (item: any) =>
`${item.status?.starRocksFeStatus?.readyReplicas || 0} / ${item.spec?.starRocksFeSpec?.replicas || 0}`
},
{
label: 'BE Replicas (Ready/Req)',
getter: (item: any) =>
`${item.status?.starRocksBeStatus?.readyReplicas || 0} / ${item.spec?.starRocksBeSpec?.replicas || 0}`
},
]}
data={srList}
/>
</SectionBox>
</div>
);
}
registerRoute({
path: '/lakehouse',
sidebar: 'lakehouse',
name: 'Data Lakehouse',
exact: true,
component: () => <LakehouseDashboard />,
});
registerSidebarEntry({
name: 'lakehouse',
label: 'Lakehouse DBs',
url: '/lakehouse',
icon: 'mdi:database-outline',
});
# 사내 npm 프록시/레지스트리가 있다면 registry 옵션 추가
npm install
# 빌드 실행
npm run build
빌드가 성공하면 dist/main.js 파일이 생성됩니다. 이 main.js 파일을 Headlamp의 ConfigMap으로 마운트하여 사용하시면 됩니다.
npm install조차 불가능한 대안만약 서버 자체가 외부 인터넷 및 npm 레지스트리와 완전히 단절되어 있다면:
npm run build를 수행합니다.dist/main.js 파일(단일 JavaScript 파일)만 RHEL 서버 또는 K8s 클러스터로 복사(scp 등)합니다.kubectl create configmap headlamp-lakehouse-plugin \
--from-file=main.js=./dist/main.js \
-n headlamp