diff --git a/dashboard/pkg/epinio/l10n/en-us.yaml b/dashboard/pkg/epinio/l10n/en-us.yaml index 13190ce..9ed006e 100644 --- a/dashboard/pkg/epinio/l10n/en-us.yaml +++ b/dashboard/pkg/epinio/l10n/en-us.yaml @@ -314,6 +314,8 @@ epinio: label: Redeploy goToEpinio: label: Epinio App + viewDeployment: + label: View In Kube Cluster wm: containerName: 'Instance: {label}' noData: There are no log entries to show. diff --git a/dashboard/pkg/epinio/models/applications.js b/dashboard/pkg/epinio/models/applications.js index ef3d81b..2311469 100644 --- a/dashboard/pkg/epinio/models/applications.js +++ b/dashboard/pkg/epinio/models/applications.js @@ -9,6 +9,8 @@ import { import { createEpinioRoute } from '../utils/custom-routing'; import EpinioNamespacedResource, { bulkRemove } from './epinio-namespaced-resource'; import { AppUtils } from '../utils/application'; +import { WORKLOAD_TYPES } from '@shell/config/types'; +import { NAME as EXPLORER } from '@shell/config/product/explorer'; // See https://github.com/epinio/epinio/blob/00684bc36780a37ab90091498e5c700337015a96/pkg/api/core/v1/models/app.go#L11 const STATES = { @@ -106,7 +108,7 @@ export default class EpinioApplicationModel extends EpinioNamespacedResource { res.push({ action: 'showAppShell', label: this.t('epinio.applications.actions.shell.label'), - icon: 'icon icon-fw icon-chevron-right', + icon: 'icon icon-fw icon-terminal', enabled: showAppShell, }); } @@ -125,10 +127,6 @@ export default class EpinioApplicationModel extends EpinioNamespacedResource { }, ); - if (showAppShell || showAppLog || showStagingLog) { - res.push({ divider: true }); - } - res.push( { action: 'restage', label: this.t('epinio.applications.actions.restage.label'), @@ -141,7 +139,6 @@ export default class EpinioApplicationModel extends EpinioNamespacedResource { icon: 'icon icon-fw icon-refresh', enabled: isRunning }, - { divider: true }, { action: 'exportApp', label: this.t('epinio.applications.export.label'), @@ -149,8 +146,21 @@ export default class EpinioApplicationModel extends EpinioNamespacedResource { enabled: isRunning }, { divider: true }, + ); - ...super._availableActions); + if (this.canViewDeployment) { + res.push({ + action: 'viewDeployment', + label: this.t('epinio.applications.actions.viewDeployment.label'), + icon: 'icon icon-fw icon-chevron-right', + }, + { divider: true }, + ); + } + + res.push( + ...super._availableActions + ); return res; } @@ -448,6 +458,52 @@ export default class EpinioApplicationModel extends EpinioNamespacedResource { return 'export'; } + get canViewDeployment() { + return !this.$rootGetters['isSingleProduct'] && !!this.$getters[`schemaFor`](WORKLOAD_TYPES.DEPLOYMENT); + } + + /** + * Attempt to view the deployment for this namespace in Rancher's UI + * + * If we can't find the deployment, just go to the deployment list with the name in the filter + */ + viewDeployment() { + const clusterId = this.$rootGetters['clusterId']; + const namespace = this.metadata.namespace; + const appName = this.metadata.name; + const url = `/k8s/clusters/${ clusterId }/v1/apps.deployments/${ namespace }?labelSelector=app.kubernetes.io/component%3Dapplication,app.kubernetes.io/name%3D${ appName }`; + + const deploymentList = { + name: `c-cluster-product-resource`, + params: { + product: EXPLORER, + cluster: clusterId, + resource: WORKLOAD_TYPES.DEPLOYMENT, + }, + query: { q: this.metadata.name } + }; + + this.$dispatch(`cluster/request`, { url }, { root: true }) + .then((deployments) => { + if (deployments?.data?.length === 1) { + const deployment = deployments.data[0]; + + this.currentRouter().push({ + name: `c-cluster-product-resource-namespace-id`, + params: { + ...deploymentList.params, + namespace: deployment.metadata.namespace, + id: deployment.metadata.name, + } + }); + } else { + this.currentRouter().push(deploymentList); + } + }).catch(() => { + this.currentRouter().push(deploymentList); + }); + } + // ------------------------------------------------------------------ // Change/handle changes of the app diff --git a/dashboard/pkg/epinio/models/cluster.ts b/dashboard/pkg/epinio/models/cluster.ts index 06d747b..d4e48f6 100644 --- a/dashboard/pkg/epinio/models/cluster.ts +++ b/dashboard/pkg/epinio/models/cluster.ts @@ -9,6 +9,7 @@ export default class EpinioCluster extends Resource { id: string; name: string; + namespace: string; state?: string; metadata?: { state: { transitioning: boolean, error: boolean, message: string }}; loggedIn: boolean; @@ -19,6 +20,7 @@ export default class EpinioCluster extends Resource { constructor(data: { id: string, name: string, + namespace: string, loggedIn: boolean, api: string, mgmtCluster: any, @@ -26,6 +28,7 @@ export default class EpinioCluster extends Resource { super(data, ctx); this.id = data.id; this.name = data.name; + this.namespace = data.namespace; this.api = data.api; this.loggedIn = data.loggedIn; this.mgmtCluster = data.mgmtCluster; diff --git a/dashboard/pkg/epinio/pages/c/_cluster/dashboard.vue b/dashboard/pkg/epinio/pages/c/_cluster/dashboard.vue index dd51951..481d7b9 100644 --- a/dashboard/pkg/epinio/pages/c/_cluster/dashboard.vue +++ b/dashboard/pkg/epinio/pages/c/_cluster/dashboard.vue @@ -154,12 +154,6 @@ export default Vue.extend({ this.sectionContent[2].isLoaded = true; this.sectionContent[2].isEnable = true; } - }, - openMetricsDetails() { - this.$router.replace({ - name: 'c-cluster-explorer', - params: { cluster: this.$store.getters['clusterId'] } - }); } }, computed: { @@ -206,6 +200,12 @@ export default Vue.extend({ return { totalNamespaces: allNamespaces.length, latestNamespaces: sortBy(allNamespaces, 'metadata.createdAt').reverse().slice(0, 2) }; }, + metricsDetails() { + return { + name: 'c-cluster-explorer', + params: { cluster: this.$store.getters['clusterId'] } + }; + } }, }); @@ -254,10 +254,11 @@ export default Vue.extend({ {{ t('epinio.intro.metrics.availability', { availableCpu, availableMemory }) }} - {{ t('epinio.intro.metrics.link.label') }} + + {{ t('epinio.intro.metrics.link.label') }} +
diff --git a/dashboard/pkg/epinio/store/epinio-store/actions.ts b/dashboard/pkg/epinio/store/epinio-store/actions.ts index 031d4c9..5c6f392 100644 --- a/dashboard/pkg/epinio/store/epinio-store/actions.ts +++ b/dashboard/pkg/epinio/store/epinio-store/actions.ts @@ -1,4 +1,4 @@ -import { METRIC, SCHEMA } from '@shell/config/types'; +import { METRIC, SCHEMA, WORKLOAD_TYPES } from '@shell/config/types'; import { handleSpoofedRequest } from '@shell/plugins/dashboard-store/actions'; import { classify } from '@shell/plugins/dashboard-store/classify'; import { normalizeType } from '@shell/plugins/dashboard-store/normalize'; @@ -12,6 +12,7 @@ import { } from '../../types'; import EpinioCluster from '../../models/cluster'; import { RedirectToError } from '@shell/utils/error'; +import { allHashSettled } from '@shell/utils/promise'; const createId = (schema: any, resource: any) => { const name = resource.meta?.name || resource.name; @@ -243,13 +244,18 @@ export default { if (!isSingleProduct) { try { - const nodeMetricsSchema = await dispatch(`cluster/request`, { url: `/k8s/clusters/${ clusterId }/v1/schemas/${ METRIC.NODE }` }, { root: true }); - - if (nodeMetricsSchema) { - data.push(nodeMetricsSchema); - } + const schemas = await allHashSettled({ + nodeMetrics: dispatch(`cluster/request`, { url: `/k8s/clusters/${ clusterId }/v1/schemas/${ METRIC.NODE }` }, { root: true }), + deployments: dispatch(`cluster/request`, { url: `/k8s/clusters/${ clusterId }/v1/schemas/${ WORKLOAD_TYPES.DEPLOYMENT }` }, { root: true }) + }); + + Object.values(schemas).forEach((res: any ) => { + if (res.value) { + data.push(res.value); + } + }); } catch (e) { - console.warn(`Unable to fetch Node metrics schema for epinio cluster: ${ clusterId }`);// eslint-disable-line no-console + console.debug(`Unable to fetch schema/s for epinio cluster: ${ clusterId }`, e);// eslint-disable-line no-console } } diff --git a/dashboard/pkg/epinio/utils/epinio-discovery.ts b/dashboard/pkg/epinio/utils/epinio-discovery.ts index dcc2817..1c69126 100644 --- a/dashboard/pkg/epinio/utils/epinio-discovery.ts +++ b/dashboard/pkg/epinio/utils/epinio-discovery.ts @@ -3,10 +3,10 @@ import { ingressFullPath } from '@shell/models/networking.k8s.io.ingress'; import epinioAuth, { EpinioAuthTypes } from '../utils/auth'; import EpinioCluster from '../models/cluster'; -export default { - ingressUrl(clusterId: string) { - return `/k8s/clusters/${ clusterId }/v1/networking.k8s.io.ingresses/epinio/epinio`; - }, +class EpinioDiscovery { + ingressUrl(clusterId: string, namespace: string) { + return `/k8s/clusters/${ clusterId }/v1/networking.k8s.io.ingresses/${ namespace }/epinio`; + } async discover(store: any) { const allClusters = await store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }, { root: true }); @@ -14,9 +14,13 @@ export default { for (const c of allClusters.filter((c: any) => c.isReady)) { try { + // Try to discover the namespace epinio is installed to + const namespace = await this.findNamespace(store, c.id); + // Get the url first, if it has this it's highly likely it's an epinio cluster - const epinioIngress = await store.dispatch(`cluster/request`, { url: this.ingressUrl(c.id) }, { root: true }); + const epinioIngress = await store.dispatch(`cluster/request`, { url: this.ingressUrl(c.id, namespace) }, { root: true }); const url = ingressFullPath(epinioIngress, epinioIngress.spec.rules?.[0]); + const loggedIn = await epinioAuth.isLoggedIn({ type: EpinioAuthTypes.AGNOSTIC, epinioUrl: url, @@ -29,15 +33,36 @@ export default { epinioClusters.push(new EpinioCluster({ id: c.id, name: c.spec.displayName, + namespace, api: url, loggedIn: !!loggedIn, mgmtCluster: c }, { rootGetters: store.getters })); } catch (err) { - console.info(`Skipping epinio discovery for ${ c.spec.displayName }`, err); // eslint-disable-line no-console + console.debug(`Skipping epinio discovery for ${ c.spec.displayName }:`, err); // eslint-disable-line no-console } } return epinioClusters; } -}; + + private async findNamespace(store: any, clusterId: string): Promise { + // Attempt to find the `epinio-server` deployment. This assumes the user had read rights to resources in the target namespace + const url = `/k8s/clusters/${ clusterId }/v1/apps.deployments?labelSelector=app.kubernetes.io/component%3Depinio,app.kubernetes.io/name%3Depinio-server`; + const deployments = await store.dispatch(`cluster/request`, { url }, { root: true }); + + if (!deployments?.data?.length) { + return Promise.reject(new Error('Could not find epinio-server deployment')); + } + + if (deployments?.data.length > 1) { + return Promise.reject(new Error('Found too many epinio-server deployments')); + } + + return deployments.data[0].metadata.namespace; + } +} + +const epinioDiscovery = new EpinioDiscovery(); + +export default epinioDiscovery;