Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ Once the services are up, you can access the following endpoints:
- Repository UI : `http://<Public_IP>:8081`
- Application Manager UI: `http://<Public_IP>:18084/web`
- OSS Management: `http://<Public_IP>:18084/web/oss/list`
- Application Catalog Management: `http://<Public_IP>:18084/web/softwareCatalog`
- Repository Management: `http://<Public_IP>:18084/web/repository/list`
- Application Catalog Management: `http://<Public_IP>:18084/web/applications/swcatalog`
- Application Status: `http://<Public_IP>:18084/web/applications/status`
- Repository Management: `http://<Public_IP>:18084/web/applications/repository`

The legacy `/web/softwareCatalog` and `/web/repository/*` UI routes redirect to the application routes above for backward compatibility.
- Yaml Generator: `http://<Public_IP>:18084/web/generate/yaml`

### Step 4-1: Certainly! Here’s the translated list of settings:
Expand Down
4 changes: 4 additions & 0 deletions applicationFE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"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",
"test:ibm-ingress-tls": "node scripts/test-ibm-ingress-tls.mjs",
"test:ingress-preparation": "node scripts/test-ingress-preparation.mjs",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
Expand Down
67 changes: 67 additions & 0 deletions applicationFE/scripts/test-ibm-ingress-tls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import ts from 'typescript'
import * as Vue from 'vue'
import { compile } from '@vue/compiler-dom'
import { renderToString } from '@vue/server-renderer'

const source = await readFile(new URL('../src/views/softwareCatalog/components/applicationInstallationForm.vue', import.meta.url), 'utf8')
const template = source.slice(0, source.indexOf('<script'))
const start = source.indexOf('const buildIngressPayload =')
const end = source.indexOf('const specCheck =', start)
const code = ts.transpileModule(source.slice(start,end)+'\nreturn buildIngressPayload;',
{compilerOptions:{target:ts.ScriptTarget.ES2022}}).outputText
let cases=0
async function test(name,fn){await fn();cases++;console.log('PASS '+name)}

for(const ibm of [true,false])for(const tls of [true,false,undefined])for(const host of ['rclone.test.com','app.cluster.containers.appdomain.cloud']){
await test('HTTP-only IBM payload, existing non-IBM TLS preserved '+JSON.stringify({ibm,tls,host}),()=>{
const data={ingressEnabled:true,ingressHost:host,ingressPath:'/',ingressTlsEnabled:tls,ingressTlsSecret:'catalog-cert'}
const build = new Function('ingressData','isIbmCluster','effectiveIngressClass','normalizeIngressHost',code)(
Vue.ref(data),Vue.ref(ibm),Vue.ref(ibm?'public-iks-k8s-nginx':'nginx'),value=>value)
const payload=build()
assert.equal(payload.ingressTlsEnabled,ibm?false:tls)
assert.equal(payload.ingressTlsSecret,ibm?null:'catalog-cert')
assert.equal(payload.ingressHost,host)
assert.deepEqual(data,{ingressEnabled:true,ingressHost:host,ingressPath:'/',ingressTlsEnabled:tls,ingressTlsSecret:'catalog-cert'})
})
}
await test('cluster switching cannot accidentally enable IBM HTTPS',()=>{
const ibm=Vue.ref(false), data=Vue.ref({ingressEnabled:true,ingressHost:'chosen.example.com',ingressTlsEnabled:true,ingressTlsSecret:'existing-cert'})
const build = new Function('ingressData','isIbmCluster','effectiveIngressClass','normalizeIngressHost',code)(data,ibm,Vue.ref('nginx'),value=>value)
assert.equal(build().ingressTlsEnabled,true)
ibm.value=true;assert.equal(build().ingressTlsEnabled,false);assert.equal(build().ingressTlsSecret,null)
ibm.value=false;assert.equal(build().ingressTlsEnabled,true);assert.equal(build().ingressTlsSecret,'existing-cert')
})
await test('protocol control, TLS discovery and preparation text are absent',()=>{
for(const removed of ['ibmIngressProtocol','loadIbmTlsSettings','k8sIngressTlsSettings','ingressPreparationMessage','Use IBM domain','Loading managed HTTPS'])assert.ok(!source.includes(removed),removed)
assert.ok(template.includes('HTTP via the IBM-managed load balancer.'))
assert.ok(template.includes('HTTP does not require a certificate and is not encrypted.'))
})

