Auto-merge for PR #652 via VersionBot

V2 device state api and fixes
This commit is contained in:
resin-io-versionbot[bot] 2018-06-26 08:15:21 +00:00 committed by GitHub
commit f8e5cd8949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 537 additions and 188 deletions

View File

@ -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]

View File

@ -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",

View File

@ -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
View 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
View File

@ -0,0 +1,7 @@
import Image from '../types/image';
declare class Images {
public getStatus(): Image[];
}
export = Images;

View File

@ -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
View 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;

View File

@ -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) {

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}