feat(xo-server): first endpoints of the beta REST API
This commit is contained in:
99
packages/xo-server/docs/rest-api.md
Normal file
99
packages/xo-server/docs/rest-api.md
Normal file
@@ -0,0 +1,99 @@
|
||||
This [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)-oriented API is available at the address `/rest/v0`.
|
||||
|
||||
### Authentication
|
||||
|
||||
A valid authentication token should be attached as a cookie to all HTTP
|
||||
requests:
|
||||
|
||||
```http
|
||||
GET /rest/v0 HTTP/1.1
|
||||
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
|
||||
```
|
||||
|
||||
The server will respond to an invalid token with a `401 Unauthorized` status.
|
||||
|
||||
The server can request the client to update its token with a `Set-Cookie` header:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Set-Cookie: authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs
|
||||
```
|
||||
|
||||
Usage with cURL:
|
||||
|
||||
```
|
||||
curl -b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs https://xo.company.lan/api/proxy/v0
|
||||
```
|
||||
|
||||
You can use `xo-cli` to create an authentication token:
|
||||
|
||||
```
|
||||
> xo-cli --createToken xoa.company.lan admin@admin.net
|
||||
Password: ********
|
||||
Successfully logged with admin@admin.net
|
||||
Authentication token created
|
||||
|
||||
DiYBFavJwf9GODZqQJs23eAx9eh3KlsRhBi8RcoX0KM
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
Collections of objects are available at `/<name>` (e.g. `/vms`)
|
||||
|
||||
Following query parameters are supported:
|
||||
|
||||
- `limit`: max number of objects returned
|
||||
- `fields`: if specified, instead of plain URLs, the results will be objects containing the requested fields
|
||||
- `filter`: a string that will be used to select only matching objects, see [the syntax documentation](https://xen-orchestra.com/docs/manage_infrastructure.html#live-filter-search)
|
||||
- `ndjson`: if specified, the result will be in [NDJSON format](http://ndjson.org/)
|
||||
|
||||
Simple request:
|
||||
|
||||
```
|
||||
GET /rest/v0/vms HTTP/1.1
|
||||
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
"/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac",
|
||||
"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"
|
||||
]
|
||||
```
|
||||
|
||||
With custom fields:
|
||||
|
||||
```
|
||||
GET /rest/v0/vms?fields=name_label,power_state HTTP/1.1
|
||||
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"name_label": "Debian 10 Cloudinit",
|
||||
"power_state": "Running",
|
||||
"url": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac"
|
||||
},
|
||||
{
|
||||
"name_label": "Debian 10 Cloudinit self-service",
|
||||
"power_state": "Halted",
|
||||
"url": "/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
As NDJSON:
|
||||
|
||||
```
|
||||
GET /rest/v0/vms?fields=name_label,power_state&ndjson HTTP/1.1
|
||||
Cookie: authenticationToken=TN2YBOMYtXB_hHtf4wTzm9p5tTuqq2i15yeuhcz2xXM
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/x-ndjson
|
||||
|
||||
{"name_label":"Debian 10 Cloudinit","power_state":"Running","url":"/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac"}
|
||||
{"name_label":"Debian 10 Cloudinit self-service","power_state":"Halted","url":"/rest/v0/vms/5019156b-f40d-bc57-835b-4a259b177be1"}
|
||||
```
|
||||
@@ -115,6 +115,11 @@ async function updateLocalConfig(diff) {
|
||||
async function createExpressApp(config) {
|
||||
const app = createExpress()
|
||||
|
||||
// For a nicer API
|
||||
//
|
||||
// https://expressjs.com/en/api.html#app.set
|
||||
app.set('json spaces', 2)
|
||||
|
||||
app.use(helmet(config.http.helmet))
|
||||
|
||||
app.use(compression())
|
||||
@@ -237,7 +242,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
|
||||
}
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
const UNCHECKED_URL_RE = /favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
|
||||
const UNCHECKED_URL_RE = /(?:^\/rest\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
|
||||
express.use(async (req, res, next) => {
|
||||
const { url } = req
|
||||
|
||||
@@ -771,6 +776,7 @@ export default async function main(args) {
|
||||
appName: APP_NAME,
|
||||
appVersion: APP_VERSION,
|
||||
config,
|
||||
express,
|
||||
safeMode,
|
||||
})
|
||||
|
||||
|
||||
91
packages/xo-server/src/xo-mixins/rest-api.mjs
Normal file
91
packages/xo-server/src/xo-mixins/rest-api.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ifDef } from '@xen-orchestra/defined'
|
||||
import { invalidCredentials, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { pipeline } from 'stream'
|
||||
import { Router } from 'express'
|
||||
import createNdJsonStream from '../_createNdJsonStream.mjs'
|
||||
import pick from 'lodash/pick.js'
|
||||
import map from 'lodash/map.js'
|
||||
import * as CM from 'complex-matcher'
|
||||
|
||||
const subRouter = (app, path) => {
|
||||
const router = Router({ strict: true })
|
||||
app.use(path, router)
|
||||
return router
|
||||
}
|
||||
|
||||
export default class RestApi {
|
||||
constructor(app, { express }) {
|
||||
const api = subRouter(express, '/rest/v0')
|
||||
|
||||
api.use(({ cookies }, res, next) => {
|
||||
app.authenticateUser({ token: cookies.authenticationToken ?? cookies.token }).then(
|
||||
() => {
|
||||
next()
|
||||
},
|
||||
error => {
|
||||
if (invalidCredentials.is(error)) {
|
||||
res.sendStatus(401)
|
||||
} else {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const vms = subRouter(api, '/vms')
|
||||
|
||||
vms.get('/', async (req, res) => {
|
||||
const { query } = req
|
||||
const basePath = req.baseUrl + req.path
|
||||
const makeUrl = vm => basePath + vm.id
|
||||
|
||||
let filter
|
||||
let userFilter = req.query.filter
|
||||
if (userFilter) {
|
||||
userFilter = CM.parse(userFilter).createPredicate()
|
||||
filter = obj => obj.type === 'VM' && userFilter(obj)
|
||||
} else {
|
||||
filter = obj => obj.type === 'VM'
|
||||
}
|
||||
|
||||
const vms = await app.getObjects({ filter, limit: ifDef(query.limit, Number) })
|
||||
|
||||
let { fields } = query
|
||||
let results
|
||||
if (fields !== undefined) {
|
||||
fields = fields.split(',')
|
||||
results = map(vms, vm => {
|
||||
const url = makeUrl(vm)
|
||||
vm = pick(vm, fields)
|
||||
vm.url = url
|
||||
return vm
|
||||
})
|
||||
} else {
|
||||
results = map(vms, makeUrl)
|
||||
}
|
||||
|
||||
if (query.ndjson !== undefined) {
|
||||
res.set('Content-Type', 'application/x-ndjson')
|
||||
pipeline(createNdJsonStream(results), res, error => {
|
||||
if (error !== undefined) {
|
||||
console.warn('pipeline error', error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
res.json(results)
|
||||
}
|
||||
})
|
||||
|
||||
vms.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
res.json(await app.getObject(req.params.id, 'VM'))
|
||||
} catch (error) {
|
||||
if (noSuchObject.is(error)) {
|
||||
next()
|
||||
} else {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user