diff --git a/applicationFE/scripts/test-builtin-helm.mjs b/applicationFE/scripts/test-builtin-helm.mjs index c1884c1..5ca1698 100644 --- a/applicationFE/scripts/test-builtin-helm.mjs +++ b/applicationFE/scripts/test-builtin-helm.mjs @@ -28,13 +28,14 @@ function harness() { } let cases=0 for (const app of ['redis','mariadb','postgresql','apache','tomcat']) { + for (const version of ['0.1.0', '0.1.1', 'custom', '']) { for (const target of ['VM','K8S']) { const h=harness() h.selectInfra.value=target h.selectedCatalogChartName.value=app - h.selectedCatalogInfo.value={helmChart:{chartName:app,repositoryName:'mcmp-builtin',chartRepositoryUrl:'classpath:helm',chartVersion:'0.1.0',packageId:'mcmp-builtin-'+app}} + h.selectedCatalogInfo.value={helmChart:{chartName:app,repositoryName:'mcmp-builtin',chartRepositoryUrl:'classpath:helm',chartVersion:version,packageId:'mcmp-builtin-'+app}} await nextTick() - const persistent=target==='K8S' && ['redis','mariadb','postgresql'].includes(app) + const persistent=version==='0.1.0' && target==='K8S' && ['redis','mariadb','postgresql'].includes(app) assert.equal(h.isBuiltInPersistentCatalog.value,persistent) assert.equal(h.storageClassRequired.value,persistent) if (persistent) { @@ -62,5 +63,6 @@ for (const app of ['redis','mariadb','postgresql','apache','tomcat']) { } cases++ } + } } console.log(`Built-in Helm form passed (${cases} scenarios plus storage and identity checks).`) diff --git a/doc/k8s-csp-storage-fixes-20260923.md b/doc/k8s-csp-storage-fixes-20260923.md new file mode 100644 index 0000000..6ab463b --- /dev/null +++ b/doc/k8s-csp-storage-fixes-20260923.md @@ -0,0 +1,134 @@ +# Kubernetes storage/spec compatibility verification — 2026-09-23 + +> Follow-up, 2026-09-28: the 52-server was reinstalled. NHN and Tencent live +> retests are complete and owned test resources have been cleaned up. Each passed +> the five-app functional checks; Tencent has a separately documented same-Pod +> Service access limitation. See [the follow-up report](k8s-nhn-tencent-retest-20260928.md). +> The blocked/outstanding entries below are the historical September 23 state, +> not the current closeout status. The old NHN cluster was also confirmed absent +> from the current account before creating the new isolated test cluster. + +> Final source adjustment, 2026-09-28: per the user's request, the bundled chart +> version stays at `0.1.0` and the proposed startup version migration was removed. +> The `0.1.1` references and JAR hash below describe the historical test build, +> not the final version policy. NHN storage and Alibaba memory fixes are retained. + +## Changes + +- Bundled Redis, MariaDB and PostgreSQL validate Cinder CSI registration before + deployment. For Cinder volumes only, a bounded init container prepares the PVC + mount root with the application's UID/GID. It does not recursively rewrite data; + the application remains non-root. Apache/Tomcat and other storage drivers do not + receive this initializer. Restricted Pod Security can reject the root initializer; + AM does not relax cluster admission policy. +- The existing NHN StorageClass creation API now explicitly requests `ext4`. + `Retain` and `WaitForFirstConsumer` remain unchanged. Existing classes and their + default designation are never modified. The managed Cinder add-on remains a + cluster prerequisite, not an automatic shared-cluster mutation by AM. +- Spider's positive finite `MemSizeMib` is preferred over legacy memory metadata. + Missing/invalid normalized fields still use the existing older-response paths. + This fixes Alibaba's `MemSizeMib=16384` / provider `Memory=0` combination without + bypassing the capacity comparison or changing the existing continue-anyway policy. +- Bundled chart version is `0.1.1`. Startup migrates only exact bundled `0.1.0` + identities. Custom mappings, running releases and VM settings are preserved. +- No IBM driver, IBM cluster or production AM image changes were made. + +## Local verification + +- Full Gradle suite: 688 tests, 0 failures/errors, 1 conditionally skipped test. +- Helm lint/template and AM values-pipeline tests executed with Helm installed. +- Frontend type check/build passed. Built-in form checks passed 44 scenarios, + including VM/K8s and both old/new chart versions. +- Additional cases cover missing/registering CSI, non-Cinder/stateless workloads, + bounded init security settings, new ext4 class creation, catalog migration, + older memory response shapes, invalid normalized capacity and requirements + both below and above the actual 16GiB capacity. + +## Alibaba live verification + +Executed from an isolated AM/DB on `52.24.20.63`, not its production AM, through +Spec Check → deployment submission → actual Service access → uninstall. + +- Final result: Apache HTTP Server, Tomcat, Redis, MariaDB and PostgreSQL passed. +- HTTP apps returned 200. DB/cache apps accepted valid credentials, rejected + invalid authentication, and preserved written data after Pod replacement. +- AM logged `16384.0MiB` and available `16.0 GiB`; no test bypassed Spec Check. +- Worker: one `ecs.u1-c1m4.xlarge`, Kubernetes `1.35.7-aliyun.1`, Tokyo. +- Persistent tests used `alicloud-disk-topology-alltype`, 20Gi, matching the AM + form's existing recommendation and initial size. +- Initial harness selection of `alicloud-disk-efficiency` was incompatible with + that worker. Its Pod reported unsupported `cloud_efficiency`, so the test was + stopped, its resources removed, and the harness aligned with the existing UI. + The initial non-pass is retained in evidence, not counted as a successful run. +- AM can report submission success before actual Pod readiness; independent + readiness and functional checks were therefore required. That pre-existing + status behavior was not changed by this scoped compatibility patch. +- Native inventory verified cluster, nodes, disks, SG, SSH key, VPC/subnets, + managed LB, NAT and test EIPs removed. Protected metadata/container identities + remained unchanged. + +## NHN live verification + +Implementation and local tests are complete, but the live test is blocked and +must not be described as passed or cleaned up. + +- Created only a new test cluster in `kr1`, one `m2.c4m8` worker, Kubernetes + `v1.34.3`. The worker was confirmed ACTIVE through the NHN native API. +- At 06:14 UTC, the cluster was still `CREATE_IN_PROGRESS`; the API reported no + failure reason. No managed Cinder installation, app deployment or test PVC had + yet been submitted to this cluster. +- At approximately 06:15 UTC (15:15 KST), the 52-server SSH connection was closed + remotely. Subsequent SSH port 22 and web-console port 3001 requests timed out. + Connectivity to server 210 and an unrelated public HTTPS endpoint still worked. + This establishes a 52-server access interruption, not its underlying cause. +- AWS console access was not available as an authenticated session. The AM/server + was not restarted by this task. Server access recovery is required to finish + validation and exact-resource cleanup. The user was informed that resources remain. + +Outstanding owned NHN resource identifiers (never substitute broad deletion): + +- Tumblebug namespace `amfix-0923`, UID `e846394d317346289e9a`. +- Cluster `am5-nhn`, UID/name `tbmpo9fbveq4jn086k0n`, native ID + `d7870e40-4a07-491d-94da-b1ca132a1587`. +- Worker `9a9ba71c-9691-49a4-9d95-a56e2f81ca25` (belongs to the new cluster). +- VPC `0c6a7af2-076c-4b43-abfb-0cfc4c9eb76b`. +- Subnets `8337179d-ce9c-407c-8887-990c214b12c2`, + `c579a81f-3313-4574-82e6-bfe0c65c012f`. +- Security group `85dacba7-843b-47f3-9113-d6085849988d`. +- SSH key `tbd8tncetn7n6f8a1dt5`. + +The isolated test AM/DB, test network/volume and namespace also require closeout. +On access recovery, first inspect whether the old waiter is still running; do not +blindly rerun initialization or create a second cluster. The evidence directory +contains `nhn.json`, protected baselines and exact-ID guarded cleanup scripts. + +## Tencent / server 210 + +The recent 52-server failure is different from the September 16 test: its default +worker image was Ubuntu 16.04.1, and kube-proxy expected xtables under +`/host/usr/sbin` while the worker supplied them under `/sbin`. No app was deployed. +The September 16 run corrected an empty tag and availability-zone subnet selection, +then succeeded with explicitly requested Ubuntu 22.04 workers. See +[the historical report](nginx-multi-csp-verification-20260916.md). + +Read-only SSH checks on `210.217.178.130` found Tumblebug/Spider `0.13.2` and an +active etcd `NOSPACE` alarm. `endpoint health` failed to commit a proposal because +of that alarm. No Tencent cloud resources were created there; no shared etcd +maintenance was attempted. Resolve/verify etcd health before retrying with an +explicit compatible worker image. This is a test blocker, not a Tencent AM app +failure. + +## Evidence and scope + +Server evidence directory: `/home/ubuntu/am-storage-fix-20260923/csp-run/` (private). +Test namespace: `amfix-0923`; one newly owned cluster at a time. Existing resources, +including archiving resources and concurrent work, are protected. + +Test JAR SHA-256: `a732bf5e6ee43892872fadbc97f6da2bc644f5878fe4466c388e1bb0727c5c69`. +Test image: `am-local:storage-fix-20260923`, digest +`sha256:e88c526ea334d5623e6bb1beae537f5fd887d174001851aa6392424838612b13`. +At the end of the September 23 run, later local additions were tests/documentation +only. The final September 28 chart-version adjustment is described above and is +not represented by this historical JAR hash. Public Ingress/CIDR and browser +interaction are not covered by these internal-Service functional tests. No commit, +push or production redeployment was part of the September 23 run. diff --git a/doc/k8s-nhn-tencent-retest-20260928.md b/doc/k8s-nhn-tencent-retest-20260928.md new file mode 100644 index 0000000..56a08b2 --- /dev/null +++ b/doc/k8s-nhn-tencent-retest-20260928.md @@ -0,0 +1,155 @@ +# NHN / Tencent 재시험 — 2026-09-28 + +> 실배포 시험 종료 후 사용자의 요청으로 내장 차트 버전은 `0.1.0`으로 유지하고, +> 제안했던 `0.1.1` DB 자동 갱신을 제거했다. 아래 실배포 결과와 JAR 해시는 변경 전 +> 시험 빌드의 기록이다. NHN/Alibaba 기능 수정은 유지하며, 최종 버전 정책 변경은 +> 로컬 회귀 검증 대상이다. 이 조정을 위해 클라우드 자원을 다시 생성하지 않았다. + +## 범위와 환경 + +- 재설치된 `52.24.20.63`에서 수행. Tumblebug 0.13.4, Spider 0.13.9. +- 테스트 전용 namespace `amfix-0928`, 별도 AM / PostgreSQL 사용. + 테스트 AM은 `127.0.0.1:19884`에만 바인딩했다. +- 이전 NHN/Alibaba 수정이 포함된 로컬 JAR을 별도 컨테이너에 마운트했다. + JAR SHA-256: `a732bf5e6ee43892872fadbc97f6da2bc644f5878fe4466c388e1bb0727c5c69`. +- 운영 AM 이미지 교체, 공유 서비스 재시작, 공유 클러스터 설정 변경은 하지 않았다. +- 비용을 줄이기 위해 CSP별 워커 1대, 앱 순차 배포, NHN 정리 후 Tencent 생성 순서로 진행했다. +- 배포는 Spec Check → AM deployment submission → 실제 Pod/Service 검증으로 진행했다. + Spec Check의 거부를 무시하거나 카탈로그 권장 사양을 낮추지 않았다. +- Ingress와 애플리케이션 외부 포트는 사용하지 않았다. 공개 Ingress/CIDR, 브라우저 UI, + HA/다중 노드, 부하 시험의 통과를 의미하지 않는다. + +## NHN — 5종 통과, 클라우드 자원 정리 확인 + +- `kr1`, `m2.c4m8` 1대, Ubuntu 22.04.5, Kubernetes v1.34.3. +- Apache HTTP Server / Tomcat: HTTP 200 응답 확인. +- Redis / MariaDB / PostgreSQL: 정상 인증, 잘못된 비밀번호 거부, 데이터 기록 후 + Pod 교체 및 데이터 재조회 확인. +- Cinder CSI 설치 전 capability는 `driverReady=false`, 설치 후에는 `true`였다. + 관리형 `cinder_csi_plugin` v1.27.102-nks4를 **이번에 만든 클러스터에만** 설치했다. +- AM StorageClass 생성 API의 ext4 / Retain / WaitForFirstConsumer / 비기본 클래스 설정, + 중복 이름 거부 및 기존 클래스 UID 보존을 확인했다. 잘못된 이름·빈 이름·지원하지 않는 + 디스크 타입 3가지 입력도 HTTP 400으로 거부됐다. +- 실제 DB 3종은 파일시스템 타입을 생략한 별도 Cinder 클래스로 시험했다. + 세 앱에만 제한된 `prepare-data-volume` 초기화가 적용되고, 본 컨테이너는 non-root였다. + Apache/Tomcat에는 해당 초기화가 없었다. +- API가 만든 Retain 클래스는 설정 검증 후 **PVC가 없음을 확인하고 삭제**했다. + 실제 반복 시험에는 명시적인 Delete 정책의 테스트 클래스를 사용해 볼륨을 회수했다. + 운영 Retain 볼륨의 자동 삭제를 시험한 것은 아니다. +- 클러스터 `a2d23590-d315-49ac-9e08-7962eb26bce3`, 테스트 VM, 볼륨, SG, SSH 키, + VPC/서브넷이 제거됐음을 Spider 목록과 NHN 원본 API로 대조했다. + 원래 있던 네트워크/보안그룹과 floating IP 연결 상태는 보존됐다. + +새 NHN 클러스터에는 여전히 **관리형 Cinder CSI 사전 준비가 필요**하다. +이번 AM 수정은 필요한 스토리지 검사와 앱 볼륨 권한 초기화이며, 공유 클러스터의 +관리형 CSI 애드온을 무조건 자동 설치하는 변경은 아니다. + +## Tencent — 일반 클라이언트 경로 5종 통과 + +- `ap-seoul-1`, `SA2.LARGE4` 1대. MariaDB의 기존 4 vCPU 요구량을 만족하는 저가 사양 선택. +- 노드 이미지 `ubuntu22.04x86_64`를 명시했다. 실제 워커는 Ubuntu 22.04.5, + Kubernetes v1.34.1-tke.8이며, kube-proxy 1/1 Ready 및 재시작 0회를 확인했다. +- 이전 Ubuntu 16.04 / iptables 경로 불일치로 클러스터가 정상화되지 않던 상태는 + 이 조합에서 재현되지 않았다. +- 현재 Spider의 Tencent SG 생성에서 inbound/outbound를 동시에 보내면 + `InvalidParameter.Coexist`로 거부됐다. 시험 전용 SG를 inbound로 생성한 뒤 + outbound를 기존 Tumblebug 규칙 추가 API로 별도 등록했다. 실패 시 남았던 SG는 + 요청 로그로 소유 ID를 확인하고 삭제했다. 공유 Spider 코드는 수정하지 않았다. +- 클러스터 생성 직후에는 관리 API가 준비되지 않아 노드그룹 생성이 거부됐다. + 원본 API의 `Running`과 미생성 노드그룹을 확인한 후 추가했다. +- 기본 제공 `cbs` StorageClass, 10Gi, RWO 사용. NHN 전용 초기화는 적용하지 않는다. +- Apache / Tomcat의 HTTP 200, Redis / MariaDB / PostgreSQL의 정상 인증·오류 인증 거부· + Pod 교체 후 데이터 유지를 모두 확인했다. DB 3종의 기능 시험은 별도 클라이언트 Pod에서 + Service DNS로 접속했다. 시험 앱, PVC, 인증 Secret, 클라이언트 Pod 제거까지 확인했다. +- 클러스터 `cls-5x7nk0yx`, 워커 `ins-2mzc7zjp`, 루트/앱 디스크, 노드풀과 ASG/launch + configuration, API용 LB `lb-m65qaq51`, 전용 SG/SSH 키/VPC/서브넷의 제거를 확인했다. + 클러스터 삭제 직후에는 LB 비동기 삭제가 끝나지 않아 SG 삭제가 한 차례 409로 거부됐다. + LB가 사라진 것을 확인한 다음 SG 등 나머지 자원을 정상 삭제했다. + 다른 작업이 만든 default 프로젝트의 Tencent 자원은 삭제하지 않았다. + +### 별도 관찰 사항: 자기 Service 접속 + +Redis Pod 안에서 직접 비교했다. + +- localhost / 자기 Pod IP: `PONG`. +- Service DNS 조회: 정상, 올바른 ClusterIP 반환. +- 같은 Pod → 자기 ClusterIP / Service DNS: 8초 제한에서 시간 초과. +- 별도 클라이언트 Pod → Redis Pod IP / ClusterIP / Service DNS: 모두 `PONG`. + +따라서 Redis 자체 미기동이나 DNS 해석 실패와는 구분된다. **자기 Service로 돌아오는 +경로의 이상은 미해결**이며, 구체적인 CNI/커널 설정 원인까지 확정하지 않았다. +일반적인 별도 클라이언트 Pod → Service 경로의 인증·지속성 시험은 통과했다. +이 사실을 숨기거나 전체 네트워크가 무조건 정상이라고 판정하지 않는다. + +### 단일 노드 제약 + +CoreDNS 2개 중 1개는 Ready, 나머지는 서로 다른 노드를 요구하는 anti-affinity 때문에 +Pending이다. 비용 절감을 위한 단일 워커 시험의 가용성 제약으로 기록했다. +다른 시스템 Pod의 실패를 무시하지 않았고, 실제 Service DNS 기능은 별도로 검증했다. +클러스터의 CoreDNS 복제 수나 배치 정책을 임의로 수정하지 않았다. + +## 로컬 재검증 + +- 관련 Gradle 테스트 17개: 실패/오류/skip 0. +- 프런트엔드 built-in 폼 검사 44개 시나리오 및 storage identity 검사 통과. +- `git diff --check` 통과. +- 9월 23일 전체 688개 테스트 결과와 구분한다. 이번에 전체 688개를 재실행한 것은 아니다. + +## 정리 및 보호 대상 — 완료 + +- 두 CSP의 테스트 소유 클라우드 자원 삭제를 원본 API로 재확인했다. +- 테스트 AM/DB 컨테이너, 전용 Docker 네트워크/볼륨, 테스트 namespace와 전용 이미지 + 참조를 제거했다. 테스트 데이터만 폐기했으며, 검증 기록은 별도로 보관한다. +- 원래 실행 중이던 **38개 컨테이너의 ID·이미지·시작 시각이 모두 동일**하고 실행 상태도 + 유지됐다. 운영 AM 이미지 교체나 공유 서비스 재시작은 없었다. +- etcd endpoint health 정상, alarm 없음. +- 서버 컨테이너 비교용 증빙에서 불필요한 운영 환경변수/비밀값을 제거했다. +- 이번 검증에서 추가로 변경한 프로젝트 파일은 결과 문서다. 기존 미커밋 구현은 보존했으며, + 커밋/푸시/운영 AM 재배포는 하지 않았다. + +다른 Jenkins 작업이 동시에 default 프로젝트 자원을 생성·삭제하고 있다. +Alibaba 초기화 인프라 `vm-csp-init-alibaba-01`의 DELETE는 `mc-workflow-manager-jenkins` +컨테이너 IP에서 시작된 요청으로 확인됐다. 이후 Tencent default 인프라 및 +`default-shared-nhn-kr1-kr-pub-a` 네트워크 생성도 관찰됐다. +이 변경을 시험 정리 대상으로 삼거나 원복하지 않았다. +최종 etcd 비교에는 Alibaba/Azure 초기화 인프라와 일부 default SG/키/VPC 등 19개 기존 +키의 차이가 기록됐다. 따라서 **서버 전체 메타데이터가 시험 시작과 완전히 동일하다고 +주장하지 않는다**. 테스트 소유 자원 정리와 다른 작업의 변경을 분리해서 검토했다. + +Tumblebug의 공용 이미지 카탈로그는 namespace 지정 등록 요청과 달리 system 범위로 +조회/삭제되는 부분이 있다. 공용 카탈로그나 CSP 원본 이미지는 삭제하지 않는다. +남은 테스트 전용 etcd 참조만 소유 UID와 값을 확인하고 compare-and-swap으로 제거했다. + +증빙은 서버의 `/home/ubuntu/am-retest-20260928/`에 제한 권한으로 보관한다. +이 폴더에는 kubeconfig 및 테스트 키가 포함되므로 원본 전체를 공개 저장소에 올리지 않는다. +최종 결과는 `public-summary.json`, `closeout.json`, `final-container-checks.json`, +`final-etcd-health.json`, `final-protected-metadata-diff.json`에 기록했다. +첫 성공 정리 시점의 클라우드 증빙과 이후 재확인(`recheck-*`)을 구분해 보존했다. + +## 검증 도구에서 발견한 비배포 이슈 + +- 초기 도구가 Helm으로 직접 제거한 Apache/Tomcat의 AM 기록은 계속 활성 상태여서 + 다음 Spec Check의 사용량에 포함됐다. 이미 제거된 테스트 릴리스만 검증해 기록을 + 정리했고, 이후 삭제는 AM action API로 수행했다. 운영 DB를 직접 수정하지 않았다. +- `/api/applications/status/all`은 lazy-loaded `unifiedLogs`의 JSON 직렬화 오류를 반환했다. + 현재 프런트엔드가 사용하는 `/api/applications/status/groups?namespace=...`는 정상이며, + 도구도 그 경로로 맞췄다. 이 별도 API 오류에 대한 AM 코드 변경은 이번 시험에 포함하지 않았다. + +## 후속 커밋 준비 — 내장 차트 0.1.0 유지 + +실배포 정리가 끝난 뒤 별도 사용자 요청에 따라 커밋할 소스를 정리했다. + +- 내장 5종의 버전은 기존 `0.1.0` 유지. `0.1.1`로 갱신하는 startup SQL은 제거했다. + 기존 차트 매핑과 실행 중인 릴리스를 자동으로 바꾸지 않는다. +- NHN Cinder 준비 검사·조건부 볼륨 권한 초기화·ext4 StorageClass 생성과 + Alibaba `MemSizeMib` 처리 수정은 유지한다. +- 차트 패키지의 `version: 0.1.0`, 기존 DB 반복 초기화 시 버전/외부 매핑 보존, + 지원하지 않는 버전·사용자 지정 매핑을 내장 차트로 취급하지 않는 경우를 검증했다. +- **최종 소스로 전체 Gradle 테스트 688개를 다시 실행**했다. + 실패/오류 0, 별도 Docker/SSH fixture를 사용하는 opt-in smoke test 1개 제외. + Helm lint/template 및 배포 values 조합 테스트도 포함한다. +- 프런트엔드 내장 차트 폼 52개 시나리오, Install SW parser 18개·VM 대상 해석 7개와 + iframe 연결 검사, VM clustering 카탈로그 24개·표시 조건 288개·전환 4개·제출 4개 통과. + 프런트엔드 타입 검사와 운영 빌드도 통과했다. +- 이 단계에서는 서버 이미지/DB를 변경하지 않았고, 클라우드 실배포도 반복하지 않았다. + 수정 전후 설치 구분에는 차트 버전 대신 AM 이미지 digest 또는 소스 커밋을 사용한다. diff --git a/src/main/java/kr/co/mcmp/ape/cbtumblebug/dto/K8sSpec.java b/src/main/java/kr/co/mcmp/ape/cbtumblebug/dto/K8sSpec.java index d1a4713..41e89d4 100644 --- a/src/main/java/kr/co/mcmp/ape/cbtumblebug/dto/K8sSpec.java +++ b/src/main/java/kr/co/mcmp/ape/cbtumblebug/dto/K8sSpec.java @@ -19,6 +19,10 @@ public class K8sSpec { @JsonProperty("Mem") private String mem; + + // Spider's normalized capacity. Provider KeyValueList can contain unrelated zero values. + @JsonProperty("MemSizeMib") + private String memSizeMib; @JsonProperty("Gpu") private Object gpu; @@ -43,4 +47,4 @@ public static class KeyValue { @JsonProperty("value") private String value; } -} \ No newline at end of file +} diff --git a/src/main/java/kr/co/mcmp/softwarecatalog/application/service/impl/SpecValidationServiceImpl.java b/src/main/java/kr/co/mcmp/softwarecatalog/application/service/impl/SpecValidationServiceImpl.java index 213bba4..2ec0fcd 100644 --- a/src/main/java/kr/co/mcmp/softwarecatalog/application/service/impl/SpecValidationServiceImpl.java +++ b/src/main/java/kr/co/mcmp/softwarecatalog/application/service/impl/SpecValidationServiceImpl.java @@ -153,6 +153,16 @@ public K8sSpec getSpecForK8s(String namespace, String clusterName) { * @return 메모리 값 문자열 (단위 포함) */ private String getMemoryValueFromSpec(K8sSpec spec) { + // Prefer Spider's normalized MiB field over provider-specific metadata (e.g. Alibaba Memory=0). + // Keep the legacy fallbacks for older Spider versions that do not supply this field. + if (StringUtils.isNotBlank(spec.getMemSizeMib())) { + try { + double mib = Double.parseDouble(spec.getMemSizeMib().trim()); + if (Double.isFinite(mib) && mib > 0) return java.math.BigDecimal.valueOf(mib).toPlainString() + "MiB"; + } catch (NumberFormatException ignored) { + log.debug("Invalid normalized memory capacity; trying legacy fields"); + } + } // 1. mem 필드 확인 if (spec.getMem() != null && !spec.getMem().trim().isEmpty()) { log.debug("Using mem field: {}", spec.getMem()); @@ -286,4 +296,3 @@ private double convertMemoryToGB(String memoryValue) { } } } - diff --git a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicy.java b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicy.java index 9bcb86b..ed10a65 100644 --- a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicy.java +++ b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicy.java @@ -18,8 +18,8 @@ static void validate(HelmChart chart, DeploymentConfigDTO config) { throw new IllegalArgumentException(app.title() + " supports one persistent instance; disable HPA and use one replica."); }); } - static void validateStorage(HelmChart chart, KubernetesClient client, DeploymentRequest request) { - if (BuiltInHelmCharts.app(chart).filter(BuiltInHelmCharts.App::persistent).isEmpty()) return; + static boolean validateStorage(HelmChart chart, KubernetesClient client, DeploymentRequest request) { + if (BuiltInHelmCharts.app(chart).filter(BuiltInHelmCharts.App::persistent).isEmpty()) return false; if (request != null && Boolean.TRUE.equals(request.getWorkloadRebalancingEnabled())) throw new IllegalArgumentException("Single-instance persistent applications do not support workload rebalancing."); Map extra = extra(request); @@ -35,6 +35,9 @@ static void validateStorage(HelmChart chart, KubernetesClient client, Deployment if (Quantity.getAmountInBytes(Quantity.parse(size(extra))).compareTo( BigDecimal.valueOf(minimum).multiply(BigDecimal.valueOf(1073741824L))) < 0) throw new IllegalArgumentException("This StorageClass requires at least " + minimum + "Gi."); + boolean cinder = NhnStorageClassService.DRIVER.equals(storage.getProvisioner()); + if (cinder) NhnStorageClassService.requireDriverReady(client); + return cinder; } static String size(Map extra) { String size = Objects.toString(extra.get("storageSize"), "10Gi"); @@ -44,10 +47,16 @@ static String size(Map extra) { return size; } static void configure(HelmChart chart, DeploymentRequest request, Map cli, Map yaml) { + configure(chart, request, cli, yaml, false); + } + static void configure(HelmChart chart, DeploymentRequest request, Map cli, Map yaml, + boolean prepareCinderVolume) { BuiltInHelmCharts.app(chart).ifPresent(app -> { cli.put("securityContext.runAsNonRoot", "true"); if (app.persistent()) { cli.put("persistence.enabled", "true"); + // Only the server's actual StorageClass lookup may enable the bounded root init container. + cli.put("volumePermissions.enabled", Boolean.toString(prepareCinderVolume)); // Typed YAML preserves literal StorageClass names instead of interpreting Helm --set syntax. yaml.put("persistence", Map.of("enabled", true, "storageClass", Objects.toString(extra(request).get("storageClass"), "").trim(), diff --git a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmChartService.java b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmChartService.java index c96ed63..0023fa8 100644 --- a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmChartService.java +++ b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmChartService.java @@ -285,7 +285,7 @@ public Release deployHelmChartWithRequest(KubernetesClient client, String namesp DeploymentConfigDTO config = DeploymentConfigDTO.from(request, catalog); HelmIngressValues.validate(helmChart, config); BuiltInHelmPolicy.validate(helmChart, config); - BuiltInHelmPolicy.validateStorage(helmChart, client, request); + boolean prepareCinderVolume = BuiltInHelmPolicy.validateStorage(helmChart, client, request); Path tempKubeconfigPath = null; Path tempValuesPath = null; Path tempChartPath = null; @@ -379,7 +379,7 @@ public Release deployHelmChartWithRequest(KubernetesClient client, String namesp } applyObjectStorageValues(catalog, request, providerName, helmChart.getChartName(), chartValues); - BuiltInHelmPolicy.configure(helmChart, request, values, chartValues); + BuiltInHelmPolicy.configure(helmChart, request, values, chartValues, prepareCinderVolume); K8sIngressPolicy.configureValues(helmChart.getChartName(), values, chartValues, config, ingressCidr); if (!chartValues.isEmpty()) { tempValuesPath = createTempValuesFile(chartValues); diff --git a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassService.java b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassService.java index 569111d..9369995 100644 --- a/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassService.java +++ b/src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassService.java @@ -29,22 +29,29 @@ public Capability capability(String namespace, String clusterName) { } catch (RuntimeException e) { throw StorageOperationException.translate(e); } } static Capability inspect(KubernetesClient client) { - if (client.storage().v1().csiDrivers().withName(DRIVER).get() == null) - return new Capability(true, false, false, - "cinder-csi-plugin is not registered. Install the managed add-on in NHN NKS, then refresh StorageClasses."); - var csiNodes = client.storage().v1().csiNodes().list().getItems(); - boolean registered = !csiNodes.isEmpty() && csiNodes.stream() - .allMatch(n -> n.getSpec() != null && n.getSpec().getDrivers() != null && - n.getSpec().getDrivers().stream().anyMatch(d -> DRIVER.equals(d.getName()))); - if (!registered) return new Capability(true, false, false, - "The Cinder CSI driver is still registering on worker nodes. Refresh after the add-on is ready."); + String issue = driverIssue(client); + if (issue != null) return new Capability(true, false, false, issue); var review = client.authorization().v1().selfSubjectAccessReview().create(new SelfSubjectAccessReviewBuilder() .withNewSpec().withNewResourceAttributes().withGroup("storage.k8s.io") .withResource("storageclasses").withVerb("create").endResourceAttributes().endSpec().build()); boolean allowed = review.getStatus() != null && Boolean.TRUE.equals(review.getStatus().getAllowed()); - return new Capability(true, true, allowed, allowed ? "Cinder CSI is ready. Create an NHN notebook StorageClass (ReadWriteOnce)." : + return new Capability(true, true, allowed, allowed ? "Cinder CSI is ready. Create an NHN application StorageClass (ReadWriteOnce)." : "AM needs cluster-level permission to create storageclasses.storage.k8s.io."); } + // Existing StorageClass users need driver readiness, not permission to create cluster-wide classes. + static void requireDriverReady(KubernetesClient client) { + String issue = driverIssue(client); + if (issue != null) throw new StorageOperationException(400, "STORAGE_CLASS_SETUP_REQUIRED", issue); + } + private static String driverIssue(KubernetesClient client) { + if (client.storage().v1().csiDrivers().withName(DRIVER).get() == null) + return "cinder-csi-plugin is not registered. Install the managed add-on in NHN NKS, then refresh StorageClasses."; + var csiNodes = client.storage().v1().csiNodes().list().getItems(); + boolean registered = !csiNodes.isEmpty() && csiNodes.stream() + .allMatch(n -> n.getSpec() != null && n.getSpec().getDrivers() != null && + n.getSpec().getDrivers().stream().anyMatch(d -> DRIVER.equals(d.getName()))); + return registered ? null : "The Cinder CSI driver is still registering on worker nodes. Refresh after the add-on is ready."; + } public K8sStorageClassDTO create(String namespace, String clusterName, CreateRequest request) { if (request == null || request.name() == null || request.name().length() > 63 || !request.name().matches("[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")) @@ -57,7 +64,7 @@ public K8sStorageClassDTO create(String namespace, String clusterName, CreateReq // Create only: never replace existing classes or change the cluster default. var sc = client.storage().v1().storageClasses().resource(new StorageClassBuilder() .withNewMetadata().withName(request.name()).endMetadata() - .withProvisioner(DRIVER).withParameters(Map.of("type", request.diskType())) + .withProvisioner(DRIVER).withParameters(Map.of("type", request.diskType(), "csi.storage.k8s.io/fstype", "ext4")) .withVolumeBindingMode("WaitForFirstConsumer").withReclaimPolicy("Retain").build()).create(); return KubernetesStorageClassService.toDto(sc); } catch (RuntimeException e) { throw StorageOperationException.translate(e); } diff --git a/src/main/resources/helm/README.md b/src/main/resources/helm/README.md index fa4182d..6ac4ad8 100644 --- a/src/main/resources/helm/README.md +++ b/src/main/resources/helm/README.md @@ -66,5 +66,30 @@ Do not use broad namespace or label deletion against shared/archiving workloads. proof of a successful CSP deployment: check readiness, native protocol access, authentication rejection, restart persistence, and exact-resource cleanup. -Chart template/image changes require a new chart version and a deliberate migration -policy; do not silently overwrite user-customized mappings or running releases. +This compatibility fix intentionally keeps the bundled chart version at `0.1.0`. +The running AM image supplies the chart templates; record its image digest/source +commit to distinguish installations made before and after the fix. Startup does +not rewrite these five mappings' existing chart versions or running releases. +Future version changes need an explicit migration and rollback policy; never +overwrite custom mappings. + +## NHN Cinder compatibility (chart version remains 0.1.0) + +The persistent Redis, MariaDB and PostgreSQL charts check that the selected Cinder +CSI driver is registered before installation. Missing CSI is a cluster prerequisite: +AM reports the managed `cinder-csi-plugin` setup requirement, but does not install +the shared add-on or change the default StorageClass automatically. The existing +NHN StorageClass creation action now explicitly sets `ext4`, with `Retain` and +`WaitForFirstConsumer`; pre-existing classes are never modified. + +For a selected Cinder class, AM enables a short-lived init container using the same +digest-pinned application image. It changes ownership and mode only on the PVC +mount root, not recursively on existing data. The application remains non-root. +Other drivers and the stateless HTTP apps do not get this initializer. A cluster +enforcing restricted Pod Security may reject the root init container; AM does not +weaken namespace admission policy to bypass that restriction. + +Reinstalling through an AM image containing this fix uses the updated templates +without a catalog-version migration. Reinstalling through an old AM image does not +include the fix. Existing running Helm releases, VM configurations and custom +chart mappings remain unchanged until an explicit deployment action. diff --git a/src/main/resources/helm/common/templates/workload.yaml b/src/main/resources/helm/common/templates/workload.yaml index 17618ca..0195128 100644 --- a/src/main/resources/helm/common/templates/workload.yaml +++ b/src/main/resources/helm/common/templates/workload.yaml @@ -28,6 +28,26 @@ spec: fsGroup: {{ .Values.uid }} fsGroupChangePolicy: OnRootMismatch seccompProfile: {type: RuntimeDefault} + {{- if and .Values.persistence.enabled .Values.volumePermissions.enabled }} + initContainers: + - name: prepare-data-volume + image: {{ .Values.image | quote }} + imagePullPolicy: IfNotPresent + command: ["sh", "-ec"] + # No recursive chown: preserve existing files; prepare only this PVC's mount root. + args: [{{ printf "chown %d:%d /mnt/data && chmod 2770 /mnt/data" (int .Values.uid) (int .Values.uid) | quote }}] + securityContext: + runAsNonRoot: false + runAsUser: 0 + runAsGroup: 0 + allowPrivilegeEscalation: false + capabilities: {drop: ["ALL"], add: ["CHOWN", "FOWNER", "DAC_OVERRIDE"]} + seccompProfile: {type: RuntimeDefault} + resources: + requests: {cpu: 10m, memory: 16Mi} + limits: {cpu: 100m, memory: 64Mi} + volumeMounts: [{name: data, mountPath: /mnt/data}] + {{- end }} containers: - name: {{ .Chart.Name }} image: {{ .Values.image | quote }} diff --git a/src/main/resources/helm/common/values.yaml b/src/main/resources/helm/common/values.yaml index 0a7086b..64a0c8c 100644 --- a/src/main/resources/helm/common/values.yaml +++ b/src/main/resources/helm/common/values.yaml @@ -12,3 +12,5 @@ container: {} dataPath: /data uid: 1001 containerPort: 8080 +volumePermissions: + enabled: false diff --git a/src/test/java/kr/co/mcmp/softwarecatalog/application/service/impl/K8sMemorySpecTest.java b/src/test/java/kr/co/mcmp/softwarecatalog/application/service/impl/K8sMemorySpecTest.java new file mode 100644 index 0000000..0715bc4 --- /dev/null +++ b/src/test/java/kr/co/mcmp/softwarecatalog/application/service/impl/K8sMemorySpecTest.java @@ -0,0 +1,49 @@ +package kr.co.mcmp.softwarecatalog.application.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.mcmp.ape.cbtumblebug.dto.K8sSpec; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import java.util.List; +import kr.co.mcmp.softwarecatalog.CatalogService; +import kr.co.mcmp.softwarecatalog.SoftwareCatalogDTO; +import kr.co.mcmp.ape.cbtumblebug.api.CbtumblebugRestApi; +import kr.co.mcmp.softwarecatalog.application.repository.DeploymentHistoryRepository; + +class K8sMemorySpecTest { + private final SpecValidationServiceImpl service = new SpecValidationServiceImpl(null, null, null); + private double memory(String json) throws Exception { + var spec = new ObjectMapper().readValue(json, K8sSpec.class); + String raw = ReflectionTestUtils.invokeMethod(service, "getMemoryValueFromSpec", spec); + return ReflectionTestUtils.invokeMethod(service, "convertMemoryToGB", raw); + } + @Test void normalizedSpiderCapacityWinsOverAlibabaZeroAndLegacyValues() throws Exception { + assertThat(memory(""" + {"MemSizeMib":"16384","KeyValueList":[{"key":"MemorySize","value":"16.00"},{"key":"Memory","value":"0"}]} + """)).isEqualTo(16); + assertThat(memory("{\"MemSizeMib\":8192,\"Mem\":\"4GB\"}")).isEqualTo(8); + assertThat(memory("{\"MemSizeMib\":\"512\"}")).isEqualTo(0.5); + assertThat(memory("{\"MemSizeMib\":\" 4096 \"}")).isEqualTo(4); + } + @Test void olderResponsesAndInvalidNormalizedFieldsStillUseLegacyData() throws Exception { + for (String invalid : new String[]{"", "0", "-1", "invalid", "NaN", "Infinity"}) + assertThat(memory("{\"MemSizeMib\":\"" + invalid + "\",\"Mem\":\"8GiB\"}")).isEqualTo(8); + assertThat(memory("{\"Mem\":\"16GB\"}")).isEqualTo(16); + assertThat(memory("{\"KeyValueList\":[{\"key\":\"MemorySizeMib\",\"value\":\"8192\"}]}")).isEqualTo(8); + assertThat(memory("{\"KeyValueList\":[{\"key\":\"Memory\",\"value\":\"4\"}]}")).isEqualTo(4); + } + @Test void realSpecDecisionUsesCapacityAndStillRejectsOversizedApplications() throws Exception { + var catalogs = mock(CatalogService.class); + var histories = mock(DeploymentHistoryRepository.class); + var validation = spy(new SpecValidationServiceImpl(catalogs, mock(CbtumblebugRestApi.class), histories)); + var spec = new ObjectMapper().readValue("{\"MemSizeMib\":\"16384\",\"VCpu\":{\"Count\":\"4\"},\"KeyValueList\":[{\"key\":\"Memory\",\"value\":\"0\"}]}", K8sSpec.class); + doReturn(spec).when(validation).getSpecForK8s("test", "cluster"); + when(histories.findByNamespaceAndClusterNameAndActionTypeNotAndStatus(any(), any(), any(), any())).thenReturn(List.of()); + for (double requirement : new double[]{0.5, 1, 8, 16, 16.1, 32}) { + when(catalogs.getCatalog(1L)).thenReturn(SoftwareCatalogDTO.builder().recommendedCpu(1d).recommendedMemory(requirement).build()); + assertThat(validation.checkSpecForK8s("test", "cluster", 1L)).isEqualTo(requirement <= 16); + } + } +} diff --git a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmChartsTest.java b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmChartsTest.java index f19d73e..5e0d0d1 100644 --- a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmChartsTest.java +++ b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmChartsTest.java @@ -28,21 +28,24 @@ static HelmChart chart(String name) { Map entries = read(archive); assertThat(entries).hasSize(9); Map metadata = new Yaml().load(entries.get(app.chart() + "/Chart.yaml")); - assertThat(metadata).containsEntry("name", app.chart()).doesNotContainKey("dependencies"); + assertThat(metadata).containsEntry("name", app.chart()).containsEntry("version", "0.1.0") + .doesNotContainKey("dependencies"); Map values = new Yaml().load(entries.get(app.chart() + "/values.yaml")); assertThat(values.get("image").toString()).matches(".+@sha256:[a-f0-9]{64}"); assertThat(((Map)values.get("persistence")).get("enabled")).isEqualTo(app.persistent()); assertThat(BuiltInHelmCharts.app(chart(app.chart()))).contains(app); // Used by the explicit Helm lint/render and real-cluster test commands; these are production archives. - Files.copy(archive, exports.resolve(app.chart() + "-0.1.0.tgz"), StandardCopyOption.REPLACE_EXISTING); + Files.copy(archive, exports.resolve(app.chart() + "-" + BuiltInHelmCharts.VERSION + ".tgz"), StandardCopyOption.REPLACE_EXISTING); } finally { Files.deleteIfExists(archive); } } } @Test void doesNotInterceptUserOrExternalChartsOrUnknownVersions() { var custom = chart("redis"); custom.setChartRepositoryUrl("https://custom.example.org"); assertThat(BuiltInHelmCharts.app(custom)).isEmpty(); - custom = chart("redis"); custom.setChartVersion("2.0.0"); - assertThat(BuiltInHelmCharts.app(custom)).isEmpty(); + for (String version : new String[]{"0.1.1", "1.0.0", "2.0.0", "", null}) { + custom = chart("redis"); custom.setChartVersion(version); + assertThat(BuiltInHelmCharts.app(custom)).isEmpty(); + } custom = chart("redis"); custom.setPackageId("custom"); assertThat(BuiltInHelmCharts.app(custom)).isEmpty(); assertThat(BuiltInHelmCharts.app(chart("../secret"))).isEmpty(); diff --git a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicyTest.java b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicyTest.java index 0561d53..38f68d5 100644 --- a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicyTest.java +++ b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmPolicyTest.java @@ -3,6 +3,8 @@ import java.util.*; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.storage.StorageClassBuilder; +import io.fabric8.kubernetes.api.model.storage.CSIDriverBuilder; +import io.fabric8.kubernetes.api.model.storage.CSINodeBuilder; import io.fabric8.kubernetes.client.server.mock.*; import io.fabric8.kubernetes.client.KubernetesClient; import kr.co.mcmp.softwarecatalog.application.dto.*; @@ -11,6 +13,30 @@ @EnableKubernetesMockClient(crud = true) class BuiltInHelmPolicyTest { KubernetesClient client; + @Test void cinderRequiresReadyDriverAndEnablesOnlyBoundedInitializationForDataApps() { + client.storage().v1().storageClasses().resource(new StorageClassBuilder().withNewMetadata().withName("cinder").endMetadata() + .withProvisioner(NhnStorageClassService.DRIVER).build()).create(); + var req = DeploymentRequest.builder().additionalConfig(Map.of("storageClass", "cinder", "storageSize", "10Gi")).build(); + var chart = BuiltInHelmChartsTest.chart("redis"); + assertThatThrownBy(() -> BuiltInHelmPolicy.validateStorage(chart, client, req)).hasMessageContaining("cinder-csi-plugin"); + client.storage().v1().csiDrivers().resource(new CSIDriverBuilder().withNewMetadata() + .withName(NhnStorageClassService.DRIVER).endMetadata().build()).create(); + assertThatThrownBy(() -> BuiltInHelmPolicy.validateStorage(chart, client, req)).hasMessageContaining("registering"); + client.storage().v1().csiNodes().resource(new CSINodeBuilder().withNewMetadata().withName("worker").endMetadata() + .withNewSpec().addNewDriver().withName(NhnStorageClassService.DRIVER).withNodeID("worker").endDriver().endSpec().build()).create(); + for (String app : List.of("redis", "mariadb", "postgresql", "apache", "tomcat")) { + var appChart = BuiltInHelmChartsTest.chart(app); + boolean cinder = BuiltInHelmPolicy.validateStorage(appChart, client, req); + var cli = new HashMap(); var yaml = new HashMap(); + BuiltInHelmPolicy.configure(appChart, req, cli, yaml, cinder); + if (List.of("redis", "mariadb", "postgresql").contains(app)) { + assertThat(cinder).isTrue(); + assertThat(cli).containsEntry("volumePermissions.enabled", "true"); + } else { + assertThat(cinder).isFalse(); assertThat(cli).doesNotContainKey("volumePermissions.enabled"); + } + } + } @Test void persistentAppsRejectHttpIngressHpaAndMultipleReplicas() { for (String app : List.of("redis", "mariadb", "postgresql")) { var chart = BuiltInHelmChartsTest.chart(app); diff --git a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmRenderTest.java b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmRenderTest.java index d2c12ed..4c84911 100644 --- a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmRenderTest.java +++ b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/BuiltInHelmRenderTest.java @@ -23,10 +23,22 @@ class BuiltInHelmRenderTest { Map deploy = ofKind(docs, "Deployment"); Map pod = map(map(map(deploy.get("spec")).get("template")).get("spec")); assertThat(map(pod.get("securityContext"))).containsEntry("runAsNonRoot", true); + assertThat(pod).doesNotContainKey("initContainers"); var container = map(((List)pod.get("containers")).get(0)); assertThat(container).containsKeys("readinessProbe", "livenessProbe"); assertThat(map(ofKind(docs, "Service").get("spec"))).containsEntry("type", "ClusterIP"); if (app.persistent()) { + String cinder = run(true, "template", "cinder", path.toString(), "--set", "volumePermissions.enabled=true"); + List> cinderDocs = new ArrayList<>(); + for (Object obj : new Yaml().loadAll(cinder)) if (obj instanceof Map) cinderDocs.add((Map)obj); + var cinderPod = map(map(map(ofKind(cinderDocs, "Deployment").get("spec")).get("template")).get("spec")); + var init = map(((List)cinderPod.get("initContainers")).get(0)); + assertThat(init.get("image")).isEqualTo(container.get("image")); + assertThat(map(init.get("securityContext"))).containsEntry("runAsUser", 0).containsEntry("runAsNonRoot", false) + .containsEntry("allowPrivilegeEscalation", false); + int uid = app.chart().equals("postgresql") ? 70 : 999; + assertThat(init.get("args")).isEqualTo(List.of("chown " + uid + ":" + uid + " /mnt/data && chmod 2770 /mnt/data")); + assertThat(map(cinderPod.get("securityContext"))).containsEntry("runAsNonRoot", true); assertThat(ofKind(docs, "PersistentVolumeClaim")).isNotNull(); assertThat(map(ofKind(docs, "Secret").get("data"))).containsKeys("password", "root-password"); run(false, "template", "bad", path.toString(), "--set", "replicaCount=2"); diff --git a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassServiceTest.java b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassServiceTest.java index e87e8de..7456a63 100644 --- a/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassServiceTest.java +++ b/src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/NhnStorageClassServiceTest.java @@ -83,4 +83,20 @@ class NhnStorageClassServiceTest { assertThat(result.canCreate()).isFalse(); verifyNoInteractions(clients); } + @Test void createsExt4RetainedNonDefaultClassWithoutMutatingExistingClasses() { + var clients = mock(KubernetesClientFactory.class); + when(clients.getClient("test", "cluster")).thenReturn(client); + var service = spy(new NhnStorageClassService(clients, mock(CbtumblebugRestApi.class))); + doReturn(new NhnStorageClassService.Capability(true, true, true, "ready")) + .when(service).capability("test", "cluster"); + service.create("test", "cluster", new NhnStorageClassService.CreateRequest("am-test", "General HDD")); + // Service closes its client. A fresh mock-server client inspects the persisted resource. + try (var check = new io.fabric8.kubernetes.client.KubernetesClientBuilder().withConfig(client.getConfiguration()).build()) { + var sc = check.storage().v1().storageClasses().withName("am-test").get(); + assertThat(sc.getParameters()).containsEntry("csi.storage.k8s.io/fstype", "ext4").containsEntry("type", "General HDD"); + assertThat(sc.getReclaimPolicy()).isEqualTo("Retain"); + assertThat(sc.getVolumeBindingMode()).isEqualTo("WaitForFirstConsumer"); + assertThat(sc.getMetadata().getAnnotations()).doesNotContainKey("storageclass.kubernetes.io/is-default-class"); + } + } } diff --git a/src/test/java/kr/co/mcmp/util/BuiltInHelmCatalogTest.java b/src/test/java/kr/co/mcmp/util/BuiltInHelmCatalogTest.java index 76182ff..57fe2bb 100644 --- a/src/test/java/kr/co/mcmp/util/BuiltInHelmCatalogTest.java +++ b/src/test/java/kr/co/mcmp/util/BuiltInHelmCatalogTest.java @@ -31,6 +31,19 @@ CREATE TABLE HELM_CHART (CATALOG_ID BIGINT UNIQUE, CHART_NAME VARCHAR, CHART_VER ReflectionTestUtils.setField(initializer, "jdbcTemplate", jdbc); } private void sync() { ReflectionTestUtils.invokeMethod(initializer, "ensureBuiltInHelmCatalogs"); } + @Test void keepsBundledVersionWithoutRewritingExistingMappings() { + sync(); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM HELM_CHART WHERE CHART_VERSION='0.1.0'", Integer.class)).isEqualTo(5); + jdbc.update("UPDATE HELM_CHART SET CHART_VERSION='0.1.1' WHERE CHART_NAME='tomcat'"); + jdbc.update("UPDATE HELM_CHART SET CHART_VERSION='custom' WHERE CHART_NAME='postgresql'"); + jdbc.update("UPDATE HELM_CHART SET CHART_REPOSITORY_URL='https://custom.example.org' WHERE CHART_NAME='apache'"); + sync(); sync(); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM HELM_CHART WHERE CHART_VERSION='0.1.0'", Integer.class)).isEqualTo(3); + assertThat(jdbc.queryForObject("SELECT CHART_VERSION FROM HELM_CHART WHERE CHART_NAME='tomcat'", String.class)).isEqualTo("0.1.1"); + assertThat(jdbc.queryForObject("SELECT CHART_VERSION FROM HELM_CHART WHERE CHART_NAME='postgresql'", String.class)).isEqualTo("custom"); + assertThat(jdbc.queryForObject("SELECT CHART_VERSION FROM HELM_CHART WHERE CHART_NAME='apache'", String.class)).isEqualTo("0.1.0"); + assertThat(jdbc.queryForObject("SELECT CHART_REPOSITORY_URL FROM HELM_CHART WHERE CHART_NAME='apache'", String.class)).isEqualTo("https://custom.example.org"); + } @Test void cleanDatabaseCreatesFiveAndRepeatedStartupIsIdempotent() { sync(); sync(); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM SOFTWARE_CATALOG", Integer.class)).isEqualTo(5);