diff --git a/CHANGELOG.md b/CHANGELOG.md index 901eedfd..c2a8fb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +* Add endpoint to get device state [Pablo] * Check for valid strings or ints in all config values [Pablo] * Remove quotes in OS version [Pablo] diff --git a/docs/API.md b/docs/API.md index 8505f24d..d4302702 100644 --- a/docs/API.md +++ b/docs/API.md @@ -405,3 +405,42 @@ $ curl -X POST --header "Content-Type:application/json" \ --data '{"deviceId": , "appId": }' \ "https://api.resin.io/supervisor/v1/regenerate-api-key" ``` + +
+ +### GET /v1/device + +Introduced in supervisor v1.6. +Returns the current device state, as reported to the Resin API and with some extra fields added to allow control over pending/locked updates. +The state is a JSON object that contains some or all of the following: +* `api_port`: Port on which the supervisor is listening. +* `commit`: Hash of the current commit of the application that is running. +* `ip_address`: Space-separated list of IP addresses of the device. +* `status`: Status of the device regarding the app, as a string, i.e. "Stopping", "Starting", "Downloading", "Installing", "Idle". +* `download_progress`: Amount of the application image that has been downloaded, expressed as a percentage. If the update has already been downloaded, this will be `null`. +* `os_version`: Version of the host OS running on the device. +* `supervisor_version`: Version of the supervisor running on the device. +* `update_pending`: This one is not reported to the Resin API. It's a boolean that will be true if the supervisor has detected there is a pending update. +* `update_downloaded`: Not reported to the Resin API either. Boolean that will be true if a pending update has already been downloaded. +* `update_failed`: Not reported to the Resin API. Boolean that will be true if the supervisor has tried to apply a pending update but failed (i.e. if the app was locked, there was a network failure or anything else went wrong). + +Other attributes may be added in the future, and some may be missing or null if they haven't been set yet. + +#### Examples: +From the app on the device: +```bash +$ curl -X GET --header "Content-Type:application/json" \ + "$RESIN_SUPERVISOR_ADDRESS/v1/device?apikey=$RESIN_SUPERVISOR_API_KEY" +``` +Response: +```json +{"api_port":48484,"ip_address":"192.168.0.114 10.42.0.3","commit":"414e65cd378a69a96f403b75f14b40b55856f860","status":"Downloading","download_progress":84,"os_version":"Resin OS 1.0.4 (fido)","supervisor_version":"1.6.0","update_pending":true,"update_downloaded":false,"update_failed":false} +``` + +Remotely via the API proxy: +```bash +$ curl -X POST --header "Content-Type:application/json" \ + --header "Authorization: Bearer " \ + --data '{"deviceId": , "appId": , "method": "GET"}' \ + "https://api.resin.io/supervisor/v1/device" +``` diff --git a/src/api.coffee b/src/api.coffee index 6da3fc54..b1d679b0 100644 --- a/src/api.coffee +++ b/src/api.coffee @@ -136,4 +136,7 @@ module.exports = (application) -> .catch (err) -> res.status(503).send(err?.message or err or 'Unknown error') + api.get '/v1/device', (req, res) -> + res.json(device.getState()) + return api diff --git a/src/application.coffee b/src/application.coffee index 705bb537..e3b56477 100644 --- a/src/application.coffee +++ b/src/application.coffee @@ -144,6 +144,7 @@ fetch = (app) -> .then -> logSystemEvent(logTypes.downloadAppSuccess, app) device.updateState(status: 'Idle', download_progress: null) + device.setUpdateState(update_downloaded: true) docker.getImage(app.imageId).inspectAsync() .catch (err) -> logSystemEvent(logTypes.downloadAppError, app, err) @@ -516,6 +517,8 @@ application.update = update = (force) -> resourcesForUpdate = compareForUpdate(localApps, remoteApps, localAppEnvs, remoteAppEnvs) { toBeRemoved, toBeDownloaded, toBeInstalled, toBeUpdated, appsWithChangedEnvs, allAppIds } = resourcesForUpdate + if !_.isEmpty(toBeRemoved) or !_.isEmpty(toBeInstalled) or !_.isEmpty(toBeUpdated) + device.setUpdateState(update_pending: true) # Run special functions against variables if remoteAppEnvs has the corresponding variable function mapping. Promise.map appsWithChangedEnvs, (appId) -> Promise.using lockUpdates(remoteApps[appId], force), -> @@ -578,11 +581,13 @@ application.update = update = (force) -> throw new Error(joinErrorMessages(failures)) if failures.length > 0 .then -> updateStatus.failed = 0 + device.setUpdateState(update_pending: false, update_downloaded: false, update_failed: false) # We cleanup here as we want a point when we have a consistent apps/images state, rather than potentially at a # point where we might clean up an image we still want. dockerUtils.cleanupContainersAndImages() .catch (err) -> updateStatus.failed++ + device.setUpdateState(update_failed: true) if updateStatus.state is UPDATE_REQUIRED console.log('Updating failed, but there is already another update scheduled immediately: ', err) return diff --git a/src/device.coffee b/src/device.coffee index a06e766d..dd59964f 100644 --- a/src/device.coffee +++ b/src/device.coffee @@ -128,13 +128,11 @@ exports.getDeviceType = do -> throw new Error('Device type not specified in config file') return configFromFile.deviceType -# Calling this function updates the local device state, which is then used to synchronise -# the remote device state, repeating any failed updates until successfully synchronised. -# This function will also optimise updates by merging multiple updates and only sending the latest state. -exports.updateState = do -> +do -> applyPromise = Promise.resolve() targetState = {} actualState = {} + updateState = { update_pending: false, update_failed: false, update_downloaded: false } getStateDiff = -> _.omit targetState, (value, key) -> @@ -169,7 +167,19 @@ exports.updateState = do -> # Check if any more state diffs have appeared whilst we've been processing this update. applyState() - return (updatedState = {}, retry = false) -> + exports.setUpdateState = (value) -> + _.merge(updateState, value) + + exports.getState = -> + fieldsToOmit = ['api_secret', 'logs_channel', 'provisioning_progress', 'provisioning_state'] + state = _.omit(targetState, fieldsToOmit) + _.merge(state, updateState) + return state + + # Calling this function updates the local device state, which is then used to synchronise + # the remote device state, repeating any failed updates until successfully synchronised. + # This function will also optimise updates by merging multiple updates and only sending the latest state. + exports.updateState = (updatedState = {}, retry = false) -> # Remove any updates that match the last we successfully sent. _.merge(targetState, updatedState)