// Render the actual footer for both modal and embedded modes, not a duplicate UI.
const footerStart=template.indexOf('<div\n class="modal-footer')
const footerEnd=template.indexOf('\n </div>',footerStart)
assert.ok(footerStart>0&&footerEnd>footerStart)
const render=new Function('Vue',compile(template.slice(footerStart,footerEnd),{mode:'function',prefixIdentifiers:true}).code)(Vue)
for(const embedded of [false,true])for(const completed of [false,true])for(const busy of [false,true]){
await test('footer renders completed/busy state '+JSON.stringify({embedded,completed,busy}),async()=>{
const html=await renderToString(Vue.createSSRApp({setup:()=>({
embedded,deploymentCompleted:completed,deploying:busy,modalTitle:'Application Installation',
shouldRunObjectStorageCheck:false,objectStorageChecking:false,objectStorageCheckPassed:true,
specChecking:false,specCheckFlag:false,projectScopeError:'',deployDisabled:completed||busy,
handleCancel(){},viewAppsStatus(){},runObjectStorageCheck(){},specCheck(){},runInstall(){}
}),render}))
assert.equal(html.includes('View Apps Status'),completed)
assert.equal(html.includes('Spec Check'),!completed)
assert.equal(html.includes('Deploying…'),busy&&!completed)
if(completed){assert.ok(html.includes('Close'));assert.ok(!html.includes('>Deploy<'));assert.ok(!html.includes('>Cancel<'))}
const deployButton=html.match(/<button[^>]*>\s*(?:Deploy|Deploying…)\s*<\/button>/)?.[0]
if(!completed){assert.ok(deployButton);assert.ok(!deployButton.includes('data-bs-dismiss'));assert.equal(deployButton.includes('disabled'),busy)}
})
}
await test('completed and busy forms cannot edit deployment inputs',()=>{
assert.match(template,/<fieldset v-show="!deploymentCompleted" :disabled="deploying \|\| deploymentCompleted"/)
assert.match(template,/<div v-if="deploymentCompleted"[^>]*role="status"/)
})
console.log('IBM HTTP and completion UI tests passed ('+cases+' cases).')
202 changes: 202 additions & 0 deletions applicationFE/scripts/test-ingress-preflight.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import ts from 'typescript'
import { ref, watch, computed } 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 isIbmCluster =', 'const selectedVmProvider ='),
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: [], servicePortCidr: '203.0.113.8/32',
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,
computed, selectedClusterProvider: ref(options.provider || 'aws'),
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,
servicePortCidr: '203.0.113.8/32'
})
assert.deepEqual(h.calls[0][1], { namespace: 'project-a', clusterName: 'cluster-a', catalogId: 7, servicePortCidr: '203.0.113.8/32', ...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', 'servicePortCidr']) {
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/)
for (const provider of ['ibm', 'IBM', 'ibm-jp-osa', 'ibmcloud', 'ibm-vpc', 'aws', 'tencent']) {
await test(`provider ${provider} selects its effective Ingress class`, async () => {
const h = harness({ provider }); await h.specCheck()
assert.equal(h.calls[0][1].ingressClass, provider.toLowerCase().startsWith('ibm') ? 'public-iks-k8s-nginx' : 'nginx')
assert.equal(h.ingressData.value.ingressClass, 'nginx', 'catalog defaults remain unchanged')
if (provider.toLowerCase().startsWith('ibm')) {
h.ingressData.value.ingressTlsEnabled = false
h.ingressData.value.ingressTlsSecret = 'stale-catalog-cert'
assert.equal(h.buildIngressPayload().ingressTlsEnabled, false)
assert.equal(h.buildIngressPayload().ingressTlsSecret, null, 'IBM certificate is resolved by the server')
}
})
}
assert.match(between('const runInstall =', 'const buildIngressPayload ='), /openServicePort:.*!isIbmCluster\.value/)
const statusSource = await readFile(new URL('../src/views/softwareCatalog/components/softwareCatalogList.vue', import.meta.url), 'utf8')
const endpointSource = statusSource.slice(statusSource.indexOf('const getEndpoint ='), statusSource.indexOf('const displayValue ='))
const getEndpoint = new Function(ts.transpileModule(endpointSource + '\nreturn getEndpoint', { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText)()
await test('IBM endpoint uses Ingress Host and scheme, not the worker IP or backend port', async () => {
for (const ingressClass of ['public-iks-k8s-nginx', 'private-iks-k8s-nginx']) {
for (const ingressTlsEnabled of [false, true]) {
assert.equal(getEndpoint({ ingressEnabled: true, ingressClass, ingressTlsEnabled, ingressHost: 'app.example.com', ingressPath: '/test' },
{ publicIp: '10.150.0.8', servicePort: 3000 }), `${ingressTlsEnabled ? 'https' : 'http'}://app.example.com/test`)
}
}
assert.equal(getEndpoint({}, { publicIp: '203.0.113.8', servicePort: 8080 }), '203.0.113.8:8080')
})
console.log(`Ingress preflight form tests passed (${cases} cases).`)
Loading
Loading