From 1c878d6ae2a355b63cc106147a6629116db108c6 Mon Sep 17 00:00:00 2001 From: yh-noh Date: Tue, 30 Jun 2026 10:50:09 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(pmk):=20NodeGroup=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20=EA=B2=BD=EC=9A=B0=20control=20plane=EB=A7=8C=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NodeGroup 없는 경우: GetAvailableK8sVersion으로 K8s 버전 동적 조회 후 version 필드 설정 - NodeGroup 없는 경우: 불필요한 pre-validation(checkK8sClusterDynamic) 제거 - NodeGroup 없는 경우: GetAvailableK8sNodeImage 호출 제거 (UNDER DEVELOPMENT API) - NodeGroup 없는 경우: CSP별 하드코딩 specId/imageId 제거 → imageId는 "default" 사용 - NodeGroup 있는 경우: pre-validation 유지 - pmk_api.js: getAvailablek8sClusterNodeImage await 누락 수정 및 반환값 통일 --- .../assets/js/common/api/services/pmk_api.js | 5 +- front/assets/js/pages/operation/manage/pmk.js | 124 ++++-------------- 2 files changed, 29 insertions(+), 100 deletions(-) diff --git a/front/assets/js/common/api/services/pmk_api.js b/front/assets/js/common/api/services/pmk_api.js index 37f5c023..87368ada 100644 --- a/front/assets/js/common/api/services/pmk_api.js +++ b/front/assets/js/common/api/services/pmk_api.js @@ -486,12 +486,11 @@ export async function getAvailablek8sClusterNodeImage(providerName, regionName) }; var controller = "/api/" + "mc-infra-manager/" + "GetAvailableK8sNodeImage"; - const response = webconsolejs["common/api/http"].commonAPIPost( + const response = await webconsolejs["common/api/http"].commonAPIPost( controller, data ) - var imageList = response - return imageList + return response.data.responseData } diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index 34ab2af8..af7c8fb4 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -1605,20 +1605,29 @@ export async function deployPmkDynamic() { const isNodeGroupVisible = nodeGroupForm && nodeGroupForm.style.display !== "none"; try { - // 사전 검증을 위한 commonSpec 결정 let commonSpec = ""; let commonImage = ""; + let k8sVersion = ""; if (isNodeGroupVisible) { - // NodeGroup이 있는 경우: 선택된 spec 사용 + // NodeGroup이 있는 경우: 선택된 spec으로 사전 검증 후 배포 commonSpec = $("#nodegroup_commonSpecId_dynamic").val(); commonImage = $("#nodegroup_image_dynamic").val(); if (!commonSpec) { webconsolejs['common/util'].showToast('Please select NodeGroup spec', 'warning'); return; } + + const checkResult = await webconsolejs["common/api/services/pmk_api"].checkK8sClusterDynamic( + selectedWorkspaceProject.nsId, + commonSpec + ); + if (!checkResult || checkResult.status !== 200) { + webconsolejs['common/util'].showToast('Failed to pre-validate. Please check the settings', 'error'); + return; + } } else { - // NodeGroup이 없는 경우: API를 통해 동적으로 spec과 image 가져오기 + // NodeGroup이 없는 경우: K8s 버전만 동적 조회 (EKS control plane만 생성) const providerName = clusterData.provider; const regionMatch = clusterData.region.match(/\[.*?\]\s*(.+)/); const regionName = regionMatch ? regionMatch[1] : ''; @@ -1628,86 +1637,34 @@ export async function deployPmkDynamic() { return; } - const specsResponse = await webconsolejs["common/api/services/pmk_api"] + const versions = await webconsolejs["common/api/services/pmk_api"] .getAvailableK8sClusterVersion(providerName, regionName); - - const imagesResponse = await webconsolejs["common/api/services/pmk_api"] - .getAvailablek8sClusterNodeImage(providerName, regionName); - - if (specsResponse && specsResponse.data && specsResponse.data.responseData) { - const specs = specsResponse.data.responseData; - if (Array.isArray(specs) && specs.length > 0) { - commonSpec = specs[0]; - } else if (typeof specs === 'object' && specs.version) { - commonSpec = specs.version[0] || ""; - } - } - - if (imagesResponse && imagesResponse.data && imagesResponse.data.responseData) { - const images = imagesResponse.data.responseData; - if (Array.isArray(images) && images.length > 0) { - commonImage = images[0]; - } else if (typeof images === 'object' && images.image) { - commonImage = images.image[0] || "default"; - } - } - - if (!commonImage || commonImage === "") { - commonImage = "default"; - } - - if (!commonSpec || commonSpec === "") { - webconsolejs['common/util'].showToast('Could not retrieve K8s specification for the selected Provider and Region', 'error'); - return; + if (versions && Array.isArray(versions) && versions.length > 0) { + k8sVersion = versions[0].id || ""; } + commonImage = "default"; } - // 사전 검증 API 호출 (동기 - 결과 확인 필요) - const checkResult = await webconsolejs["common/api/services/pmk_api"].checkK8sClusterDynamic( - selectedWorkspaceProject.nsId, - commonSpec - ); - - if (!checkResult || checkResult.status !== 200) { - webconsolejs['common/util'].showToast('Failed to pre-validate. Please check the settings', 'error'); - return; - } - - // 실제 클러스터 생성 데이터 준비 - let createData; - - // Azure provider인 경우 테스트용 하드코딩된 값 사용 - if (clusterData.provider.toLowerCase() === 'azure') { - createData = { - imageId: "default", - specId: "azure+koreacentral+standard_b4ms", - name: clusterData.name, // 폼에서 입력한 값 사용 - nodeGroupName: isNodeGroupVisible ? $("#nodegroup_name_dynamic").val() : "k8sng01" // 폼에서 입력한 값이 있으면 사용, 없으면 기본값 - }; - } else { - // 다른 provider는 기존 로직 사용 - createData = { - imageId: commonImage, - specId: commonSpec, - connectionName: clusterData.connection, - name: clusterData.name, - nodeGroupName: isNodeGroupVisible ? $("#nodegroup_name_dynamic").val() : "" - }; - } + // 클러스터 생성 데이터 준비 + const createData = { + imageId: commonImage || "default", + specId: commonSpec, + connectionName: clusterData.connection, + name: clusterData.name, + nodeGroupName: isNodeGroupVisible ? $("#nodegroup_name_dynamic").val() : "" + }; - // commonImage가 없으면 "default"로 설정 - if (!createData.commonImage || createData.commonImage === "") { - createData.commonImage = "default"; + // NodeGroup 없는 경우 동적 조회한 K8s 버전 설정 + if (k8sVersion) { + createData.version = k8sVersion; } // NodeGroup이 있는 경우 추가 정보 설정 if (isNodeGroupVisible) { - // NodeGroup 필수 필드 검증 if (!createData.nodeGroupName) { webconsolejs['common/util'].showToast('Please input NodeGroup name', 'warning'); return; } - // AutoScaling On일 때만 min/max 포함 const autoScalingVal = $("#nodegroup_autoscaling_dynamic").val(); createData.onAutoScaling = autoScalingVal || "false"; if (autoScalingVal === "true") { @@ -1722,33 +1679,6 @@ export async function deployPmkDynamic() { } } - // available k8sversion 조회 - if(clusterData.provider.toLowerCase() === 'alibaba'){ - //createData.k8sVersion = "1.33.3-aliyun.1"; - //createData.k8sVersion = "1.33";//(사용못함 format 안맞음) - //createData.k8sVersion = "1.31.9-aliyun.1";// - //createData.k8sVersion = "1.22.15-aliyun.1"; - // createData.k8sVersion = "1.32.7-aliyun.1"; - createData.k8sVersion = "1.32.7-aliyun.1"; - } - // const k8sVersionList = await webconsolejs["common/api/services/pmk_api"].getAvailableK8sVersionList( - // selectedWorkspaceProject.nsId - // ); - // if (k8sVersionList && k8sVersionList.status === 200) { - // console.log(k8sVersionList); - // } - // // 가져온 k8sversion 중 가장 최신 버전 선택 - // if (k8sVersionList && k8sVersionList.data && k8sVersionList.data.responseData && k8sVersionList.data.responseData.length > 0) { - // const latestK8sVersion = k8sVersionList.data.responseData[0]; - // if (!latestK8sVersion) { - // if(clusterData.provider.toLowerCase() === 'alibaba'){ - // latestK8sVersion = "1.33.3-aliyun.1"; - // } - // }else{ - // createData.k8sVersion = latestK8sVersion; - // } - // } - // 동적 클러스터 생성 API 호출 (비동기 - 결과를 기다리지 않음) webconsolejs["common/api/services/pmk_api"].createK8sClusterDynamic( selectedWorkspaceProject.nsId, From ec587f9a34601287572d4dd52a15ea13b232cf84 Mon Sep 17 00:00:00 2001 From: yh-noh Date: Tue, 30 Jun 2026 12:40:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(pmk):=20NodeGroup=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=ED=95=84=EB=93=9C=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - desiredNodeSize/minNodeSize/maxNodeSize/rootDiskSize를 int로 변환 (tumblebug 타입 불일치 해소) - createNode 검증을 name/specId/imageId 필수로 완화 (autoScaling OFF 시 min/maxNodeSize 없음) - clustercreate.js에서 createNode await 추가 및 검증 실패 시 early return 처리 - RecommendK8sNode 실패 시 CSP별 default specId fallback 적용 (NodeGroup-less 배포용) --- .../assets/js/common/api/services/pmk_api.js | 44 ++++++++++++++++--- front/assets/js/pages/operation/manage/pmk.js | 27 ++++++++++-- .../operation/manage/clustercreate.js | 12 ++--- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/front/assets/js/common/api/services/pmk_api.js b/front/assets/js/common/api/services/pmk_api.js index 87368ada..1b582a25 100644 --- a/front/assets/js/common/api/services/pmk_api.js +++ b/front/assets/js/common/api/services/pmk_api.js @@ -392,11 +392,11 @@ export async function createNode(k8sClusterId, nsId, Create_Node_Config_Arr) { var obj = Create_Node_Config_Arr[0]; - // 2. 필수 필드 검증 - if (!obj.name || !obj.specId || !obj.imageId || !obj.sshKeyId || !obj.minNodeSize || !obj.maxNodeSize || !obj.onAutoScaling) { + // 2. 필수 필드 검증 (min/maxNodeSize는 autoScaling OFF 시 없을 수 있으므로 제외) + if (!obj.name || !obj.specId || !obj.imageId) { console.error('Missing required fields:', obj); webconsolejs["common/util"].showToast('Missing required fields for node creation', 'error'); - return; + return false; } // 3. 데이터 준비 (기본값 포함) @@ -406,13 +406,13 @@ export async function createNode(k8sClusterId, nsId, Create_Node_Config_Arr) { k8sClusterId: k8sClusterId, }, request: { - "desiredNodeSize": obj.desiredNodeSize || "1", + "desiredNodeSize": parseInt(obj.desiredNodeSize) || 1, "imageId": obj.imageId, - "maxNodeSize": obj.maxNodeSize || obj.desiredNodeSize || "1", - "minNodeSize": obj.minNodeSize || obj.desiredNodeSize || "1", + "maxNodeSize": parseInt(obj.maxNodeSize) || parseInt(obj.desiredNodeSize) || 1, + "minNodeSize": parseInt(obj.minNodeSize) || parseInt(obj.desiredNodeSize) || 1, "name": obj.name, "onAutoScaling": obj.onAutoScaling || "false", - "rootDiskSize": obj.rootDiskSize || "", + "rootDiskSize": parseInt(obj.rootDiskSize) || 0, "rootDiskType": obj.rootDiskType || "", "specId": obj.specId, "sshKeyId": obj.sshKeyId @@ -495,6 +495,36 @@ export async function getAvailablek8sClusterNodeImage(providerName, regionName) } +// connectionName에 맞는 K8s 노드 spec을 동적 조회 (RecommendK8sNode) +// connectionName이 일치하는 첫 번째 specId를 반환; 없거나 오류 시 "" +export async function getRecommendedK8sSpecId(connectionName) { + const data = { + request: { + limit: 200 + } + }; + + var controller = "/api/" + "mc-infra-manager/" + "RecommendK8sNode"; + try { + const response = await webconsolejs["common/api/http"].commonAPIPost( + controller, + data + ); + + if (!response || !response.data || !response.data.responseData) { + return ""; + } + + const specs = response.data.responseData; + if (!Array.isArray(specs)) return ""; + + const matched = specs.find(spec => spec.connectionName === connectionName); + return matched ? (matched.id || "") : ""; + } catch (e) { + return ""; + } +} + // // MCIS 상태를 UI에서 표현하는 방식으로 변경 export function getPmkStatusFormatter(pmkFullStatus) { // console.log("getPmkStatusFormatter " + pmkFullStatus); diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index af7c8fb4..51ad4a01 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -1627,21 +1627,43 @@ export async function deployPmkDynamic() { return; } } else { - // NodeGroup이 없는 경우: K8s 버전만 동적 조회 (EKS control plane만 생성) + // NodeGroup이 없는 경우: K8s 버전 + specId 동적 조회 후 control plane만 생성 const providerName = clusterData.provider; const regionMatch = clusterData.region.match(/\[.*?\]\s*(.+)/); - const regionName = regionMatch ? regionMatch[1] : ''; + const regionName = regionMatch ? regionMatch[1].trim() : ''; if (!providerName || !regionName) { webconsolejs['common/util'].showToast('Please select both Provider and Region', 'warning'); return; } + // K8s 버전 조회 const versions = await webconsolejs["common/api/services/pmk_api"] .getAvailableK8sClusterVersion(providerName, regionName); if (versions && Array.isArray(versions) && versions.length > 0) { k8sVersion = versions[0].id || ""; } + + // provider 컨텍스트용 specId: RecommendK8sNode 조회 → 없으면 CSP별 하드코딩 fallback + commonSpec = await webconsolejs["common/api/services/pmk_api"] + .getRecommendedK8sSpecId(clusterData.connection); + if (!commonSpec) { + const K8S_DEFAULT_SPEC = { + 'aws': 't3.medium', + 'azure': 'Standard_B2s', + 'gcp': 'n1-standard-2', + 'alibaba': 'ecs.c1.small', + 'ncp': 'SVR.VSVR.STAND.C002.M004.NET.SSD.B050.G002', + 'nhncloud': 'm2.c4m8', + 'ibm': 'cx2-4x8', + }; + const sepIdx = clusterData.connection.indexOf('-'); + const csp = clusterData.connection.substring(0, sepIdx).toLowerCase(); + const region = clusterData.connection.substring(sepIdx + 1); + const instanceType = K8S_DEFAULT_SPEC[csp] || 't3.medium'; + commonSpec = `${csp}+${region}+${instanceType}`; + } + commonImage = "default"; } @@ -1654,7 +1676,6 @@ export async function deployPmkDynamic() { nodeGroupName: isNodeGroupVisible ? $("#nodegroup_name_dynamic").val() : "" }; - // NodeGroup 없는 경우 동적 조회한 K8s 버전 설정 if (k8sVersion) { createData.version = k8sVersion; } diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index da2e105a..3e34478e 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -486,14 +486,14 @@ export async function createNode() { var selectedNsId = selectedWorkspaceProject.nsId; var k8sClusterId = webconsolejs["pages/operation/manage/pmk"].selectedPmkObj[0].id - // NodeGroup 생성 요청만 보내고 결과를 기다리지 않음 (fire and forget) - webconsolejs["common/api/services/pmk_api"].createNode( - k8sClusterId, - selectedNsId, + const result = await webconsolejs["common/api/services/pmk_api"].createNode( + k8sClusterId, + selectedNsId, Create_Node_Config_Arr ); - - // 즉시 메시지 표시 + + if (result === false) return; + webconsolejs['common/util'].showToast('NodeGroup creation request has been sent', 'info'); // NodeGroup Configuration 폼 닫기 From bc5ce706f4df30ee36b840eff025cf1c56c85c86 Mon Sep 17 00:00:00 2001 From: yh-noh Date: Tue, 30 Jun 2026 13:44:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(pmk):=20Spec/Image=20Recommendation=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Node 시 Spec Recommendation 팝업에서 선택된 클러스터의 provider 자동 필터링 (selectedPmkObj fallback 추가 — cluster_provider_dynamic이 없는 경우 대응) - Image Recommendation 팝업에 is Kubernetes Image 토글 추가 (기본 ON) K8s 이미지가 없을 때 OFF로 전환해 전체 이미지 재조회 가능 --- .../operation/manage/clustercreate.js | 1 + .../manage/pmk_imagerecommendation.js | 34 ++++++++--- .../manage/pmk_serverrecommendation.js | 1 + .../manage/_pmk_imagerecommendation.html | 56 ++++++++++++------- 4 files changed, 63 insertions(+), 29 deletions(-) diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 3e34478e..5f91c3dd 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -432,6 +432,7 @@ export async function displayNewNodeForm() { webconsolejs["partials/operation/manage/clustercreate"].callbackNodegroupServerRecommendation ); } + // provider 필터는 pmk_serverrecommendation.js의 shown.bs.modal 핸들러가 selectedPmkObj로 처리 }); } diff --git a/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js b/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js index eeaf1b15..827ffaac 100644 --- a/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js +++ b/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js @@ -54,7 +54,7 @@ export function selectOSTypePmk(osType) { export function updateGPUStatusPmk() { var gpuCheckbox = document.getElementById('assist_gpu_image-pmk'); var gpuValue = document.getElementById('gpu_image_value-pmk'); - + if (gpuCheckbox.checked) { gpuValue.value = 'true'; } else { @@ -62,6 +62,17 @@ export function updateGPUStatusPmk() { } } +export function updateK8sStatusPmk() { + var k8sCheckbox = document.getElementById('assist_k8s_image-pmk'); + var k8sValue = document.getElementById('k8s_image_value-pmk'); + + if (k8sCheckbox.checked) { + k8sValue.value = 'true'; + } else { + k8sValue.value = 'false'; + } +} + function initRecommendImageTablePmk() { // 기존 테이블이 있으면 제거 if (recommendImageTablePmk) { @@ -192,7 +203,8 @@ export async function getRecommendImageInfoPmk() { var osType = $("#assist_os_type-pmk").val() var isGPUImage = $("#gpu_image_value-pmk").val() - + var isK8sImage = $("#k8s_image_value-pmk").val() + // 전역 변수에서 정보 가져오기 var specId = window.selectedPmkSpecInfo.id; // spec의 전체 ID (예: "aws+ap-northeast-2+t2.small") var provider = window.selectedPmkSpecInfo.provider; @@ -206,18 +218,21 @@ export async function getRecommendImageInfoPmk() { // API 호출을 위한 파라미터 구성 var searchParams = { - providerName: provider, // 개별 전달 - regionName: region, // 개별 전달 - matchedSpecId: specId, // 유지 - // isKubernetesImage: true, // PMK 필수 + providerName: provider, + regionName: region, + matchedSpecId: specId, maxResults: 100 }; - + // 선택적 파라미터 if (osType && osType.trim() !== "") { searchParams.osType = osType.trim(); } - + + if (isK8sImage === "true") { + searchParams.isKubernetesImage = true; + } + // GPU 이미지가 필요한 경우에만 추가 if (isGPUImage === "true") { searchParams.isGPUImage = true; @@ -384,6 +399,9 @@ if (!webconsolejs['partials/operation/manage/pmk_imagerecommendation'].setImageS if (!webconsolejs['partials/operation/manage/pmk_imagerecommendation'].filterByProviderPmk) { webconsolejs['partials/operation/manage/pmk_imagerecommendation'].filterByProviderPmk = filterByProviderPmk; } +if (!webconsolejs['partials/operation/manage/pmk_imagerecommendation'].updateK8sStatusPmk) { + webconsolejs['partials/operation/manage/pmk_imagerecommendation'].updateK8sStatusPmk = updateK8sStatusPmk; +} if (!webconsolejs['partials/operation/manage/pmk_imagerecommendation'].updateGPUStatusPmk) { webconsolejs['partials/operation/manage/pmk_imagerecommendation'].updateGPUStatusPmk = updateGPUStatusPmk; } diff --git a/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js b/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js index f008ef5c..f4bb40e3 100644 --- a/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js +++ b/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js @@ -20,6 +20,7 @@ export function initServerRecommendationPmk(callbackfunction) { modalEl.addEventListener('shown.bs.modal', function () { const provider = document.getElementById('cluster_provider_dynamic')?.value || document.getElementById('cluster_provider')?.value + || webconsolejs["pages/operation/manage/pmk"]?.selectedPmkObj?.[0]?.provider || ''; const badge = document.getElementById('spec-provider-badge-pmk'); const hidden = document.getElementById('spec-provider-value-pmk'); diff --git a/front/templates/partials/operation/manage/_pmk_imagerecommendation.html b/front/templates/partials/operation/manage/_pmk_imagerecommendation.html index ff38632f..4fee59e6 100644 --- a/front/templates/partials/operation/manage/_pmk_imagerecommendation.html +++ b/front/templates/partials/operation/manage/_pmk_imagerecommendation.html @@ -110,27 +110,41 @@ -