26Z21b

QK·2026년 8월 21일

폐쇄망 환경이거나 네트워크 방화벽, 또는 npx 명령 실행이 막혀 있는 경우 npx CLI 도구 없이 직접 수동으로 프로젝트 구조와 파일을 생성하여 빌드하면 됩니다.

Headlamp 플러그인은 특수한 바이너리가 아니라 표준 TypeScript/React 웹팩 프로젝트이므로, 디렉터리를 만들고 아래 파일들을 그대로 복사해 넣으면 동일하게 동작합니다.


수동 프로젝트 구성 (4단계)

1. 디렉터리 생성 및 진입

mkdir -p lakehouse-monitor/src
cd lakehouse-monitor

2. 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"
  }
}

3. 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/**/*"]
}

4. 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',
});

5. 의존성 설치 및 빌드

# 사내 npm 프록시/레지스트리가 있다면 registry 옵션 추가
npm install

# 빌드 실행
npm run build

빌드가 성공하면 dist/main.js 파일이 생성됩니다. 이 main.js 파일을 Headlamp의 ConfigMap으로 마운트하여 사용하시면 됩니다.


폐쇄망이라 npm install조차 불가능한 대안

만약 서버 자체가 외부 인터넷 및 npm 레지스트리와 완전히 단절되어 있다면:

  1. 외부 인터넷이 되는 로컬 PC(노트북 등)에서 위 과정을 거쳐 npm run build를 수행합니다.
  2. 생성된 dist/main.js 파일(단일 JavaScript 파일)만 RHEL 서버 또는 K8s 클러스터로 복사(scp 등)합니다.
  3. K8s ConfigMap으로 등록합니다:
kubectl create configmap headlamp-lakehouse-plugin \
  --from-file=main.js=./dist/main.js \
  -n headlamp
profile
engineer

0개의 댓글