mirror of https://github.com/apache/cloudstack.git
ui: introduce section-level “advisories” with quick-fix actions (#11763)
* ui: introduce section-level “advisories” with quick-fix actions This change adds a lightweight “advisories” mechanism to section configs and ships the first advisory to help operators satisfy some of the CKS prerequisites. Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * fix Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * fix endpoint.url check Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * label consistency Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * Update ui/src/components/view/AdvisoriesView.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * improvements Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * remove comments Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * allow disabling Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> --------- Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
98debd235f
commit
10e0d42f45
|
|
@ -292,8 +292,10 @@
|
|||
"label.add.isolated.network": "Add Isolated Network",
|
||||
"label.add.kubernetes.cluster": "Add Kubernetes Cluster",
|
||||
"label.add.acl.name": "ACL name",
|
||||
"label.add.latest.kubernetes.iso": "Add latest Kubernetes ISO",
|
||||
"label.add.ldap.account": "Add LDAP Account",
|
||||
"label.add.logical.router": "Add Logical Router to this Network",
|
||||
"label.add.minimum.required.compute.offering": "Add minimum required Compute Offering",
|
||||
"label.add.more": "Add more",
|
||||
"label.add.nodes": "Add Nodes to Kubernetes Cluster",
|
||||
"label.add.netscaler.device": "Add Netscaler Device",
|
||||
|
|
@ -1102,6 +1104,7 @@
|
|||
"label.firstname": "First name",
|
||||
"label.firstname.lower": "firstname",
|
||||
"label.fix.errors": "Fix errors",
|
||||
"label.fix.global.setting": "Fix Global Setting",
|
||||
"label.fixed": "Fixed Offering",
|
||||
"label.for": "for",
|
||||
"label.forcks": "For CKS",
|
||||
|
|
@ -1135,6 +1138,9 @@
|
|||
"label.globo.dns.configuration": "GloboDNS configuration",
|
||||
"label.glustervolume": "Volume",
|
||||
"label.go.back": "Go back",
|
||||
"label.go.to.compute.offerings": "Go to Compute Offerings",
|
||||
"label.go.to.global.settings": "Go to Global Settings",
|
||||
"label.go.to.kubernetes.isos": "Go to Kubernetes ISOs",
|
||||
"label.gpu": "GPU",
|
||||
"label.gpucardid": "GPU Card",
|
||||
"label.gpucardname": "GPU Card",
|
||||
|
|
@ -3058,12 +3064,17 @@
|
|||
"message.add.ip.v6.firewall.rule.failed": "Failed to add IPv6 firewall rule",
|
||||
"message.add.ip.v6.firewall.rule.processing": "Adding IPv6 firewall rule...",
|
||||
"message.add.ip.v6.firewall.rule.success": "Added IPv6 firewall rule",
|
||||
"message.advisory.cks.endpoint.url.not.configured": "Endpoint URL which will be used by Kubernetes clusters is not configured correctly",
|
||||
"message.advisory.cks.min.offering": "No suitable Compute Offering found for Kubernetes cluster nodes with minimum required resources (2 vCPU, 2 GB RAM)",
|
||||
"message.advisory.cks.version.check": "No Kubernetes version found that can be used to deploy a Kubernetes cluster",
|
||||
"message.redeliver.webhook.delivery": "Redeliver this Webhook delivery",
|
||||
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
|
||||
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
|
||||
"message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule",
|
||||
"message.remove.sslcert.failed": "Failed to remove SSL certificate from load balancer",
|
||||
"message.remove.sslcert.processing": "Removing SSL certificate from load balancer...",
|
||||
"message.add.latest.kubernetes.iso.failed": "Failed to add latest Kubernetes ISO",
|
||||
"message.add.minimum.required.compute.offering.kubernetes.cluster.failed": "Failed to add minimum required Compute Offering for Kubernetes cluster nodes",
|
||||
"message.add.netris.controller": "Add Netris Provider",
|
||||
"message.add.nsx.controller": "Add NSX Provider",
|
||||
"message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>",
|
||||
|
|
@ -3104,9 +3115,13 @@
|
|||
"message.add.vpn.gateway": "Please confirm that you want to add a VPN Gateway.",
|
||||
"message.add.vpn.gateway.failed": "Adding VPN gateway failed",
|
||||
"message.add.vpn.gateway.processing": "Adding VPN gateway...",
|
||||
"message.added.latest.kubernetes.iso": "Latest Kubernetes ISO added successfully",
|
||||
"message.added.minimum.required.compute.offering.kubernetes.cluster": "Minimum required Compute Offering for Kubernetes cluster nodes added successfully",
|
||||
"message.added.vpc.offering": "Added VPC offering",
|
||||
"message.adding.firewall.policy": "Adding Firewall Policy",
|
||||
"message.adding.host": "Adding host",
|
||||
"message.adding.latest.kubernetes.iso": "Adding latest Kubernetes ISO",
|
||||
"message.adding.minimum.required.compute.offering.kubernetes.cluster": "Adding minimum required Compute Offering for Kubernetes cluster nodes",
|
||||
"message.adding.netscaler.device": "Adding Netscaler device",
|
||||
"message.adding.netscaler.provider": "Adding Netscaler provider",
|
||||
"message.adding.nodes.to.cluster": "Adding nodes to Kubernetes cluster",
|
||||
|
|
@ -3544,6 +3559,8 @@
|
|||
"message.failed.to.remove": "Failed to remove",
|
||||
"message.forgot.password.success": "An email has been sent to your email address with instructions on how to reset your password.",
|
||||
"message.generate.keys": "Please confirm that you would like to generate new API/Secret keys for this User.",
|
||||
"message.global.setting.updated": "Global Setting updated successfully.",
|
||||
"message.global.setting.update.failed": "Failed to update Global Setting.",
|
||||
"message.chart.statistic.info": "The shown charts are self-adjustable, that means, if the value gets close to the limit or overpass it, it will grow to adjust the shown value",
|
||||
"message.chart.statistic.info.hypervisor.additionals": "The metrics data depend on the hypervisor plugin used for each hypervisor. The behavior can vary across different hypervisors. For instance, with KVM, metrics are real-time statistics provided by libvirt. In contrast, with VMware, the metrics are averaged data for a given time interval controlled by configuration.",
|
||||
"message.guest.traffic.in.advanced.zone": "Guest Network traffic is communication between end-user Instances. Specify a range of VLAN IDs or VXLAN Network identifiers (VNIs) to carry guest traffic for each physical Network.",
|
||||
|
|
|
|||
|
|
@ -140,3 +140,7 @@ export function oauthlogin (arg) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getBaseUrl () {
|
||||
return vueProps.axios.defaults.baseURL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="advisory in advisories" :key="advisory.id" style="margin-bottom: 10px;">
|
||||
<a-alert
|
||||
:type="advisory.severity || 'info'"
|
||||
:show-icon="true"
|
||||
:closable="true"
|
||||
:message="$t(advisory.message)"
|
||||
@close="onAlertClose(advisory)">
|
||||
<template #description>
|
||||
<a-space direction="horizontal" size="small">
|
||||
<span v-for="(action, idx) in advisory.actions" :key="idx">
|
||||
<a-button
|
||||
v-if="typeof action.show === 'function' ? action.show($store) : action.show"
|
||||
size="small"
|
||||
:type="(action.primary || advisory.actions.length === 1) ? 'primary' : 'default'"
|
||||
@click="onAlertBtnClick(action, advisory)">
|
||||
{{ $t(action.label) }}
|
||||
</a-button>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const DISMISSED_ADVISORIES_KEY = 'dismissed_advisories'
|
||||
|
||||
export default {
|
||||
name: 'AdvisoriesView',
|
||||
components: {
|
||||
},
|
||||
props: {},
|
||||
data () {
|
||||
return {
|
||||
advisories: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.evaluateAdvisories()
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
async evaluateAdvisories () {
|
||||
this.advisories = []
|
||||
const metaAdvisories = this.$route.meta.advisories || []
|
||||
const dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
|
||||
const advisoryPromises = metaAdvisories.map(async advisory => {
|
||||
if (dismissedAdvisories.includes(advisory.id)) {
|
||||
return null
|
||||
}
|
||||
const active = await Promise.resolve(advisory.condition(this.$store))
|
||||
if (active) {
|
||||
return advisory
|
||||
} else if (advisory.dismissOnConditionFail) {
|
||||
this.dismissAdvisory(advisory.id, true)
|
||||
}
|
||||
return null
|
||||
})
|
||||
const results = await Promise.all(advisoryPromises)
|
||||
this.advisories = results.filter(a => a !== null)
|
||||
},
|
||||
onAlertClose (advisory) {
|
||||
this.dismissAdvisory(advisory.id)
|
||||
},
|
||||
dismissAdvisory (advisoryId, skipUpdateLocal) {
|
||||
let dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
|
||||
dismissedAdvisories = dismissedAdvisories.filter(id => id !== advisoryId)
|
||||
dismissedAdvisories.push(advisoryId)
|
||||
this.$localStorage.set(DISMISSED_ADVISORIES_KEY, dismissedAdvisories)
|
||||
if (skipUpdateLocal) {
|
||||
return
|
||||
}
|
||||
this.advisories = this.advisories.filter(advisory => advisory.id !== advisoryId)
|
||||
},
|
||||
undismissAdvisory (advisory, evaluate) {
|
||||
let dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
|
||||
dismissedAdvisories = dismissedAdvisories.filter(id => id !== advisory.id)
|
||||
this.$localStorage.set(DISMISSED_ADVISORIES_KEY, dismissedAdvisories)
|
||||
if (evaluate) {
|
||||
Promise.resolve(advisory.condition(this.$store)).then(active => {
|
||||
if (active) {
|
||||
this.advisories.push(advisory)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.advisories.push(advisory)
|
||||
}
|
||||
},
|
||||
handleAdvisoryActionError (action, advisory, evaluate) {
|
||||
if (action.errorMessage) {
|
||||
this.showActionMessage('error', advisory.id, action.errorMessage)
|
||||
}
|
||||
this.undismissAdvisory(advisory, evaluate)
|
||||
},
|
||||
handleAdvisoryActionResult (action, advisory, result) {
|
||||
if (result && action.successMessage) {
|
||||
this.showActionMessage('success', advisory.id, action.successMessage)
|
||||
return
|
||||
}
|
||||
this.handleAdvisoryActionError(action, advisory, false)
|
||||
},
|
||||
showActionMessage (type, key, content) {
|
||||
const data = {
|
||||
content: this.$t(content),
|
||||
key: key,
|
||||
duration: type === 'loading' ? 0 : 3
|
||||
}
|
||||
if (type === 'loading') {
|
||||
this.$message.loading(data)
|
||||
} else if (type === 'success') {
|
||||
this.$message.success(data)
|
||||
} else if (type === 'error') {
|
||||
this.$message.error(data)
|
||||
} else {
|
||||
this.$message.info(data)
|
||||
}
|
||||
},
|
||||
onAlertBtnClick (action, advisory) {
|
||||
this.dismissAdvisory(advisory.id)
|
||||
if (typeof action.run !== 'function') {
|
||||
return
|
||||
}
|
||||
if (action.loadingLabel) {
|
||||
this.showActionMessage('loading', advisory.id, action.loadingLabel)
|
||||
}
|
||||
const result = action.run(this.$store, this.$router)
|
||||
if (result instanceof Promise) {
|
||||
result.then(success => {
|
||||
this.handleAdvisoryActionResult(action, advisory, success)
|
||||
}).catch(() => {
|
||||
this.handleAdvisoryActionError(action, advisory, true)
|
||||
})
|
||||
} else {
|
||||
this.handleAdvisoryActionResult(action, advisory, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -81,6 +81,7 @@ function generateRouterMap (section) {
|
|||
filters: child.filters,
|
||||
params: child.params ? child.params : {},
|
||||
columns: child.columns,
|
||||
advisories: !vueProps.$config.advisoriesDisabled ? child.advisories : undefined,
|
||||
details: child.details,
|
||||
searchFilters: child.searchFilters,
|
||||
related: child.related,
|
||||
|
|
@ -180,6 +181,10 @@ function generateRouterMap (section) {
|
|||
map.meta.columns = section.columns
|
||||
}
|
||||
|
||||
if (!vueProps.$config.advisoriesDisabled && section.advisories) {
|
||||
map.meta.advisories = section.advisories
|
||||
}
|
||||
|
||||
if (section.actions) {
|
||||
map.meta.actions = section.actions
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
import { shallowRef, defineAsyncComponent } from 'vue'
|
||||
import store from '@/store'
|
||||
import { isZoneCreated } from '@/utils/zone'
|
||||
import { getAPI, postAPI, getBaseUrl } from '@/api'
|
||||
import { getLatestKubernetesIsoParams } from '@/utils/acsrepo'
|
||||
import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline'
|
||||
|
||||
export default {
|
||||
|
|
@ -582,6 +584,182 @@ export default {
|
|||
}
|
||||
],
|
||||
resourceType: 'KubernetesCluster',
|
||||
advisories: [
|
||||
{
|
||||
id: 'cks-min-offering',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.min.offering',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
if (!('listServiceOfferings' in store.getters.apis)) {
|
||||
return false
|
||||
}
|
||||
const params = {
|
||||
cpunumber: 2,
|
||||
memory: 2048,
|
||||
issystem: false
|
||||
}
|
||||
try {
|
||||
const json = await getAPI('listServiceOfferings', params)
|
||||
const offerings = json?.listserviceofferingsresponse?.serviceoffering || []
|
||||
return !offerings.some(o => !o.iscustomized)
|
||||
} catch (error) {}
|
||||
return false
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
primary: true,
|
||||
label: 'label.add.minimum.required.compute.offering',
|
||||
loadingLabel: 'message.adding.minimum.required.compute.offering.kubernetes.cluster',
|
||||
show: (store) => { return ('createServiceOffering' in store.getters.apis) },
|
||||
run: async () => {
|
||||
const params = {
|
||||
name: 'CKS Instance',
|
||||
cpunumber: 2,
|
||||
cpuspeed: 1000,
|
||||
memory: 2048,
|
||||
iscustomized: false,
|
||||
issystem: false
|
||||
}
|
||||
try {
|
||||
const json = await postAPI('createServiceOffering', params)
|
||||
if (json?.createserviceofferingresponse?.serviceoffering) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
return false
|
||||
},
|
||||
successMessage: 'message.added.minimum.required.compute.offering.kubernetes.cluster',
|
||||
errorMessage: 'message.add.minimum.required.compute.offering.kubernetes.cluster.failed'
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.compute.offerings',
|
||||
show: (store) => { return ('listServiceOfferings' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'computeoffering' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cks-version-check',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.version.check',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
const api = 'listKubernetesSupportedVersions'
|
||||
if (!(api in store.getters.apis)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const json = await getAPI(api, {})
|
||||
const versions = json?.listkubernetessupportedversionsresponse?.kubernetessupportedversion || []
|
||||
return versions.length === 0
|
||||
} catch (error) {}
|
||||
return false
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
primary: true,
|
||||
label: 'label.add.latest.kubernetes.iso',
|
||||
loadingLabel: 'message.adding.latest.kubernetes.iso',
|
||||
show: (store) => { return ('addKubernetesSupportedVersion' in store.getters.apis) },
|
||||
run: async () => {
|
||||
let arch = 'x86_64'
|
||||
if ('listClusters' in store.getters.apis) {
|
||||
try {
|
||||
const json = await getAPI('listClusters', { allocationstate: 'Enabled', page: 1, pagesize: 1 })
|
||||
const cluster = json?.listclustersresponse?.cluster?.[0] || {}
|
||||
arch = cluster.architecture || 'x86_64'
|
||||
} catch (error) {}
|
||||
}
|
||||
const params = await getLatestKubernetesIsoParams(arch)
|
||||
try {
|
||||
const json = await postAPI('addKubernetesSupportedVersion', params)
|
||||
if (json?.addkubernetessupportedversionresponse?.kubernetessupportedversion) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
return false
|
||||
},
|
||||
successMessage: 'message.added.latest.kubernetes.iso',
|
||||
errorMessage: 'message.add.latest.kubernetes.iso.failed'
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.kubernetes.isos',
|
||||
show: true,
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'kubernetesiso' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'cks-endpoint-url',
|
||||
severity: 'warning',
|
||||
message: 'message.advisory.cks.endpoint.url.not.configured',
|
||||
docsHelp: 'plugins/cloudstack-kubernetes-service.html',
|
||||
dismissOnConditionFail: true,
|
||||
condition: async (store) => {
|
||||
if (!['Admin'].includes(store.getters.userInfo.roletype)) {
|
||||
return false
|
||||
}
|
||||
let url = ''
|
||||
const baseUrl = getBaseUrl()
|
||||
if (baseUrl.startsWith('/')) {
|
||||
url = window.location.origin + baseUrl
|
||||
}
|
||||
if (!url || url.startsWith('http://localhost')) {
|
||||
return false
|
||||
}
|
||||
const params = {
|
||||
name: 'endpoint.url'
|
||||
}
|
||||
const json = await getAPI('listConfigurations', params)
|
||||
const configuration = json?.listconfigurationsresponse?.configuration?.[0] || {}
|
||||
return !configuration.value || configuration.value.startsWith('http://localhost')
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
primary: true,
|
||||
label: 'label.fix.global.setting',
|
||||
show: (store) => { return ('updateConfiguration' in store.getters.apis) },
|
||||
run: async () => {
|
||||
let url = ''
|
||||
const baseUrl = getBaseUrl()
|
||||
if (baseUrl.startsWith('/')) {
|
||||
url = window.location.origin + baseUrl
|
||||
}
|
||||
const params = {
|
||||
name: 'endpoint.url',
|
||||
value: url
|
||||
}
|
||||
try {
|
||||
const json = await postAPI('updateConfiguration', params)
|
||||
if (json?.updateconfigurationresponse?.configuration) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
return false
|
||||
},
|
||||
successMessage: 'message.global.setting.updated',
|
||||
errorMessage: 'message.global.setting.update.failed'
|
||||
},
|
||||
{
|
||||
label: 'label.go.to.global.settings',
|
||||
show: (store) => { return ('listConfigurations' in store.getters.apis) },
|
||||
run: (store, router) => {
|
||||
router.push({ name: 'globalsetting' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
api: 'createKubernetesCluster',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
const BASE_KUBERNETES_ISO_URL = 'https://download.cloudstack.org/cks/'
|
||||
|
||||
function getDefaultLatestKubernetesIsoParams (arch) {
|
||||
return {
|
||||
name: 'v1.33.1-calico-' + arch,
|
||||
semanticversion: '1.33.1',
|
||||
url: BASE_KUBERNETES_ISO_URL + 'setup-v1.33.1-calico-' + arch + '.iso',
|
||||
arch: arch,
|
||||
mincpunumber: 2,
|
||||
minmemory: 2048
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest Kubernetes ISO info for the given architecture.
|
||||
* Falls back to a hardcoded default if fetching fails.
|
||||
* @param {string} arch
|
||||
* @returns {Promise<{name: string, semanticversion: string, url: string, arch: string}>}
|
||||
*/
|
||||
export async function getLatestKubernetesIsoParams (arch) {
|
||||
arch = arch || 'x86_64'
|
||||
try {
|
||||
const html = await fetch(BASE_KUBERNETES_ISO_URL, { cache: 'no-store' }).then(r => r.text())
|
||||
|
||||
const hrefs = [...html.matchAll(/href="([^"]+\.iso)"/gi)].map(m => m[1])
|
||||
|
||||
// Prefer files that explicitly include the arch (e.g. ...-x86_64.iso)
|
||||
let isoHrefs = hrefs.filter(h => new RegExp(`${arch}\\.iso$`, 'i').test(h))
|
||||
|
||||
// Fallback: older files without arch suffix (e.g. setup-1.28.4.iso)
|
||||
if (isoHrefs.length === 0) {
|
||||
isoHrefs = hrefs.filter(h => /setup-\d+\.\d+\.\d+\.iso$/i.test(h))
|
||||
}
|
||||
|
||||
const entries = isoHrefs.map(h => {
|
||||
const m = h.match(/setup-(?:v)?(\d+\.\d+\.\d+)(?:-calico)?(?:-(x86_64|arm64))?/i)
|
||||
return m
|
||||
? {
|
||||
name: h.replace('.iso', ''),
|
||||
semanticversion: m[1],
|
||||
url: new URL(h, BASE_KUBERNETES_ISO_URL).toString(),
|
||||
arch: m[2] || arch,
|
||||
mincpunumber: 2,
|
||||
minmemory: 2048
|
||||
}
|
||||
: null
|
||||
}).filter(Boolean)
|
||||
|
||||
if (entries.length === 0) throw new Error('No matching ISOs found')
|
||||
|
||||
entries.sort((a, b) => {
|
||||
const pa = a.semanticversion.split('.').map(Number)
|
||||
const pb = b.semanticversion.split('.').map(Number)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pb[i] ?? 0) !== (pa[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return entries[0]
|
||||
} catch {
|
||||
return { ...getDefaultLatestKubernetesIsoParams(arch) }
|
||||
}
|
||||
}
|
||||
|
|
@ -540,6 +540,9 @@
|
|||
class="row-element"
|
||||
v-else
|
||||
>
|
||||
<advisories-view
|
||||
v-if="$route.meta.advisories && !loading"
|
||||
/>
|
||||
<list-view
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
|
|
@ -604,6 +607,7 @@ import ResourceIcon from '@/components/view/ResourceIcon'
|
|||
import BulkActionProgress from '@/components/view/BulkActionProgress'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import DetailsInput from '@/components/widgets/DetailsInput'
|
||||
import AdvisoriesView from '@/components/view/AdvisoriesView'
|
||||
|
||||
export default {
|
||||
name: 'Resource',
|
||||
|
|
@ -617,7 +621,8 @@ export default {
|
|||
TooltipLabel,
|
||||
OsLogo,
|
||||
ResourceIcon,
|
||||
DetailsInput
|
||||
DetailsInput,
|
||||
AdvisoriesView
|
||||
},
|
||||
mixins: [mixinDevice],
|
||||
provide: function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue