mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-24 07:46:41 +00:00
commit
f8e5cd8949
@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v7.12.0 - 2018-06-26
|
||||
|
||||
* Fix /v1/device endpoint returning null for commit after an update #652 [Cameron Diver]
|
||||
* Add applications state v2 endpoint #652 [Cameron Diver]
|
||||
* Move compose types to ./types and add partial definitions for compose modules #652 [Cameron Diver]
|
||||
* Move v2 endpoints to separate module #652 [Cameron Diver]
|
||||
* Refactor v1 api into seperate modules #652 [Cameron Diver]
|
||||
|
||||
## v7.11.3 - 2018-06-25
|
||||
|
||||
* Add fromDockerOpts and normalization to PortMap class, and use in fromContainer #655 [Cameron Diver]
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "resin-supervisor",
|
||||
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
|
||||
"version": "7.11.3",
|
||||
"version": "7.12.0",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -24,6 +24,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bluebird": "^3.5.20",
|
||||
"@types/express": "^4.11.1",
|
||||
"@types/knex": "^0.14.14",
|
||||
"@types/lodash": "^4.14.109",
|
||||
"@types/mz": "0.0.32",
|
||||
|
@ -21,8 +21,9 @@ Volumes = require './compose/volumes'
|
||||
|
||||
Proxyvisor = require './proxyvisor'
|
||||
|
||||
serviceAction = (action, serviceId, current, target, options = {}) ->
|
||||
return { action, serviceId, current, target, options }
|
||||
{ createV1Api } = require './device-api/v1'
|
||||
{ createV2Api } = require './device-api/v2'
|
||||
{ serviceAction } = require './device-api/common'
|
||||
|
||||
# TODO: move this to an Image class?
|
||||
imageForService = (service) ->
|
||||
@ -48,193 +49,15 @@ pathExistsOnHost = (p) ->
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
|
||||
appNotFoundMsg = "App not found: an app needs to be installed for this endpoint to work.
|
||||
If you've recently moved this device from another app,
|
||||
please push an app and wait for it to be installed first."
|
||||
|
||||
# TODO: implement additional v2 endpoints
|
||||
# Some v1 endpoins only work for single-container apps as they assume the app has a single service.
|
||||
createApplicationManagerRouter = (applications) ->
|
||||
{ eventTracker, deviceState, _lockingIfNecessary, logger } = applications
|
||||
router = express.Router()
|
||||
router.use(bodyParser.urlencoded(extended: true))
|
||||
router.use(bodyParser.json())
|
||||
|
||||
doRestart = (appId, force) ->
|
||||
_lockingIfNecessary appId, { force }, ->
|
||||
deviceState.getCurrentForComparison()
|
||||
.then (currentState) ->
|
||||
app = currentState.local.apps[appId]
|
||||
imageIds = _.map(app.services, 'imageId')
|
||||
applications.clearTargetVolatileForServices(imageIds)
|
||||
stoppedApp = _.cloneDeep(app)
|
||||
stoppedApp.services = []
|
||||
currentState.local.apps[appId] = stoppedApp
|
||||
deviceState.pausingApply ->
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.then ->
|
||||
currentState.local.apps[appId] = app
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.finally ->
|
||||
deviceState.triggerApplyTarget()
|
||||
|
||||
doPurge = (appId, force) ->
|
||||
logger.logSystemMessage("Purging data for app #{appId}", { appId }, 'Purge data')
|
||||
_lockingIfNecessary appId, { force }, ->
|
||||
deviceState.getCurrentForComparison()
|
||||
.then (currentState) ->
|
||||
app = currentState.local.apps[appId]
|
||||
if !app?
|
||||
throw new Error(appNotFoundMsg)
|
||||
purgedApp = _.cloneDeep(app)
|
||||
purgedApp.services = []
|
||||
purgedApp.volumes = {}
|
||||
currentState.local.apps[appId] = purgedApp
|
||||
deviceState.pausingApply ->
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.then ->
|
||||
currentState.local.apps[appId] = app
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.finally ->
|
||||
deviceState.triggerApplyTarget()
|
||||
.tap ->
|
||||
logger.logSystemMessage('Purged data', { appId }, 'Purge data success')
|
||||
.tapCatch (err) ->
|
||||
logger.logSystemMessage("Error purging data: #{err}", { appId, error: err }, 'Purge data error')
|
||||
|
||||
router.post '/v1/restart', (req, res) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
eventTracker.track('Restart container (v1)', { appId })
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
doRestart(appId, force)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
v1StopOrStart = (req, res, action) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
applications.getCurrentApp(appId)
|
||||
.then (app) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
return res.status(400).send('App not found')
|
||||
if app.services.length > 1
|
||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||
applications.setTargetVolatileForService(service.imageId, running: action != 'stop')
|
||||
applications.executeStepAction(serviceAction(action, service.serviceId, service, service, { wait: true }), { force })
|
||||
.then ->
|
||||
if action == 'stop'
|
||||
return service
|
||||
# We refresh the container id in case we were starting an app with no container yet
|
||||
applications.getCurrentApp(appId)
|
||||
.then (app) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
throw new Error('App not found after running action')
|
||||
return service
|
||||
.then (service) ->
|
||||
res.status(200).json({ containerId: service.containerId })
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v1/apps/:appId/stop', (req, res) ->
|
||||
v1StopOrStart(req, res, 'stop')
|
||||
|
||||
router.post '/v1/apps/:appId/start', (req, res) ->
|
||||
v1StopOrStart(req, res, 'start')
|
||||
|
||||
router.get '/v1/apps/:appId', (req, res) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
eventTracker.track('GET app (v1)', { appId })
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
Promise.join(
|
||||
applications.getCurrentApp(appId)
|
||||
applications.getStatus()
|
||||
(app, status) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
return res.status(400).send('App not found')
|
||||
if app.services.length > 1
|
||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||
# Don't return data that will be of no use to the user
|
||||
appToSend = {
|
||||
appId
|
||||
containerId: service.containerId
|
||||
env: _.omit(service.environment, constants.privateAppEnvVars)
|
||||
releaseId: service.releaseId
|
||||
imageId: service.image
|
||||
}
|
||||
if status.commit?
|
||||
appToSend.commit = status.commit
|
||||
res.json(appToSend)
|
||||
)
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v1/purge', (req, res) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
errMsg = 'Invalid or missing appId'
|
||||
return res.status(400).send(errMsg)
|
||||
doPurge(appId, force)
|
||||
.then ->
|
||||
res.status(200).json(Data: 'OK', Error: '')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v2/applications/:appId/purge', (req, res) ->
|
||||
{ force } = req.body
|
||||
{ appId } = req.params
|
||||
doPurge(appId, force)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
handleServiceAction = (req, res, action) ->
|
||||
{ imageId, force } = req.body
|
||||
{ appId } = req.params
|
||||
_lockingIfNecessary appId, { force }, ->
|
||||
applications.getCurrentApp(appId)
|
||||
.then (app) ->
|
||||
if !app?
|
||||
return res.status(404).send(appNotFoundMsg)
|
||||
service = _.find(app.services, { imageId })
|
||||
if !service?
|
||||
errMsg = 'Service not found, a container must exist for this endpoint to work.'
|
||||
return res.status(404).send(errMsg)
|
||||
applications.setTargetVolatileForService(service.imageId, running: action != 'stop')
|
||||
applications.executeStepAction(serviceAction(action, service.serviceId, service, service, { wait: true }), { skipLock: true })
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v2/applications/:appId/restart-service', (req, res) ->
|
||||
handleServiceAction(req, res, 'restart')
|
||||
|
||||
router.post '/v2/applications/:appId/stop-service', (req, res) ->
|
||||
handleServiceAction(req, res, 'stop')
|
||||
|
||||
router.post '/v2/applications/:appId/start-service', (req, res) ->
|
||||
handleServiceAction(req, res, 'start')
|
||||
|
||||
router.post '/v2/applications/:appId/restart', (req, res) ->
|
||||
{ force } = req.body
|
||||
{ appId } = req.params
|
||||
doRestart(appId, force)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
createV1Api(router, applications)
|
||||
createV2Api(router, applications)
|
||||
|
||||
router.use(applications.proxyvisor.router)
|
||||
|
||||
@ -288,6 +111,8 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
@services.start(step.target)
|
||||
.then (container) =>
|
||||
@_containerStarted[container.id] = true
|
||||
updateCommit: (step) =>
|
||||
@config.set({ currentCommit: step.target })
|
||||
handover: (step, { force = false, skipLock = false } = {}) =>
|
||||
@_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, =>
|
||||
@services.handover(step.current, step.target)
|
||||
@ -349,8 +174,9 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
Promise.join(
|
||||
@services.getStatus()
|
||||
@images.getStatus()
|
||||
@config.get('currentCommit')
|
||||
@db.models('app').select([ 'appId', 'releaseId', 'commit' ])
|
||||
(services, images, targetApps) ->
|
||||
(services, images, currentCommit, targetApps) ->
|
||||
apps = {}
|
||||
dependent = {}
|
||||
releaseId = null
|
||||
@ -393,15 +219,14 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
console.log('Ignoring legacy dependent image', image)
|
||||
|
||||
obj = { local: apps, dependent }
|
||||
if releaseId and targetApps[0]?.releaseId == releaseId
|
||||
obj.commit = targetApps[0].commit
|
||||
obj.commit = currentCommit
|
||||
return obj
|
||||
)
|
||||
|
||||
getDependentState: =>
|
||||
@proxyvisor.getCurrentStates()
|
||||
|
||||
_buildApps: (services, networks, volumes) ->
|
||||
_buildApps: (services, networks, volumes, currentCommit) ->
|
||||
apps = {}
|
||||
|
||||
# We iterate over the current running services and add them to the current state
|
||||
@ -421,6 +246,11 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
apps[appId] ?= { appId, services: [], volumes: {}, networks: {} }
|
||||
apps[appId].volumes[volume.name] = volume.config
|
||||
|
||||
# multi-app warning!
|
||||
# This is just wrong on every level
|
||||
for app in apps
|
||||
app.commit = currentCommit
|
||||
|
||||
return apps
|
||||
|
||||
getCurrentForComparison: =>
|
||||
@ -428,6 +258,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
@services.getAll()
|
||||
@networks.getAll()
|
||||
@volumes.getAll()
|
||||
@config.get('currentCommit')
|
||||
@_buildApps
|
||||
)
|
||||
|
||||
@ -436,6 +267,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
@services.getAllByAppId(appId)
|
||||
@networks.getAllByAppId(appId)
|
||||
@volumes.getAllByAppId(appId)
|
||||
@configget('currentCommit')
|
||||
@_buildApps
|
||||
).get(appId)
|
||||
|
||||
@ -759,6 +591,13 @@ module.exports = class ApplicationManager extends EventEmitter
|
||||
for pair in volumePairs
|
||||
pairSteps = @_nextStepsForVolume(pair, currentApp, removePairs.concat(updatePairs))
|
||||
steps = steps.concat(pairSteps)
|
||||
|
||||
if _.isEmpty(steps) and currentApp.commit != targetApp.commit
|
||||
steps.push({
|
||||
action: 'updateCommit'
|
||||
target: targetApp.commit
|
||||
})
|
||||
|
||||
return _.map(steps, (step) -> _.assign({}, step, { appId }))
|
||||
|
||||
normaliseAppForDB: (app) =>
|
||||
|
53
src/application-manager.d.ts
vendored
Normal file
53
src/application-manager.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { ServiceAction } from './device-api/common';
|
||||
import { DeviceApplicationState } from './types/state';
|
||||
|
||||
import Images = require('./compose/images');
|
||||
import ServiceManager = require('./compose/service-manager');
|
||||
import DB = require('./db');
|
||||
|
||||
import { Service } from './types/service';
|
||||
|
||||
declare interface Options {
|
||||
force?: boolean;
|
||||
running?: boolean;
|
||||
skipLock?: boolean;
|
||||
}
|
||||
|
||||
// TODO: This needs to be moved to the correct module's typings
|
||||
declare interface Application {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
// This is a non-exhaustive typing for ApplicationManager to avoid
|
||||
// having to recode the entire class (and all requirements in TS).
|
||||
export class ApplicationManager extends EventEmitter {
|
||||
|
||||
// These probably could be typed, but the types are so messy that we're
|
||||
// best just waiting for the relevant module to be recoded in typescript.
|
||||
// At least any types we can be sure of then.
|
||||
//
|
||||
// TODO: When the module which is/declares these fields is converted to
|
||||
// typecript, type the following
|
||||
public _lockingIfNecessary: any;
|
||||
public logger: any;
|
||||
public deviceState: any;
|
||||
public eventTracker: any;
|
||||
|
||||
public services: ServiceManager;
|
||||
public db: DB;
|
||||
public images: Images;
|
||||
|
||||
public getCurrentApp(appId: number): Promise<Application | null>;
|
||||
|
||||
// TODO: This actually returns an object, but we don't need the values just yet
|
||||
public setTargetVolatileForService(serviceId: number, opts: Options): void;
|
||||
|
||||
public executeStepAction(serviceAction: ServiceAction, opts: Options): Promise<void>;
|
||||
|
||||
public getStatus(): Promise<DeviceApplicationState>;
|
||||
|
||||
}
|
||||
|
||||
export default ApplicationManager;
|
7
src/compose/images.d.ts
vendored
Normal file
7
src/compose/images.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import Image from '../types/image';
|
||||
|
||||
declare class Images {
|
||||
public getStatus(): Image[];
|
||||
}
|
||||
|
||||
export = Images;
|
@ -188,7 +188,15 @@ module.exports = class ServiceManager extends EventEmitter
|
||||
.then (services) =>
|
||||
status = _.clone(@volatileState)
|
||||
for service in services
|
||||
status[service.containerId] ?= _.pick(service, [ 'appId', 'imageId', 'status', 'releaseId', 'commit', 'createdAt' ])
|
||||
status[service.containerId] ?= _.pick(service, [
|
||||
'appId',
|
||||
'imageId',
|
||||
'status',
|
||||
'releaseId',
|
||||
'commit',
|
||||
'createdAt',
|
||||
'serviceName',
|
||||
])
|
||||
return _.values(status)
|
||||
|
||||
getByDockerContainerId: (containerId) =>
|
||||
|
10
src/compose/service-manager.d.ts
vendored
Normal file
10
src/compose/service-manager.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { Service } from '../types/service';
|
||||
|
||||
// FIXME: Unfinished definition for this class...
|
||||
declare class ServiceManager extends EventEmitter {
|
||||
public getStatus(): Service[];
|
||||
}
|
||||
|
||||
export = ServiceManager;
|
@ -194,6 +194,7 @@ class Config extends EventEmitter {
|
||||
nativeLogger: { source: 'db', mutable: true, default: 'true' },
|
||||
// a JSON value, which is either null, or { app: number, commit: string }
|
||||
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
||||
currentCommit: { source: 'db', mutable: true },
|
||||
};
|
||||
|
||||
public constructor({ db, configPath }: ConfigOpts) {
|
||||
|
52
src/device-api/common.coffee
Normal file
52
src/device-api/common.coffee
Normal file
@ -0,0 +1,52 @@
|
||||
_ = require('lodash')
|
||||
|
||||
{ appNotFoundMessage } = require('../lib/messages')
|
||||
|
||||
exports.doRestart = (applications, appId, force) ->
|
||||
{ _lockingIfNecessary, deviceState } = applications
|
||||
|
||||
_lockingIfNecessary appId, { force }, ->
|
||||
deviceState.getCurrentForComparison()
|
||||
.then (currentState) ->
|
||||
app = currentState.local.apps[appId]
|
||||
imageIds = _.map(app.services, 'imageId')
|
||||
applications.clearTargetVolatileForServices(imageIds)
|
||||
stoppedApp = _.cloneDeep(app)
|
||||
stoppedApp.services = []
|
||||
currentState.local.apps[appId] = stoppedApp
|
||||
deviceState.pausingApply ->
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.then ->
|
||||
currentState.local.apps[appId] = app
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.finally ->
|
||||
deviceState.triggerApplyTarget()
|
||||
|
||||
exports.doPurge = (applications, appId, force) ->
|
||||
{ logger, _lockingIfNecessary, deviceState } = applications
|
||||
|
||||
logger.logSystemMessage("Purging data for app #{appId}", { appId }, 'Purge data')
|
||||
_lockingIfNecessary appId, { force }, ->
|
||||
deviceState.getCurrentForComparison()
|
||||
.then (currentState) ->
|
||||
app = currentState.local.apps[appId]
|
||||
if !app?
|
||||
throw new Error(appNotFoundMessage)
|
||||
purgedApp = _.cloneDeep(app)
|
||||
purgedApp.services = []
|
||||
purgedApp.volumes = {}
|
||||
currentState.local.apps[appId] = purgedApp
|
||||
deviceState.pausingApply ->
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.then ->
|
||||
currentState.local.apps[appId] = app
|
||||
deviceState.applyIntermediateTarget(currentState, { skipLock: true })
|
||||
.finally ->
|
||||
deviceState.triggerApplyTarget()
|
||||
.tap ->
|
||||
logger.logSystemMessage('Purged data', { appId }, 'Purge data success')
|
||||
.tapCatch (err) ->
|
||||
logger.logSystemMessage("Error purging data: #{err}", { appId, error: err }, 'Purge data error')
|
||||
|
||||
exports.serviceAction = (action, serviceId, current, target, options = {}) ->
|
||||
return { action, serviceId, current, target, options }
|
22
src/device-api/common.d.ts
vendored
Normal file
22
src/device-api/common.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import ApplicationManager from '../application-manager';
|
||||
import { Service } from '../application-manager';
|
||||
|
||||
export interface ServiceAction {
|
||||
action: string;
|
||||
serviceId: number;
|
||||
current: Service;
|
||||
target: Service;
|
||||
options: any;
|
||||
}
|
||||
|
||||
declare function doRestart(applications: ApplicationManager, appId: number, force: boolean): Promise<void>;
|
||||
|
||||
declare function doPurge(applications: ApplicationManager, appId: number, force: boolean): Promise<void>;
|
||||
|
||||
declare function serviceAction(
|
||||
action: string,
|
||||
serviceId: number,
|
||||
current: Service,
|
||||
target: Service,
|
||||
options: any,
|
||||
): ServiceAction;
|
99
src/device-api/v1.coffee
Normal file
99
src/device-api/v1.coffee
Normal file
@ -0,0 +1,99 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
|
||||
constants = require('../lib/constants')
|
||||
{ checkInt, checkTruthy } = require('../lib/validation')
|
||||
{ doRestart, doPurge, serviceAction } = require('./common')
|
||||
|
||||
exports.createV1Api = (router, applications) ->
|
||||
|
||||
{ eventTracker } = applications
|
||||
|
||||
router.post '/v1/restart', (req, res) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
eventTracker.track('Restart container (v1)', { appId })
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
doRestart(applications, appId, force)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
v1StopOrStart = (req, res, action) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
applications.getCurrentApp(appId)
|
||||
.then (app) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
return res.status(400).send('App not found')
|
||||
if app.services.length > 1
|
||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||
applications.setTargetVolatileForService(service.imageId, running: action != 'stop')
|
||||
applications.executeStepAction(serviceAction(action, service.serviceId, service, service, { wait: true }), { force })
|
||||
.then ->
|
||||
if action == 'stop'
|
||||
return service
|
||||
# We refresh the container id in case we were starting an app with no container yet
|
||||
applications.getCurrentApp(appId)
|
||||
.then (app) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
throw new Error('App not found after running action')
|
||||
return service
|
||||
.then (service) ->
|
||||
res.status(200).json({ containerId: service.containerId })
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v1/apps/:appId/stop', (req, res) ->
|
||||
v1StopOrStart(req, res, 'stop')
|
||||
|
||||
router.post '/v1/apps/:appId/start', (req, res) ->
|
||||
v1StopOrStart(req, res, 'start')
|
||||
|
||||
router.get '/v1/apps/:appId', (req, res) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
eventTracker.track('GET app (v1)', { appId })
|
||||
if !appId?
|
||||
return res.status(400).send('Missing app id')
|
||||
Promise.join(
|
||||
applications.getCurrentApp(appId)
|
||||
applications.getStatus()
|
||||
(app, status) ->
|
||||
service = app?.services?[0]
|
||||
if !service?
|
||||
return res.status(400).send('App not found')
|
||||
if app.services.length > 1
|
||||
return res.status(400).send('Some v1 endpoints are only allowed on single-container apps')
|
||||
# Don't return data that will be of no use to the user
|
||||
appToSend = {
|
||||
appId
|
||||
containerId: service.containerId
|
||||
env: _.omit(service.environment, constants.privateAppEnvVars)
|
||||
releaseId: service.releaseId
|
||||
imageId: service.image
|
||||
}
|
||||
if status.commit?
|
||||
appToSend.commit = status.commit
|
||||
res.json(appToSend)
|
||||
)
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
router.post '/v1/purge', (req, res) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
errMsg = 'Invalid or missing appId'
|
||||
return res.status(400).send(errMsg)
|
||||
doPurge(applications, appId, force)
|
||||
.then ->
|
||||
res.status(200).json(Data: 'OK', Error: '')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
202
src/device-api/v2.ts
Normal file
202
src/device-api/v2.ts
Normal file
@ -0,0 +1,202 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ApplicationManager } from '../application-manager';
|
||||
import { appNotFoundMessage, serviceNotFoundMessage } from '../lib/messages';
|
||||
import Service from '../types/service';
|
||||
import { doPurge, doRestart, serviceAction } from './common';
|
||||
|
||||
export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
|
||||
const { _lockingIfNecessary } = applications;
|
||||
|
||||
const handleServiceAction = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
action: any,
|
||||
): Bluebird<void> => {
|
||||
const { imageId, force } = req.body;
|
||||
const { appId } = req.params;
|
||||
|
||||
return _lockingIfNecessary(appId, { force }, () => {
|
||||
return applications.getCurrentApp(appId)
|
||||
.then((app) => {
|
||||
if (app == null) {
|
||||
res.status(404).send(appNotFoundMessage);
|
||||
return;
|
||||
}
|
||||
const service = _.find(app.services, { imageId }) as Service | null;
|
||||
if (service == null) {
|
||||
res.status(404).send(serviceNotFoundMessage);
|
||||
return;
|
||||
}
|
||||
applications.setTargetVolatileForService(
|
||||
service.imageId,
|
||||
{ running: action !== 'stop' },
|
||||
);
|
||||
return applications.executeStepAction(
|
||||
serviceAction(
|
||||
action,
|
||||
service.serviceId,
|
||||
service,
|
||||
service,
|
||||
{ wait: true },
|
||||
),
|
||||
{ skipLock: true },
|
||||
)
|
||||
.then(() => {
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
let message;
|
||||
if (err != null) {
|
||||
if (err.message != null) {
|
||||
message = err.message;
|
||||
} else {
|
||||
message = err;
|
||||
}
|
||||
} else {
|
||||
message = 'Unknown error';
|
||||
}
|
||||
res.status(503).send(message);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
router.post('/v2/applications/:appId/purge', (req: Request, res: Response) => {
|
||||
const { force } = req.body;
|
||||
const { appId } = req.params;
|
||||
|
||||
return doPurge(applications, appId, force)
|
||||
.then(() => {
|
||||
res.status(200).send('OK');
|
||||
})
|
||||
.catch((err) => {
|
||||
let message;
|
||||
if (err != null) {
|
||||
message = err.message;
|
||||
if (message == null) {
|
||||
message = err;
|
||||
}
|
||||
} else {
|
||||
message = 'Unknown error';
|
||||
}
|
||||
res.status(503).send(message);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v2/applications/:appId/restart-service', (req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'restart');
|
||||
});
|
||||
|
||||
router.post('/v2/applications/:appId/stop-service', (req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'stop');
|
||||
});
|
||||
|
||||
router.post('/v2/applications/:appId/start-service', (req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'start');
|
||||
});
|
||||
|
||||
router.post('/v2/applications/:appId/restart', (req: Request, res: Response) => {
|
||||
const { force } = req.body;
|
||||
const { appId } = req.params;
|
||||
|
||||
return doRestart(applications, appId, force)
|
||||
.then(() => {
|
||||
res.status(200).send('OK');
|
||||
})
|
||||
.catch((err) => {
|
||||
let message;
|
||||
if (err != null) {
|
||||
message = err.message;
|
||||
if (message == null) {
|
||||
message = err;
|
||||
}
|
||||
} else {
|
||||
message = 'Unknown error';
|
||||
}
|
||||
res.status(503).send(message);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Support dependent applications when this feature is complete
|
||||
router.get('/v2/applications/state', (_req: Request, res: Response) => {
|
||||
|
||||
// It's kinda hacky to access the services and db via the application manager
|
||||
// maybe refactor this code
|
||||
Bluebird.join(
|
||||
applications.services.getStatus(),
|
||||
applications.images.getStatus(),
|
||||
applications.db.models('app').select([ 'appId', 'commit', 'name' ]),
|
||||
(
|
||||
services,
|
||||
images,
|
||||
apps: Array<{ appId: string, commit: string, name: string }>,
|
||||
) => {
|
||||
// Create an object which is keyed my application name
|
||||
const response: {
|
||||
[appName: string]: {
|
||||
appId: number;
|
||||
commit: string;
|
||||
services: {
|
||||
[serviceName: string]: {
|
||||
status: string;
|
||||
releaseId: number;
|
||||
downloadProgress: number | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} = { };
|
||||
|
||||
const appNameById: { [id: number]: string } = { };
|
||||
|
||||
apps.forEach((app) => {
|
||||
const appId = parseInt(app.appId, 10);
|
||||
response[app.name] = {
|
||||
appId,
|
||||
commit: app.commit,
|
||||
services: { },
|
||||
};
|
||||
|
||||
appNameById[appId] = app.name;
|
||||
});
|
||||
|
||||
images.forEach((img) => {
|
||||
const appName = appNameById[img.appId];
|
||||
if (appName == null) {
|
||||
console.log('Image found for unknown application!');
|
||||
console.log(' Image: ', JSON.stringify(img));
|
||||
return;
|
||||
}
|
||||
|
||||
const svc = _.find(services, (svc: Service) => {
|
||||
return svc.imageId === img.imageId;
|
||||
});
|
||||
|
||||
let status: string;
|
||||
if (svc == null) {
|
||||
status = img.status;
|
||||
} else {
|
||||
status = svc.status;
|
||||
}
|
||||
response[appName].services[img.serviceName] = {
|
||||
status,
|
||||
releaseId: img.releaseId,
|
||||
downloadProgress: img.downloadProgress,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v2/applications/:appId/state', (_req: Request, res: Response) => {
|
||||
// Get all services and their statuses, and return it
|
||||
applications.getStatus()
|
||||
.then((apps) => {
|
||||
res.status(200).json(apps);
|
||||
});
|
||||
});
|
||||
}
|
5
src/lib/messages.ts
Normal file
5
src/lib/messages.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const appNotFoundMessage = `App not found: an app needs to be installed for this endpoint to work.
|
||||
If you've recently moved this device from another app,
|
||||
please push an app and wait for it to be installed first.`;
|
||||
|
||||
export const serviceNotFoundMessage = 'Service not found, a container must exist for this endpoint to work';
|
15
src/types/image.ts
Normal file
15
src/types/image.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface Image {
|
||||
id: number;
|
||||
name: string;
|
||||
appId: number;
|
||||
serviceId: number;
|
||||
serviceName: string;
|
||||
imageId: number;
|
||||
releaseId: number;
|
||||
dependent: number;
|
||||
dockerImageId: string;
|
||||
status: string;
|
||||
downloadProgress: number | null;
|
||||
}
|
||||
|
||||
export default Image;
|
11
src/types/service.ts
Normal file
11
src/types/service.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface Service {
|
||||
imageId: number;
|
||||
serviceId: number;
|
||||
appId: number;
|
||||
status: string;
|
||||
releaseId: number;
|
||||
createdAt: Date;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export default Service;
|
16
src/types/state.ts
Normal file
16
src/types/state.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface DeviceApplicationState {
|
||||
local: {
|
||||
[appId: string]: {
|
||||
services: {
|
||||
[serviceId: string]: {
|
||||
status: string;
|
||||
releaseId: number;
|
||||
download_progress: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// TODO
|
||||
dependent: any;
|
||||
commit: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user