From 6e500d4f418b2e7ab9cdb0c1ceaba02c500484a0 Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Fri, 11 Jul 2025 15:01:30 -0700 Subject: [PATCH] ACM-21523: forklift api exploratory work Signed-off-by: Matthew Short --- backend/src/app.ts | 2 + backend/src/routes/forklift.ts | 93 +++++++++++ frontend/src/components/LoadData.tsx | 17 ++ .../VirtualMachines/migrate-utils.ts | 155 ++++++++++++++++++ frontend/webpack.config.ts | 1 + 5 files changed, 268 insertions(+) create mode 100644 backend/src/routes/forklift.ts create mode 100644 frontend/src/routes/Infrastructure/VirtualMachines/migrate-utils.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index a8bfe38ba6b..3a42c7ff2bd 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -31,6 +31,7 @@ import { username } from './routes/username' import { userpreference } from './routes/userpreference' import { virtualMachineGETProxy, virtualMachineProxy, vmResourceUsageProxy } from './routes/virtualMachineProxy' import { managedClusterProxy } from './routes/managedClusterProxy' +import { forklift } from './routes/forklift' const isProduction = process.env.NODE_ENV === 'production' const isDevelopment = process.env.NODE_ENV === 'development' @@ -80,6 +81,7 @@ router.all('/virtualmachinerestores', virtualMachineProxy) router.get('/vmResourceUsage/cluster/:cluster/namespace/:namespace', vmResourceUsageProxy) router.get('/multiclusterhub/components', multiClusterHubComponents) router.all('/managedclusterproxy/*', managedClusterProxy) +router.get('/forklift/*', forklift) router.get('/*', serveHandler) export async function requestHandler(req: Http2ServerRequest, res: Http2ServerResponse): Promise { diff --git a/backend/src/routes/forklift.ts b/backend/src/routes/forklift.ts new file mode 100644 index 00000000000..b4a498fc9b5 --- /dev/null +++ b/backend/src/routes/forklift.ts @@ -0,0 +1,93 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import { Http2ServerRequest, Http2ServerResponse } from 'http2' +import { fetchRetry } from '../lib/fetch-retry' +import { logger } from '../lib/logger' +import { respond, respondInternalServerError, catchInternalServerError } from '../lib/respond' +import { getServiceAccountToken } from '../lib/serviceAccountToken' +import { ResourceList } from '../resources/resource-list' +import { Route } from '../resources/route' +import { getAuthenticatedToken } from '../lib/token' + +async function getForkliftInventoryRoute(): Promise { + const consoleServiceAccountToken = getServiceAccountToken() + + // Get routes with the service=forklift-inventory label in openshift-mtv namespace + const routesPath = + process.env.CLUSTER_API_URL + + '/apis/route.openshift.io/v1/namespaces/openshift-mtv/routes?labelSelector=service%3Dforklift-inventory' + + const response = await fetchRetry(routesPath, { + headers: { + Authorization: `Bearer ${consoleServiceAccountToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const error = Object.assign( + new Error(`Error getting forklift-inventory route: ${response.status} ${response.statusText}`), + { statusCode: response.status } + ) + throw error + } + + const routesList = (await response.json()) as ResourceList + if (routesList.items.length > 1) { + logger.warn('Multiple forklift-inventory routes found and is not expected, using the first one') + } + + const forkliftRoute = routesList.items[0] + if (!forkliftRoute?.spec?.host) { + const error = Object.assign(new Error('forklift-inventory route not found or missing host'), { statusCode: 404 }) + throw error + } + + return forkliftRoute.spec.host +} + +export async function forklift(req: Http2ServerRequest, res: Http2ServerResponse): Promise { + const consoleServiceAccountToken = getServiceAccountToken() + const userToken = await getAuthenticatedToken(req, res) + if (!consoleServiceAccountToken || !userToken) { + respondInternalServerError(req, res) + return + } + + try { + const path = req.url || '' + // Remove /forklift/ prefix + const forkliftPath = path.replace(/^\/forklift\/?/, '') + + // Get forklift inventory host from the route + const host = await getForkliftInventoryRoute() + + // Construct forklift inventory URL + const forkliftInventoryUrl = forkliftPath ? `https://${host}/${forkliftPath}` : `https://${host}` + + const response = await fetchRetry(forkliftInventoryUrl, { + headers: { + Authorization: `Bearer ${consoleServiceAccountToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + logger.error({ + msg: 'Forklift Inventory API error', + status: response.status, + statusText: response.statusText, + }) + respond( + res, + { error: `Forklift Inventory API error: ${response.status} ${response.statusText}` }, + response.status + ) + return + } + + const jsonResponse: unknown = await response.json() + respond(res, jsonResponse) + } catch (err) { + catchInternalServerError(res)(err) + } +} diff --git a/frontend/src/components/LoadData.tsx b/frontend/src/components/LoadData.tsx index a7d2fecb665..b502de270ad 100644 --- a/frontend/src/components/LoadData.tsx +++ b/frontend/src/components/LoadData.tsx @@ -177,6 +177,13 @@ import { PluginDataContext } from '../lib/PluginDataContext' import { useQuery } from '../lib/useQuery' import { AccessControlApiVersion, AccessControlKind } from '../resources/access-control' import { MultiClusterHubComponent } from '../resources/multi-cluster-hub-component' +import { + canVMStorageBeMigrated, + getClusterNetworkAttachmentDefinitions, + getClusterStorageClasses, + getProviders, + getVMDetails, +} from '../routes/Infrastructure/VirtualMachines/migrate-utils' export function LoadData(props: { children?: ReactNode }) { const { loadCompleted, setLoadStarted, setLoadCompleted } = useContext(PluginDataContext) @@ -630,6 +637,16 @@ export function LoadData(props: { children?: ReactNode }) { if (process.env.MODE !== 'plugin') { checkLoggedIn() + const providers = getProviders() + console.log('forklift providers', providers) + const sc = getClusterStorageClasses('201d9f70-bf54-4e12-95a2-f7ca191d3dbd') + console.log('forklift sc', sc) + const nad = getClusterNetworkAttachmentDefinitions('201d9f70-bf54-4e12-95a2-f7ca191d3dbd') + console.log('forklift nad', nad) + const vm = getVMDetails('201d9f70-bf54-4e12-95a2-f7ca191d3dbd', 'ad29e70b-4ef9-406a-b817-1d5955036067') + console.log('forklift vm', vm) + + canVMStorageBeMigrated('e398db81-157b-4ac3-bd14-9d0274735e99', 'sno-2-b9657') } }, []) diff --git a/frontend/src/routes/Infrastructure/VirtualMachines/migrate-utils.ts b/frontend/src/routes/Infrastructure/VirtualMachines/migrate-utils.ts new file mode 100644 index 00000000000..9792c8ee204 --- /dev/null +++ b/frontend/src/routes/Infrastructure/VirtualMachines/migrate-utils.ts @@ -0,0 +1,155 @@ +import { getBackendUrl } from '../../../resources/utils' +//import { IResource } from '../../../resources/resource' + +// export interface Provider extends IResource { +// metadata: { +// name: string +// namespace: string +// uid: string +// } +// } + +// export interface NetworkMap extends IResource { +// spec: { +// map: Array<{ +// destination: { +// type: string +// } +// source: { +// type: string +// } +// }> +// provider: { +// destination: Provider +// source: Provider +// } +// } +// } + +// export interface StorageMap extends IResource { +// spec: { +// map: Array<{ +// destination: { +// storageClass: string +// } +// source: { +// id: string +// name: string +// } +// }> +// provider: { +// destination: Provider +// source: Provider +// } +// } +// } + +export interface ForkliftResource { + uid: string + name: string + namespace: string +} + +export interface VMResource { + uid: string + name: string + namespace: string + networks: any[] + architecture: string + dataVolumeTemplates: any[] +} + +async function fetchForkliftData(endpoint: string) { + try { + const res = await fetch(`${getBackendUrl()}${endpoint}`, { + credentials: 'include', + headers: { accept: 'application/json' }, + }) + return await res.json() + } catch (err) { + console.error(err) + } +} + +function transformToForkliftResource(data: any[]): ForkliftResource[] { + if (!Array.isArray(data)) return [] + + return data.map((item) => ({ + uid: item.uid, + name: item.name, + namespace: item.namespace || '', + })) +} + +function transformToVMResource(data: any): VMResource { + if (!data) { + return { + uid: '', + name: '', + namespace: '', + networks: [], + architecture: '', + dataVolumeTemplates: [], + } + } + + return { + uid: data.uid || '', + name: data.name || '', + namespace: data.namespace || '', + networks: data.object?.spec?.template?.spec?.networks || [], + architecture: data.object?.spec?.template?.spec?.architecture || '', + dataVolumeTemplates: data.object?.spec?.dataVolumeTemplates || [], + } +} + +export async function getProviders(): Promise { + const data = await fetchForkliftData('/forklift/providers/openshift') + return transformToForkliftResource(data) +} + +export async function getClusterStorageClasses(providerID: string): Promise { + const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/storageclasses`) + return transformToForkliftResource(data) +} + +export async function getClusterNetworkAttachmentDefinitions(providerID: string): Promise { + const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/networkattachmentdefinitions`) + return transformToForkliftResource(data) +} + +export async function getVMDetails(providerID: string, vmID: string): Promise { + const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/vms/${vmID}`) + return transformToVMResource(data) +} + +async function getProviderIdFromCluster(clusterName: string): Promise { + const providers = await fetchForkliftData(`/forklift/providers/openshift`) + + if (!Array.isArray(providers)) { + return '' + } + + for (const provider of providers) { + // Remove '-mtv' suffix from provider name before comparing + const providerNameWithoutSuffix = provider.name?.endsWith('-mtv') ? provider.name.slice(0, -4) : provider.name + + if (providerNameWithoutSuffix === clusterName) { + return provider.uid || '' + } + } + + return '' +} + +export async function canVMStorageBeMigrated(vmID: string, targetCluster: string): Promise { + const providerID = await getProviderIdFromCluster(targetCluster) + const vm = await getVMDetails(providerID, vmID) + const vmStorage = vm.dataVolumeTemplates + const clusterStorage = await getClusterStorageClasses(providerID) + + console.log('MATT vmStorage', vmStorage) + console.log('MATT clusterStorage', clusterStorage) + + return false +} diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index f1ee2c9a8e1..b175fb6954f 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -180,6 +180,7 @@ module.exports = function (env: any, argv: { hot?: boolean; mode: string | undef '/multicloud/multiclusterhub/components', '/multicloud/vmResourceUsage', '/multicloud/managedclusterproxy', + '/multicloud/forklift', ].map((backendPath) => ({ path: backendPath, target: `https://localhost:${process.env.BACKEND_PORT}`,