Merge pull request #126 from resin-io/add-stop-start-endpoints

WIP: Add stop, start and get app endpoints
This commit is contained in:
Kostas Lekkas 2016-04-25 13:27:08 +03:00
commit 8577280dc5
4 changed files with 176 additions and 7 deletions

View File

@ -1,3 +1,5 @@
# Add endpoints to start, stop and get app [Pablo]
# v1.7.0
* Add RESIN_HOST_LOG_TO_DISPLAY variable [Pablo]

View File

@ -228,9 +228,7 @@ $ curl -X POST --header "Content-Type:application/json" \
Restarts a user application container
When successful, responds with 200 and a
```
(This is implemented in Go)
When successful, responds with 200 and an "OK"
#### Request body
Has to be a JSON object with an `appId` property, corresponding to the ID of the application the device is running.
@ -380,3 +378,109 @@ $ curl -X POST --header "Content-Type:application/json" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/device"
```
<hr>
### POST /v1/apps/:appId/stop
Introduced in supervisor v1.8.
Temporarily stops a user application container. A reboot or supervisor restart will cause the container to start again.
The container is not removed with this endpoint.
When successful, responds with 200 and the Id of the stopped container.
The appId must be specified in the URL.
#### Request body
Can contain a `force` property, which if set to `true` will cause the update lock to be overridden.
#### Examples:
From the app on the device:
```bash
$ curl -X POST --header "Content-Type:application/json" \
"$RESIN_SUPERVISOR_ADDRESS/v1/apps/<appId>/stop?apikey=$RESIN_SUPERVISOR_API_KEY"
```
Response:
```json
{"containerId":"5f4d4a857742e9ecac505ba5710834d3852ad7d71e10389fc6f61d8655a21806"}
```
Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
"https://api.resin.io/supervisor/v1/apps/<appId>/stop"
```
<hr>
### POST /v1/apps/:appId/start
Introduced in supervisor v1.8.
Starts a user application container, usually after it has been stopped with `/v1/stop`.
When successful, responds with 200 and the Id of the started container.
The appId must be specified in the URL.
#### Examples:
From the app on the device:
```bash
$ curl -X POST --header "Content-Type:application/json" \
"$RESIN_SUPERVISOR_ADDRESS/v1/apps/<appId>/start?apikey=$RESIN_SUPERVISOR_API_KEY"
```
Response:
```json
{"containerId":"6d9e1efdb9aad90fdb2df911f785b6aa00270e9448e75226a9a7361c8a9500cf"}
```
Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
"https://api.resin.io/supervisor/v1/apps/<appId>/start"
```
<hr>
### GET /v1/apps/:appId
Introduced in supervisor v1.8.
Returns the application running on the device
The app is a JSON object that contains the following:
* `appId`: The id of the app as per the Resin API.
* `commit`: Application commit that is running.
* `imageId`: The docker image of the current application build.
* `containerId`: ID of the docker container of the running app.
* `env`: A key-value store of the app's environment variables.
The appId must be specified in the URL.
#### Examples:
From the app on the device:
```bash
$ curl -X GET --header "Content-Type:application/json" \
"$RESIN_SUPERVISOR_ADDRESS/v1/apps/<appId>?apikey=$RESIN_SUPERVISOR_API_KEY"
```
Response:
```json
{"appId": 3134,"commit":"414e65cd378a69a96f403b75f14b40b55856f860","imageId":"registry.resin.io/superapp/414e65cd378a69a96f403b75f14b40b55856f860","containerId":"e5c1eace8b4e","env":{"FOO":"bar"}}
```
Remotely via the API proxy:
```bash
$ curl -X POST --header "Content-Type:application/json" \
--header "Authorization: Bearer <auth token>" \
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
"https://api.resin.io/supervisor/v1/apps/<appId>"
```

View File

@ -7,6 +7,12 @@ bodyParser = require 'body-parser'
request = require 'request'
config = require './config'
device = require './device'
_ = require 'lodash'
privateAppEnvVars = [
'RESIN_SUPERVISOR_API_KEY'
'RESIN_API_KEY'
]
module.exports = (application) ->
api = express()
@ -95,6 +101,58 @@ module.exports = (application) ->
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
api.post '/v1/apps/:appId/stop', (req, res) ->
{ appId } = req.params
{ force } = req.body
utils.mixpanelTrack('Stop container', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, force), ->
knex('app').select().where({ appId })
.then ([ app ]) ->
if !app?
throw new Error('App not found')
application.kill(app, true, false)
.then ->
res.json(_.pick(app, 'containerId'))
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
api.post '/v1/apps/:appId/start', (req, res) ->
{ appId } = req.params
utils.mixpanelTrack('Start container', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId), ->
knex('app').select().where({ appId })
.then ([ app ]) ->
if !app?
throw new Error('App not found')
application.start(app)
.then ->
res.json(_.pick(app, 'containerId'))
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
api.get '/v1/apps/:appId', (req, res) ->
{ appId } = req.params
utils.mixpanelTrack('GET app', appId)
if !appId?
return res.status(400).send('Missing app id')
Promise.using application.lockUpdates(appId, true), ->
columns = [ 'appId', 'containerId', 'commit', 'imageId', 'env' ]
knex('app').select(columns).where({ appId })
.then ([ app ]) ->
if !app?
throw new Error('App not found')
# Don't return keys on the endpoint
app.env = _.omit(JSON.parse(app.env), privateAppEnvVars)
# Don't return data that will be of no use to the user
res.json(app)
.catch (err) ->
res.status(503).send(err?.message or err or 'Unknown error')
# Expires the supervisor's API key and generates a new one.
# It also communicates the new key to the Resin API.
api.post '/v1/regenerate-api-key', (req, res) ->

View File

@ -95,13 +95,14 @@ logSpecialAction = (action, value, success) ->
application = {}
application.kill = kill = (app, updateDB = true) ->
application.kill = kill = (app, updateDB = true, removeContainer = true) ->
logSystemEvent(logTypes.stopApp, app)
device.updateState(status: 'Stopping')
container = docker.getContainer(app.containerId)
container.stopAsync(t: 10)
.then ->
container.removeAsync(v: true)
container.removeAsync(v: true) if removeContainer
return
# Bluebird throws OperationalError for errors resulting in the normal execution of a promisified function.
.catch Promise.OperationalError, (err) ->
# Get the statusCode from the original cause and make sure statusCode its definitely a string for comparison
@ -109,7 +110,8 @@ application.kill = kill = (app, updateDB = true) ->
statusCode = '' + err.statusCode
# 304 means the container was already stopped - so we can just remove it
if statusCode is '304'
return container.removeAsync(v: true)
container.removeAsync(v: true) if removeContainer
return
# 404 means the container doesn't exist, precisely what we want! :D
if statusCode is '404'
return
@ -118,7 +120,7 @@ application.kill = kill = (app, updateDB = true) ->
lockFile.unlockAsync(lockPath(app))
.tap ->
logSystemEvent(logTypes.stopAppSuccess, app)
if updateDB == true
if removeContainer && updateDB
app.containerId = null
knex('app').update(app).where(appId: app.appId)
.catch (err) ->
@ -239,6 +241,9 @@ application.start = start = (app) ->
return
# If starting the container failed, we remove it so that it doesn't litter
container.removeAsync(v: true)
.then ->
app.containerId = null
knex('app').update(app).where(appId: app.appId)
.finally ->
logSystemEvent(logTypes.startAppError, app, err)
throw err