Skip to content

Commit

Permalink
feat(@xen-orchestra/rest-api): expose get servers and get server
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuRA committed Feb 26, 2025
1 parent e567a3b commit 366a43b
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 28 deletions.
20 changes: 19 additions & 1 deletion @vates/types/src/xo.mts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ export type XoPool = {
type: 'pool'
}

export type XoServer = {
allowUnauthorized: boolean
enabled: boolean
error?: Record<string, unknown>
host: string
httpProxy?: string
id: Branded<'server'>
label?: string
poolId?: XoPool['id']
poolNameDescription?: string
poolNameLabel?: string
readOnly: boolean
status: 'connected' | 'disconnected' | 'connecting'
username: string
}

export type XoSr = {
id: Branded<'SR'>
type: 'SR'
Expand Down Expand Up @@ -200,4 +216,6 @@ export type XapiXoRecord =
| XoVmTemplate
| XoVtpm

export type XoRecord = XapiXoRecord | XoGroup | XoUser
export type NonXapiXoRecord = XoGroup | XoServer | XoUser

export type XoRecord = XapiXoRecord | NonXapiXoRecord
1 change: 1 addition & 0 deletions @xen-orchestra/rest-api/.USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The REST API is based on the `TSOA` framework and therefore we use decorators a
@Routes('foo')
@Security('*')
@Response(401)
@Tags('foo')
@provide(Foo)
class Foo extends Controller {}
```
Expand Down
1 change: 1 addition & 0 deletions @xen-orchestra/rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The REST API is based on the `TSOA` framework and therefore we use decorators a
@Routes('foo')
@Security('*')
@Response(401)
@Tags('foo')
@provide(Foo)
class Foo extends Controller {}
```
Expand Down
26 changes: 26 additions & 0 deletions @xen-orchestra/rest-api/src/abstract-classes/base-controller.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from 'tsoa'
import { Request } from 'express'
import { XoRecord } from '@vates/types'

import { RestApi } from '../rest-api/rest-api.mjs'
import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs'
import type { WithHref } from '../helpers/helper.type.mjs'

export abstract class BaseController<T extends XoRecord, IsSync extends boolean> extends Controller {
abstract getObjects(): IsSync extends false ? Promise<Record<T['id'], T>> : Record<T['id'], T>
abstract getObject(id: T['id']): IsSync extends false ? Promise<T> : T

restApi: RestApi

constructor(restApi: RestApi) {
super()
this.restApi = restApi
}

sendObjects(objects: T[], req: Request): string[] | WithHref<T>[] | WithHref<Partial<T>>[] {
const mapper = makeObjectMapper(req)
const mappedObjects = objects.map(mapper) as string[] | WithHref<T>[] | WithHref<Partial<T>>[]

return mappedObjects
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import * as CM from 'complex-matcher'
import { Controller } from 'tsoa'
import { Request } from 'express'
import type { XapiXoRecord } from '@vates/types/xo'

import { RestApi } from '../rest-api/rest-api.mjs'
import { makeObjectMapper } from '../helpers/object-wrapper.helper.mjs'
import type { WithHref } from '../helpers/helper.type.mjs'
import { BaseController } from './base-controller.mjs'

export abstract class XapiXoController<T extends XapiXoRecord> extends Controller {
export abstract class XapiXoController<T extends XapiXoRecord> extends BaseController<T, true> {
#type: T['type']
restApi: RestApi

constructor(type: T['type'], restApi: RestApi) {
super()
super(restApi)
this.#type = type
this.restApi = restApi
}

getObjects({ filter, limit }: { filter?: string; limit?: number } = {}): Record<T['id'], T> {
Expand All @@ -27,11 +22,4 @@ export abstract class XapiXoController<T extends XapiXoRecord> extends Controlle
getObject(id: T['id']): T {
return this.restApi.getObject<T>(id, this.#type)
}

sendObjects(objects: T[], req: Request): string[] | WithHref<T>[] | WithHref<Partial<T>>[] {
const mapper = makeObjectMapper(req)
const mappedObjects = objects.map(mapper) as string[] | WithHref<T>[] | WithHref<Partial<T>>[]

return mappedObjects
}
}
47 changes: 47 additions & 0 deletions @xen-orchestra/rest-api/src/abstract-classes/xo-controller.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as CM from 'complex-matcher'
import type { NonXapiXoRecord } from '@vates/types/xo'

import { BaseController } from './base-controller.mjs'

import { RestApi } from '../rest-api/rest-api.mjs'

export abstract class XoController<T extends NonXapiXoRecord> extends BaseController<T, false> {
abstract _abstractGetObjects(): Promise<T[]>
abstract _abstractGetObject(id: T['id']): Promise<T>

constructor(restApi: RestApi) {
super(restApi)
}

async getObjects({
filter,
limit,
}: {
filter?: string
limit?: number
} = {}): Promise<Record<T['id'], T>> {
const _limit = limit ?? Infinity
let objects = await this._abstractGetObjects()

if (filter !== undefined) {
const predicate = CM.parse(filter).createPredicate()
objects = objects.filter(predicate)
}

if (_limit < objects.length) {
objects.length = _limit
}

const objectById = {} as Record<T['id'], T>

objects.forEach(obj => {
objectById[obj.id] = obj
})

return objectById
}

async getObject(id: T['id']): Promise<T> {
return this._abstractGetObject(id)
}
}
4 changes: 2 additions & 2 deletions @xen-orchestra/rest-api/src/helpers/object-wrapper.helper.mts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import path from 'node:path'
import pick from 'lodash/pick.js'
import { Request } from 'express'
import type { XapiXoRecord } from '@vates/types'
import type { XoRecord } from '@vates/types'

import type { WithHref } from './helper.type.mjs'

const { join } = path.posix

export function makeObjectMapper<T extends XapiXoRecord>(req: Request, path = req.path) {
export function makeObjectMapper<T extends XoRecord>(req: Request, path = req.path) {
const makeUrl = ({ id }: T) => join(baseUrl, path, typeof id === 'number' ? String(id) : id)
let objectMapper: (object: T) => string | WithHref<Partial<T>> | WithHref<T>

Expand Down
8 changes: 8 additions & 0 deletions @xen-orchestra/rest-api/src/rest-api/rest-api.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export class RestApi {
return this.#xoApp.authenticateUser(...args)
}

getAllXenServers() {
return this.#xoApp.getAllXenServers()
}

getObject<T extends XapiXoRecord>(id: T['id'], type: T['type']) {
return this.#xoApp.getObject(id, type)
}
Expand All @@ -21,6 +25,10 @@ export class RestApi {
return this.#xoApp.getObjectsByType(type, opts)
}

getXenServer(...args: Parameters<XoApp['getXenServer']>) {
return this.#xoApp.getXenServer(...args)
}

runWithApiContext(...args: Parameters<XoApp['runWithApiContext']>) {
return this.#xoApp.runWithApiContext(...args)
}
Expand Down
4 changes: 3 additions & 1 deletion @xen-orchestra/rest-api/src/rest-api/rest-api.type.mts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { XoUser, XapiXoRecord } from '@vates/types/xo'
import type { XoUser, XapiXoRecord, XoServer } from '@vates/types/xo'

export type XoApp = {
authenticateUser: (
credentials: { token?: string; username?: string; password?: string },
userData?: { ip?: string },
opts?: { bypassOtp?: boolean }
) => Promise<{ bypassOtp: boolean; expiration: number; user: XoUser }>
getAllXenServers(): Promise<XoServer[]>
getObject: <T extends XapiXoRecord>(id: T['id'], type: T['type']) => T
getObjectsByType: <T extends XapiXoRecord>(
type: T['type'],
opts?: { filter?: string; limit?: number }
) => Record<T['id'], T>
getXenServer(id: XoServer['id']): Promise<XoServer>
runWithApiContext: (user: XoUser, fn: () => void) => Promise<unknown>
}
52 changes: 52 additions & 0 deletions @xen-orchestra/rest-api/src/servers/server.controller.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa'
import { Request as ExRequest } from 'express'
import { inject } from 'inversify'
import { provide } from 'inversify-binding-decorators'
import type { XoServer } from '@vates/types'

import { RestApi } from '../rest-api/rest-api.mjs'
import type { Unbrand, WithHref } from '../helpers/helper.type.mjs'
import { XoController } from '../abstract-classes/xo-controller.mjs'

@Route('servers')
@Security('*')
@Response(401)
@Tags('servers')
@provide(ServerController)
export class ServerController extends XoController<XoServer> {
// --- abstract methods
_abstractGetObjects(): Promise<XoServer[]> {
return this.restApi.getAllXenServers()
}
_abstractGetObject(id: XoServer['id']): Promise<XoServer> {
return this.restApi.getXenServer(id)
}

constructor(@inject(RestApi) restApi: RestApi) {
super(restApi)
}

/**
* @example fields "status,uuid"
* @example filter "status:/^connected$/"
* @example limit 42
*/
@Get('')
async getServers(
@Request() req: ExRequest,
@Query() fields?: string,
@Query() filter?: string,
@Query() limit?: number
): Promise<string[] | WithHref<Unbrand<XoServer>>[] | WithHref<Partial<Unbrand<XoServer>>>[]> {
const servers = Object.values(await this.getObjects({ filter, limit }))
return this.sendObjects(servers, req)
}

/**
* @example id "f07ab729-c0e8-721c-45ec-f11276377030"
*/
@Get('{id}')
getServer(@Path() id: string) {
return this.getObject(id as XoServer['id'])
}
}
10 changes: 1 addition & 9 deletions packages/xo-server/src/xo-mixins/rest-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ export default class RestApi {

const collections = { __proto__: null }
// add migrated collections to maintain their discoverability
const swaggerEndpoints = ['docs', 'vms']
const swaggerEndpoints = ['docs', 'vms', 'servers']

const withParams = (fn, paramsSchema) => {
fn.params = paramsSchema
Expand Down Expand Up @@ -982,14 +982,6 @@ export default class RestApi {
return stream[Symbol.asyncIterator]()
},
}
collections.servers = {
getObject(id) {
return app.getXenServer(id)
},
async getObjects(filter, limit) {
return handleArray(await app.getAllXenServers(), filter, limit)
},
}
collections.users = {
getObject(id) {
return app.getUser(id).then(getUserPublicProperties)
Expand Down

0 comments on commit 366a43b

Please sign in to comment.