Skip to content
Open
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
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<void> {
Expand Down
93 changes: 93 additions & 0 deletions backend/src/routes/forklift.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<Route>
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<void> {
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)
}
}
17 changes: 17 additions & 0 deletions frontend/src/components/LoadData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
}
}, [])

Expand Down
155 changes: 155 additions & 0 deletions frontend/src/routes/Infrastructure/VirtualMachines/migrate-utils.ts
Original file line number Diff line number Diff line change
@@ -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<ForkliftResource[]> {
const data = await fetchForkliftData('/forklift/providers/openshift')
return transformToForkliftResource(data)
}

export async function getClusterStorageClasses(providerID: string): Promise<ForkliftResource[]> {
const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/storageclasses`)
return transformToForkliftResource(data)
}

export async function getClusterNetworkAttachmentDefinitions(providerID: string): Promise<ForkliftResource[]> {
const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/networkattachmentdefinitions`)
return transformToForkliftResource(data)
}

export async function getVMDetails(providerID: string, vmID: string): Promise<VMResource> {
const data = await fetchForkliftData(`/forklift/providers/openshift/${providerID}/vms/${vmID}`)
return transformToVMResource(data)
}

async function getProviderIdFromCluster(clusterName: string): Promise<string> {
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<boolean> {
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
}
1 change: 1 addition & 0 deletions frontend/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down