diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 1dd29d5a9297..da1f3888eb65 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -287,8 +287,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",
@@ -1078,6 +1080,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",
@@ -1110,6 +1113,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",
@@ -3010,12 +3016,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: ",
@@ -3056,9 +3067,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",
@@ -3494,6 +3509,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.",
diff --git a/ui/src/api/index.js b/ui/src/api/index.js
index 0c8e8e9696c3..5ec73a0b20e1 100644
--- a/ui/src/api/index.js
+++ b/ui/src/api/index.js
@@ -140,3 +140,7 @@ export function oauthlogin (arg) {
}
})
}
+
+export function getBaseUrl () {
+ return vueProps.axios.defaults.baseURL
+}
diff --git a/ui/src/components/view/AdvisoriesView.vue b/ui/src/components/view/AdvisoriesView.vue
new file mode 100644
index 000000000000..c668ec023801
--- /dev/null
+++ b/ui/src/components/view/AdvisoriesView.vue
@@ -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.
+
+
+
+
+
+
+
+
+
+ {{ $t(action.label) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/config/router.js b/ui/src/config/router.js
index 582fbaaf2f35..04788dc3e693 100644
--- a/ui/src/config/router.js
+++ b/ui/src/config/router.js
@@ -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
}
diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js
index 7688566df423..62273269d247 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -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'
export default {
name: 'compute',
@@ -581,6 +583,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',
diff --git a/ui/src/utils/acsrepo/index.js b/ui/src/utils/acsrepo/index.js
new file mode 100644
index 000000000000..809bd7f17483
--- /dev/null
+++ b/ui/src/utils/acsrepo/index.js
@@ -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) }
+ }
+}
diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue
index f55767ded2a0..55d4f5df70f3 100644
--- a/ui/src/views/AutogenView.vue
+++ b/ui/src/views/AutogenView.vue
@@ -540,6 +540,9 @@
class="row-element"
v-else
>
+