From effdaae5620ad24c8b4d9b9d21564c5b978ee5b5 Mon Sep 17 00:00:00 2001 From: eunjuny Date: Wed, 9 Sep 2026 17:59:56 +0900 Subject: [PATCH 1/4] feat: improve deployment targeting and ingress safety --- applicationFE/package.json | 2 + .../scripts/test-ingress-preflight.mjs | 173 ++++ applicationFE/scripts/test-vm-clustering.mjs | 101 +++ applicationFE/src/api/softwareCatalog.ts | 13 + applicationFE/src/utils/vmClustering.ts | 18 + .../applicationInstallationForm.vue | 604 +++++++++---- doc/helm-ingress-mapping.md | 192 +++++ doc/network-access-scope-test.html | 792 ++++++++++++++++++ .../controller/ApplicationController.java | 12 + .../application/dto/DeploymentConfigDTO.java | 2 +- .../application/dto/DeploymentRequest.java | 7 +- .../application/dto/DeploymentRequestDTO.java | 2 + .../dto/K8sIngressCheckRequest.java | 26 + .../dto/K8sIngressCheckResult.java | 6 + .../application/model/DeploymentHistory.java | 2 +- .../impl/ApplicationHistoryServiceImpl.java | 5 +- .../ApplicationOrchestrationServiceImpl.java | 6 + .../service/impl/DockerDeploymentService.java | 36 +- .../impl/VmNodeGroupTargetResolver.java | 107 +++ .../config/KubernetesNamespaces.java | 17 + .../kubernetes/service/HelmChartService.java | 200 +++-- .../kubernetes/service/HelmIngressValues.java | 155 ++++ .../kubernetes/service/K8sIngressPolicy.java | 32 +- .../service/KubernetesDeployService.java | 35 +- .../KubernetesIngressPreflightService.java | 70 ++ .../KubernetesIngressRouteValidator.java | 110 +++ .../service/KubernetesIngressTlsWarnings.java | 43 + .../service/KubernetesMonitoringService.java | 11 +- .../service/KubernetesOperationService.java | 26 +- .../static/assets/IconPlus-BOL0vjBp.js | 16 + .../assets/InstallSoftwareIframe-mJiyHWup.js | 1 + .../static/assets/OssList-D2OI2UM1.js | 17 + .../assets/RepositoryDetail-O1k2zX2-.js | 1 + ...ue_type_script_setup_true_lang-B3H_ilsl.js | 15 + .../static/assets/RepositoryList-C9jiLgJk.js | 1 + ...ue_type_script_setup_true_lang-BlwG_2zL.js | 17 + .../static/assets/SoftwareCatalog-0Lgs7aMw.js | 102 +++ .../SoftwareCatalogListTest-DHufUnKL.js | 11 + ...ue_vue_type_style_index_0_lang-CbSawsMs.js | 22 + .../static/assets/YamlGenerate-COjTeWAK.js | 1 + .../applicationInstallationForm-BIUFxLef.js | 1 + .../applicationInstallationForm-CIrFSkx4.css | 1 + .../static/assets/bootstrap.esm-BBiGMP8d.js | 5 + .../resources/static/assets/index-BYDlO0dm.js | 42 + .../static/assets/lodash-DElnlo5r.js | 27 + .../static/assets/repository-0e01PSMr.js | 1 + .../static/assets/request-CHanIBNf.js | 1 + src/main/resources/static/index.html | 2 +- ...ApplicationIngressCheckControllerTest.java | 69 ++ ...nOrchestrationServiceProjectScopeTest.java | 5 +- .../impl/DockerDeploymentTunnelTest.java | 4 +- .../impl/VmNodeGroupTargetResolverTest.java | 140 ++++ .../HelmChartServiceImageDefaultsTest.java | 23 + .../service/HelmIngressPolicyMergeTest.java | 75 ++ .../service/HelmIngressPreflightTest.java | 106 +++ .../service/HelmIngressRenderTest.java | 154 ++++ .../service/HelmIngressValuesTest.java | 226 +++++ .../service/KubernetesDeployRollbackTest.java | 4 +- ...netesDeployServiceIngressConflictTest.java | 115 +++ .../KubernetesDeployServiceNamespaceTest.java | 111 +++ ...KubernetesIngressPreflightServiceTest.java | 145 ++++ .../KubernetesIngressRouteValidatorTest.java | 170 ++++ .../KubernetesIngressTlsWarningsTest.java | 68 ++ 63 files changed, 4168 insertions(+), 336 deletions(-) create mode 100644 applicationFE/scripts/test-ingress-preflight.mjs create mode 100644 applicationFE/scripts/test-vm-clustering.mjs create mode 100644 applicationFE/src/utils/vmClustering.ts create mode 100644 doc/helm-ingress-mapping.md create mode 100644 doc/network-access-scope-test.html create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/application/dto/K8sIngressCheckRequest.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/application/dto/K8sIngressCheckResult.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/application/service/impl/VmNodeGroupTargetResolver.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/config/KubernetesNamespaces.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmIngressValues.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressPreflightService.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressRouteValidator.java create mode 100644 src/main/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressTlsWarnings.java create mode 100644 src/main/resources/static/assets/IconPlus-BOL0vjBp.js create mode 100644 src/main/resources/static/assets/InstallSoftwareIframe-mJiyHWup.js create mode 100644 src/main/resources/static/assets/OssList-D2OI2UM1.js create mode 100644 src/main/resources/static/assets/RepositoryDetail-O1k2zX2-.js create mode 100644 src/main/resources/static/assets/RepositoryDetail.vue_vue_type_script_setup_true_lang-B3H_ilsl.js create mode 100644 src/main/resources/static/assets/RepositoryList-C9jiLgJk.js create mode 100644 src/main/resources/static/assets/RepositoryList.vue_vue_type_script_setup_true_lang-BlwG_2zL.js create mode 100644 src/main/resources/static/assets/SoftwareCatalog-0Lgs7aMw.js create mode 100644 src/main/resources/static/assets/SoftwareCatalogListTest-DHufUnKL.js create mode 100644 src/main/resources/static/assets/Tabulator.vue_vue_type_style_index_0_lang-CbSawsMs.js create mode 100644 src/main/resources/static/assets/YamlGenerate-COjTeWAK.js create mode 100644 src/main/resources/static/assets/applicationInstallationForm-BIUFxLef.js create mode 100644 src/main/resources/static/assets/applicationInstallationForm-CIrFSkx4.css create mode 100644 src/main/resources/static/assets/bootstrap.esm-BBiGMP8d.js create mode 100644 src/main/resources/static/assets/index-BYDlO0dm.js create mode 100644 src/main/resources/static/assets/lodash-DElnlo5r.js create mode 100644 src/main/resources/static/assets/repository-0e01PSMr.js create mode 100644 src/main/resources/static/assets/request-CHanIBNf.js create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/application/controller/ApplicationIngressCheckControllerTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/application/service/impl/VmNodeGroupTargetResolverTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmIngressPolicyMergeTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmIngressPreflightTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmIngressRenderTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/HelmIngressValuesTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesDeployServiceIngressConflictTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesDeployServiceNamespaceTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressPreflightServiceTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressRouteValidatorTest.java create mode 100644 src/test/java/kr/co/mcmp/softwarecatalog/kubernetes/service/KubernetesIngressTlsWarningsTest.java diff --git a/applicationFE/package.json b/applicationFE/package.json index 9cc94c61..0aa73fe2 100644 --- a/applicationFE/package.json +++ b/applicationFE/package.json @@ -10,6 +10,8 @@ "build-only": "vite build", "type-check": "vue-tsc --build --force", "test:install-integration": "node scripts/test-install-target.mjs", + "test:vm-clustering": "node scripts/test-vm-clustering.mjs", + "test:ingress-preflight": "node scripts/test-ingress-preflight.mjs", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "format": "prettier --write src/" }, diff --git a/applicationFE/scripts/test-ingress-preflight.mjs b/applicationFE/scripts/test-ingress-preflight.mjs new file mode 100644 index 00000000..7e995306 --- /dev/null +++ b/applicationFE/scripts/test-ingress-preflight.mjs @@ -0,0 +1,173 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import ts from 'typescript' +import { ref, watch } from 'vue' + +// Execute the production form functions and its real Vue invalidation watcher (no browser/network). +const source = await readFile(new URL('../src/views/softwareCatalog/components/applicationInstallationForm.vue', import.meta.url), 'utf8') +function between(start, end) { + const begin = source.indexOf(start) + const finish = source.indexOf(end, begin) + assert.ok(begin >= 0 && finish > begin, `Missing production block: ${start}`) + return source.slice(begin, finish) +} +const code = ts.transpileModule([ + 'let specCheckVersion = 0;', + between('const normalizeIngressHost =', 'const _getSoftwareCatalogList ='), + between('const onChangeForm =', 'const onSelectVm ='), + between('const buildIngressPayload =', 'const selectedCatalogInfo ='), + between('watch([selectInfra,', 'watch(selectInfra, async'), + 'return { specCheck, buildIngressPayload, onChangeForm };' +].join('\n'), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText + +function harness(options = {}) { + const calls = [] + const state = Object.fromEntries(Object.entries({ + selectInfra: 'K8S', selectNsId: 'project-a', selectCluster: 'cluster-a', selectMci: 'mci-a', + selectedVmList: ['vm-a'], vmTargetMode: 'VM', selectVmNodeGroupId: '', selectedNodeGroupVmIds: ['vm-a', 'vm-b'], + selectedCatalogIdx: 7, selectedStorageClass: 'standard', projectContextKey: 'ws/project-a', + modalTitle: 'Application Installation', projectScopeError: '', specCheckFlag: true, specChecking: false, + specCheckErrors: [], specCheckWarnings: [], + ingressData: { ingressEnabled: true, ingressHost: 'https://APP.Example.com:30880/ignored', + ingressPath: '/app', ingressClass: 'nginx', ingressTlsEnabled: false, ingressTlsSecret: '' } + }).map(([name, value]) => [name, ref(value)])) + const env = { + ...state, watch, + validateStorageClassSelection: () => true, + toast: { error: message => calls.push(['toast-error', message]), success: message => calls.push(['toast-success', message]) }, + confirm: message => { calls.push(['confirm', message]); return options.confirm ?? true }, + k8sIngressCheck: async payload => { + calls.push(['ingress', payload]) + return options.ingress ? options.ingress(payload) : { data: { valid: true, errors: [], warnings: [] } } + }, + k8sSpecCheck: async payload => { + calls.push(['resources', payload]) + return options.resources ? options.resources(payload) : { data: true } + }, + vmSpecCheck: async payload => { calls.push(['vm', payload]); return { data: true } } + } + const functions = new Function(...Object.keys(env), code)(...Object.values(env)) + return { ...state, ...functions, calls } +} + +let cases = 0 +async function test(name, fn) { await fn(); cases++; console.log(`PASS ${name}`) } +const count = (h, kind) => h.calls.filter(c => c[0] === kind).length + +await test('current normalized inputs are checked before resources; an empty UI Secret is omitted', async () => { + const h = harness() + await h.specCheck() + assert.equal(h.specCheckFlag.value, false) + assert.equal(h.specChecking.value, false) + assert.deepEqual(h.calls.slice(0, 2).map(c => c[0]), ['ingress', 'resources']) + assert.deepEqual(h.calls[0][1], { + namespace: 'project-a', clusterName: 'cluster-a', catalogId: 7, ingressEnabled: true, + ingressHost: 'app.example.com', ingressPath: '/app', ingressClass: 'nginx', ingressTlsEnabled: false, ingressTlsSecret: null + }) + assert.deepEqual(h.calls[0][1], { namespace: 'project-a', clusterName: 'cluster-a', catalogId: 7, ...h.buildIngressPayload() }) +}) + +await test('conflicts cannot enter the low-spec override flow', async () => { + const h = harness({ ingress: () => ({ data: { valid: false, errors: ['Host/Path conflict'], warnings: [] } }) }) + await h.specCheck() + assert.equal(h.specCheckFlag.value, true) + assert.deepEqual(h.specCheckErrors.value, ['Host/Path conflict']) + assert.equal(count(h, 'resources'), 0) + assert.equal(count(h, 'confirm'), 0) +}) + +await test('TLS readiness warnings are visible but do not block deployment', async () => { + const h = harness({ ingress: () => ({ data: { valid: true, errors: [], warnings: ['Secret is not ready'] } }) }) + h.ingressData.value.ingressTlsEnabled = true + h.ingressData.value.ingressTlsSecret = 'selected-cert' + await h.specCheck() + assert.equal(h.specCheckFlag.value, false) + assert.deepEqual(h.specCheckWarnings.value, ['Secret is not ready']) + assert.equal(h.calls[0][1].ingressTlsSecret, 'selected-cert') +}) + +for (const input of ['ingressHost', 'ingressPath', 'ingressClass', 'ingressEnabled', 'ingressTlsEnabled', 'ingressTlsSecret']) { + await test(`changing ${input} invalidates the completed check`, async () => { + const h = harness(); await h.specCheck() + h.specCheckWarnings.value = ['old warning'] + h.ingressData.value[input] = typeof h.ingressData.value[input] === 'boolean' ? !h.ingressData.value[input] : 'changed' + assert.equal(h.specCheckFlag.value, true) + assert.deepEqual(h.specCheckWarnings.value, []) + }) +} +for (const input of ['selectNsId', 'selectCluster', 'selectedCatalogIdx', 'projectContextKey', 'selectedStorageClass']) { + await test(`changing ${input} invalidates the completed check`, async () => { + const h = harness(); await h.specCheck() + h[input].value = input === 'selectedCatalogIdx' ? 8 : 'changed' + assert.equal(h.specCheckFlag.value, true) + }) +} +for (const stage of ['ingress', 'resources']) { + await test(`late ${stage} response cannot validate changed or reverted form inputs`, async () => { + let finish + const pending = new Promise(resolve => { finish = resolve }) + const h = harness({ [stage]: () => pending }) + const run = h.specCheck() + await Promise.resolve(); await Promise.resolve() + const previous = h.ingressData.value.ingressHost + h.ingressData.value.ingressHost = 'changed.example.com' + h.ingressData.value.ingressHost = previous + finish({ data: stage === 'ingress' ? { valid: true, errors: [], warnings: ['stale'] } : true }) + await run + assert.equal(h.specCheckFlag.value, true) + assert.equal(h.specChecking.value, false) + assert.deepEqual(h.specCheckWarnings.value, []) + assert.equal(count(h, 'toast-success'), 0) + }) +} +for (const result of ['reject', 'malformed']) { + await test(`${result} Ingress response fails closed and permits retry`, async () => { + const h = harness({ ingress: () => { if (result === 'reject') throw new Error('private'); return { data: {} } } }) + await h.specCheck() + assert.equal(h.specCheckFlag.value, true) + assert.equal(h.specChecking.value, false) + assert.equal(count(h, 'resources'), 0) + assert.ok(h.specCheckErrors.value.length) + assert.ok(!h.specCheckErrors.value.join().includes('private')) + await h.specCheck() + assert.equal(count(h, 'ingress'), 2) + }) +} +for (const proceed of [false, true]) { + await test(`resource shortage retains existing user override=${proceed}`, async () => { + const h = harness({ resources: () => ({ data: false }), confirm: proceed }) + await h.specCheck() + assert.equal(h.specCheckFlag.value, !proceed) + assert.equal(count(h, 'confirm'), 1) + }) +} +await test('duplicate clicks send only one request while checking', async () => { + let finish + const h = harness({ ingress: () => new Promise(resolve => { finish = resolve }) }) + const first = h.specCheck(); await h.specCheck() + assert.equal(count(h, 'ingress'), 1) + finish({ data: { valid: true } }); await first +}) +await test('VM checks never call the K8s preflight and preserve NodeGroup checks', async () => { + const h = harness(); h.selectInfra.value = 'VM'; h.vmTargetMode.value = 'NODE_GROUP'; h.selectVmNodeGroupId.value = 'group-a' + await h.specCheck() + assert.equal(count(h, 'ingress'), 0) + assert.equal(count(h, 'vm'), 2) + assert.equal(h.specCheckFlag.value, false) +}) +await test('missing target prevents any API call', async () => { + const h = harness(); h.selectCluster.value = ''; await h.specCheck() + assert.equal(count(h, 'ingress'), 0) + assert.equal(h.specCheckFlag.value, true) +}) +await test('retry after correcting the route clears errors and can pass', async () => { + let invalid = true + const h = harness({ ingress: () => ({ data: { valid: !invalid, errors: invalid ? ['conflict'] : [] } }) }) + await h.specCheck(); h.ingressData.value.ingressPath = '/new'; invalid = false; await h.specCheck() + assert.equal(h.specCheckFlag.value, false) + assert.deepEqual(h.specCheckErrors.value, []) +}) + +assert.match(between('const runInstall =', 'const buildIngressPayload ='), /\.\.\.buildIngressPayload\(\)/) +assert.match(between('const runInstall =', 'const buildIngressPayload ='), /specCheckFlag\.value \|\| specChecking\.value/) +console.log(`Ingress preflight form tests passed (${cases} cases).`) diff --git a/applicationFE/scripts/test-vm-clustering.mjs b/applicationFE/scripts/test-vm-clustering.mjs new file mode 100644 index 00000000..10fef27c --- /dev/null +++ b/applicationFE/scripts/test-vm-clustering.mjs @@ -0,0 +1,101 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import * as Vue from 'vue' +import { compile } from '@vue/compiler-dom' +import { renderToString } from '@vue/server-renderer' +import ts from 'typescript' + +const transpile = source => ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } +}).outputText +const policy = await readFile(new URL('../src/utils/vmClustering.ts', import.meta.url), 'utf8') +const { isVmClusteringCatalog } = await import( + `data:text/javascript;base64,${Buffer.from(transpile(policy)).toString('base64')}`) +const catalog = (name, packageName) => ({ name, packageInfo: { packageName } }) +const cases = [ + [catalog('Redis', 'redis'), true], + [catalog(' Redis ', 'library/redis:7.4'), true], + [catalog('Redis 7', 'docker.io/library/redis'), true], + [catalog('redis', 'registry.example:5000/cache/redis'), true], + [catalog('REDIS', 'redis@sha256:abc123'), true], + [catalog('Elasticsearch', 'elasticsearch'), true], + [catalog('Elasticsearch 8', 'docker.elastic.co/elasticsearch/elasticsearch'), true], + [catalog('ELASTICSEARCH', 'library/elasticsearch:8.17.0'), true], + [catalog('Nginx', 'nginx'), false], + [catalog('Grafana', 'grafana/grafana'), false], + [catalog('Jenkins', 'jenkins/jenkins'), false], + [catalog('Rclone GUI', 'rclone/rclone'), false], + [catalog('Redis Exporter', 'oliver006/redis_exporter'), false], + [catalog('Redis Insight', 'redis/redisinsight'), false], + [catalog('Elasticsearch Exporter', 'prometheuscommunity/elasticsearch-exporter'), false], + [catalog('Redis', 'nginx'), false], + [catalog('Elasticsearch', 'redis'), false], + [catalog('Redis Elasticsearch', 'redis'), false], + [catalog('My cache', 'redis'), false], // Existing backend cannot pick Redis ports from this label. + [catalog('Notredis', 'redis'), false], + [{ name: 'Redis' }, false], + [{ name: 'Redis', packageInfo: null }, false], + [null, false], + [undefined, false] +] +for (const [value, expected] of cases) assert.equal(isVmClusteringCatalog(value), expected, JSON.stringify(value)) + +const form = await readFile(new URL('../src/views/softwareCatalog/components/applicationInstallationForm.vue', import.meta.url), 'utf8') +const start = form.indexOf('const canSelectClustering = computed(') +const end = form.indexOf('\nconst selectedClusterProvider', start) +assert.ok(start > 0 && end > start) +const makeAvailability = new Function('computed', 'watch', 'selectInfra', 'vmTargetMode', + 'isTargetLocked', 'selectedCatalogInfo', 'selectDeploymentType', 'isVmClusteringCatalog', + transpile(form.slice(start, end)) + '\nreturn canSelectClustering;') +const radio = form.match(/
[\s\S]*?<\/div>/)?.[0] +assert.ok(radio, 'The actual Clustering radio must use the eligibility condition') +const render = new Function('Vue', compile(radio, { mode: 'function', prefixIdentifiers: true }).code)(Vue) +let renderedCases = 0 +for (const [value, eligible] of cases) { + for (const infra of ['VM', 'K8S', '']) { + for (const target of ['VM', 'NODE_GROUP']) { + for (const locked of [false, true]) { + const scope = Vue.effectScope() + const selection = Vue.ref('Standalone') + const available = scope.run(() => makeAvailability(Vue.computed, Vue.watch, + Vue.ref(infra), Vue.ref(target), Vue.ref(locked), Vue.ref(value), selection, isVmClusteringCatalog)) + const expected = eligible && infra === 'VM' && target === 'VM' && !locked + assert.equal(available.value, expected) + const html = await renderToString(Vue.createSSRApp({ + setup: () => ({ canSelectClustering: available, selectDeploymentType: selection, isTargetLocked: locked }), render + })) + assert.equal(html.includes('id="Clustering"'), expected) + scope.stop() + renderedCases++ + } + } + } +} + +for (const change of ['catalog', 'target', 'locked', 'infra']) { + const scope = Vue.effectScope() + const infra = Vue.ref('VM'), target = Vue.ref('VM'), locked = Vue.ref(false) + const info = Vue.ref(catalog('Redis', 'redis')), selection = Vue.ref('Clustering') + scope.run(() => makeAvailability(Vue.computed, Vue.watch, infra, target, locked, info, selection, isVmClusteringCatalog)) + if (change === 'catalog') info.value = catalog('Grafana', 'grafana/grafana') + if (change === 'target') target.value = 'NODE_GROUP' + if (change === 'locked') locked.value = true + if (change === 'infra') infra.value = 'K8S' + assert.equal(selection.value, 'Standalone', `Reset synchronously after ${change} changes`) + scope.stop() +} + +// Exercise the actual submit guard, including a stale/modified hidden selection. +const submitStart = form.indexOf('const runInstall = async () => {') + 'const runInstall = async () => {'.length +const guardEnd = form.indexOf("\n if (modalTitle.value === 'Application Installation' && (specCheckFlag", submitStart) +assert.ok(guardEnd > submitStart) +const submitGuard = new Function('modalTitle', 'selectInfra', 'selectDeploymentType', 'canSelectClustering', 'toast', + transpile(form.slice(submitStart, guardEnd)) + '\nreturn "continue";') +const installation = { value: 'Application Installation' } +let errors = 0 +assert.equal(submitGuard(installation, { value: 'VM' }, { value: 'Clustering' }, { value: false }, { error: () => errors++ }), undefined) +assert.equal(errors, 1) +assert.equal(submitGuard(installation, { value: 'VM' }, { value: 'Standalone' }, { value: false }, {}), 'continue') +assert.equal(submitGuard(installation, { value: 'VM' }, { value: 'Clustering' }, { value: true }, {}), 'continue') +assert.equal(submitGuard({ value: 'Application Uninstallation' }, { value: 'VM' }, { value: 'Clustering' }, { value: false }, {}), 'continue') +console.log(`VM clustering passed: ${cases.length} catalog cases, ${renderedCases} rendered visibility cases, 4 transitions, 4 submit cases.`) diff --git a/applicationFE/src/api/softwareCatalog.ts b/applicationFE/src/api/softwareCatalog.ts index 25c52bca..5a0aa273 100644 --- a/applicationFE/src/api/softwareCatalog.ts +++ b/applicationFE/src/api/softwareCatalog.ts @@ -36,6 +36,7 @@ export const runVmInstall = (params: { namespace: string, mciId: string, vmIds: string[], + vmNodeGroupId?: string, clusterName: string, catalogId: number, servicePort?: number, @@ -124,6 +125,18 @@ export const k8sSpecCheck = (params: { return request.get(`/applications/k8s/check?namespace=${params.namespace}&clusterName=${params.clusterName}&catalogId=${params.catalogId}`) } +export const k8sIngressCheck = (params: { + namespace: string, + clusterName: string, + catalogId: number, + ingressEnabled?: boolean, + ingressHost?: string, + ingressPath?: string, + ingressClass?: string, + ingressTlsEnabled?: boolean, + ingressTlsSecret?: string | null +}) => request.post('/applications/k8s/ingress/check', params) + export const getBuildLogList = (jobName: string) => { return request.get(`/ape/log/${jobName}`) } diff --git a/applicationFE/src/utils/vmClustering.ts b/applicationFE/src/utils/vmClustering.ts new file mode 100644 index 00000000..a1b17032 --- /dev/null +++ b/applicationFE/src/utils/vmClustering.ts @@ -0,0 +1,18 @@ +interface VmClusteringCatalog { + name?: string + packageInfo?: { packageName?: string } | null +} + +/** UI eligibility only; this does not certify that a deployed cluster is healthy. */ +export function isVmClusteringCatalog(catalog?: VmClusteringCatalog | null): boolean { + const image = String(catalog?.packageInfo?.packageName || '').trim().toLowerCase() + const imageName = image.split('/').pop()?.split(/[:@]/)[0] + if (imageName !== 'redis' && imageName !== 'elasticsearch') return false + + // The existing VM backend chooses its port profile from the catalog name, + // but its runtime configuration from the image name. Both must agree. + // In particular, Redis exporters/insights are not Redis server clusters. + const name = String(catalog?.name || '').trim().toLowerCase() + return new RegExp(`(^|[^a-z0-9])${imageName}([^a-z0-9]|$)`).test(name) + && !(imageName === 'redis' && name.includes('elasticsearch')) +} diff --git a/applicationFE/src/views/softwareCatalog/components/applicationInstallationForm.vue b/applicationFE/src/views/softwareCatalog/components/applicationInstallationForm.vue index 11b1145f..f54a510d 100644 --- a/applicationFE/src/views/softwareCatalog/components/applicationInstallationForm.vue +++ b/applicationFE/src/views/softwareCatalog/components/applicationInstallationForm.vue @@ -41,31 +41,31 @@
-

Select the Infra what is the Infra will be installed

-

Select the Infra what is the Infra will be uninstalled

-
- -

Select the namespace where the application will be installed

-

Select the namespace where the application will be uninstalled

- + - + -